diff --git a/.gitignore b/.gitignore index a6c6061f..832ccc66 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ # vim stuff tree-sitter-rust *-E + +backups/ +worlds/ diff --git a/Cargo.lock b/Cargo.lock index 98985d1d..2ce39741 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -183,6 +183,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "approx" version = "0.4.0" @@ -2133,6 +2183,46 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + [[package]] name = "clipboard-win" version = "5.4.1" @@ -2153,6 +2243,12 @@ dependencies = [ "unicode-width 0.2.2", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "combine" version = "4.6.7" @@ -3362,6 +3458,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -3581,6 +3683,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -4515,6 +4623,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -5228,6 +5342,7 @@ dependencies = [ "bincode", "cgmath", "chrono", + "clap", "egui", "egui_plot", "noise", @@ -5632,6 +5747,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -6084,6 +6205,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.19.0" diff --git a/Cargo.toml b/Cargo.toml index abe11ac3..1f215a48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ serde = { version = "1.0.203", features = ["derive"] } serde-big-array = "0.5.1" chrono = "0.4.38" rayon = "1.10.0" +clap = { version = "4.5.54", features = ["derive"] } [patch.crates-io] # TODO: Remove patch once egui requirement is more flexible. @@ -44,9 +45,6 @@ renet = {git = "https://github.com/cb341/renet.git" } [profile.dev.package."*"] opt-level = 3 -[profile.dev.package.objc2] -debug-assertions = false - [profile.release] opt-level = 3 diff --git a/README.md b/README.md index 05f66f87..8fb60b4c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ A stupid little Minecraft clone written in Rust, powered by the Bevy engine. * World update synchronization between game clients * World physics using rapier * World updates using primitive ray casting +* World saving/loading with `.rsmcw` files +* Periodic world backups in `./backups/` directory * Modular architecture using ECS ## Installation @@ -24,9 +26,8 @@ A stupid little Minecraft clone written in Rust, powered by the Bevy engine. Just run the cargo command to install the dependencies and start the game: ```bash -cargo run --bin server -cargo run --bin client - +cargo rs # run server +cargo rc # run client ``` ### More optimal setup @@ -34,20 +35,14 @@ cargo run --bin client Release Builds (for better performance): ```bash -cargo run --bin server --release -cargo run --bin client --release +cargo rs --release +cargo rc --release ``` -Dynamic Linking (to reduce compile times): -```bash -cargo run --bin server --features dynamic_linking -cargo run --bin client --features dynamic_linking -``` +Hot patch client -Automatic Reloading (with [cargo watch](https://docs.rs/crate/cargo-watch)): ```bash -cargo watch -x 'run --bin server' -cargo watch -x 'run --bin client' +bin/dev ``` ### Installation on NixOS @@ -56,10 +51,10 @@ Nix shell can be used to run the code using the given [Nix Shell Config File](./ Strongly inspired by the [Bevy NixOS installation guide](https://github.com/bevyengine/bevy/blob/latest/docs/linux_dependencies.md) ```bash -nix-shell --run "cargo run --bin server" -nix-shell --run "cargo run --bin client" +nix-shell --run "cargo rs" +nix-shell --run "cargo rc" ``` ## Notes -Checkout the [Wiki](https://github.com/CuddlyBunion341/rsmc/wiki) for additional project information. +Checkout the [Wiki](https://github.com/cb341/rsmc/wiki) for additional project information. diff --git a/src/client/terrain/systems.rs b/src/client/terrain/systems.rs index bed45ff4..5c000e37 100644 --- a/src/client/terrain/systems.rs +++ b/src/client/terrain/systems.rs @@ -89,7 +89,7 @@ pub fn handle_chunk_mesh_update_events_system( "Received chunk mesh update event for chunk {:?}", event.chunk_position ); - let chunk_option = chunk_manager.get_chunk(event.chunk_position); + let chunk_option = chunk_manager.get_chunk(&event.chunk_position); match chunk_option { Some(chunk) => { tasks.task_list.push(FutureChunkMesh { diff --git a/src/server/main.rs b/src/server/main.rs index c0535ad8..bdb0e22e 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -4,6 +4,9 @@ pub mod player; pub mod prelude; pub mod terrain; +use bevy::app::TerminalCtrlCHandlerPlugin; +use clap::Parser; + #[cfg(feature = "egui_layer")] use bevy::DefaultPlugins; #[cfg(feature = "egui_layer")] @@ -14,8 +17,18 @@ use bevy::log::LogPlugin; use crate::prelude::*; +#[derive(Debug, Parser)] +#[command(version)] +#[command(long_about = None)] +struct Cli { + #[command(subcommand)] + world_commands: terrain_commands::WorldCommands, +} + fn main() { let mut app = App::new(); + app.add_plugins(TerminalCtrlCHandlerPlugin); + #[cfg(not(feature = "egui_layer"))] { app.add_plugins(MinimalPlugins); @@ -30,12 +43,21 @@ fn main() { app.add_systems(Startup, gui::setup_camera_system); } + let args = Cli::parse(); + match terrain::TerrainPlugin::from_command(args.world_commands) { + Ok(terrain_plugin) => app.add_plugins(terrain_plugin), + Err(error) => { + eprintln!("Error: {}", error); + return; + } + }; + app.add_plugins(player::PlayerPlugin); app.add_plugins(networking::NetworkingPlugin); - app.add_plugins(terrain::TerrainPlugin); #[cfg(feature = "chat")] app.add_plugins(chat::ChatPlugin); + println!("Server is starting!"); app.run(); } diff --git a/src/server/networking/systems.rs b/src/server/networking/systems.rs index ece6d5be..ed2a2777 100644 --- a/src/server/networking/systems.rs +++ b/src/server/networking/systems.rs @@ -4,6 +4,7 @@ pub fn receive_message_system( mut server: ResMut, mut player_states: ResMut, mut past_block_updates: ResMut, + mut chunk_manager: ResMut, mut request_queue: ResMut, #[cfg(feature = "chat")] mut chat_message_events: MessageWriter< chat_events::PlayerChatMessageSendEvent, @@ -20,6 +21,7 @@ pub fn receive_message_system( "Received block update from client {} {} {:?}", client_id, position, block ); + chunk_manager.update_block(position, block); past_block_updates .updates .push(terrain_events::BlockUpdateEvent { position, block }); diff --git a/src/server/prelude.rs b/src/server/prelude.rs index e687b684..dcb21d1f 100644 --- a/src/server/prelude.rs +++ b/src/server/prelude.rs @@ -4,7 +4,7 @@ pub use std::net::UdpSocket; pub use std::time::SystemTime; // bevy crates -pub use bevy::app::{App, Plugin, Startup, Update}; +pub use bevy::app::{App, Last, Plugin, PreUpdate, Startup, Update}; pub use bevy::ecs::event::*; pub use bevy::ecs::message::Message; pub use bevy::ecs::message::*; @@ -40,6 +40,7 @@ pub use crate::networking::systems as networking_systems; pub use crate::player::resources as player_resources; pub use crate::player::systems as player_systems; +pub use crate::terrain::commands as terrain_commands; pub use crate::terrain::events as terrain_events; pub use crate::terrain::resources as terrain_resources; pub use crate::terrain::systems as terrain_systems; diff --git a/src/server/terrain/commands.rs b/src/server/terrain/commands.rs new file mode 100644 index 00000000..ee072bfe --- /dev/null +++ b/src/server/terrain/commands.rs @@ -0,0 +1,38 @@ +use clap::Subcommand; +use rand::RngCore; + +use crate::terrain::TerrainPlugin; + +#[derive(Debug, Subcommand)] +pub enum WorldCommands { + #[command(about = "Generate a new world with the given name")] + GenerateWorld { + #[arg(required = true)] + world_name: String, + #[arg(short, long = "replace", help = "Replace existing")] + replace_existing: bool, + #[arg(short, long, help = "Seed for world generation")] + seed: Option, + }, + #[command(about = "Load an existing world from disk")] + LoadWorld { + #[arg()] + world_name: String, + }, +} + +impl TerrainPlugin { + pub fn from_command(command: WorldCommands) -> Result { + match command { + WorldCommands::GenerateWorld { + world_name, + replace_existing, + seed, + } => { + let seed = seed.unwrap_or_else(|| rand::rng().next_u32()); + Self::new_with_seed(world_name, replace_existing, seed) + } + WorldCommands::LoadWorld { world_name } => Self::load_from_save(&world_name), + } + } +} diff --git a/src/server/terrain/mod.rs b/src/server/terrain/mod.rs index bd174a7a..9f0b0e89 100644 --- a/src/server/terrain/mod.rs +++ b/src/server/terrain/mod.rs @@ -1,20 +1,85 @@ -use crate::prelude::*; +use std::io::ErrorKind::{NotFound, PermissionDenied}; +use crate::{prelude::*, terrain::persistence::WorldSave}; + +pub mod commands; pub mod events; pub mod resources; pub mod systems; pub mod util; -pub struct TerrainPlugin; +mod persistence; + +pub enum TerrainStrategy { + SeededRandom(String, u32), + LoadFromSave(Box), +} + +pub struct TerrainPlugin { + strategy: TerrainStrategy, +} + +impl TerrainPlugin { + pub fn load_from_save(world_name: &str) -> Result { + println!("Loading world '{}'...", world_name); + let world_save = + persistence::read_world_save_by_name(world_name).map_err(|err| match err.kind() { + NotFound => format!( + "Save File '{}' not found. Make sure it is located within 'worlds/' directory", + world_name + ), + PermissionDenied => format!( + "Permission denied. Check file permissions '{}'.", + world_name + ), + _ => format!("Unknown Error loading file: {}", err), + })?; + + Ok(Self { + strategy: TerrainStrategy::LoadFromSave(Box::new(world_save)), + }) + } + + pub fn new_with_seed(world_name: String, replace: bool, seed: u32) -> Result { + if !replace && persistence::world_save_exists(&world_name) { + Err(format!( + "World Save '{}' already exists, pass replace flag if you want to replace it", + world_name + )) + } else { + Ok(Self { + strategy: TerrainStrategy::SeededRandom(world_name, seed), + }) + } + } +} impl Plugin for TerrainPlugin { fn build(&self, app: &mut App) { - app.insert_resource(ChunkManager::new()); + match &self.strategy { + TerrainStrategy::SeededRandom(world_name, seed) => { + println!("Generating new world '{}' with seed [{}]", world_name, seed); + + app.insert_resource(resources::AutoSaveName::with_name(world_name.clone())); + app.insert_resource(ChunkManager::new()); + app.insert_resource(resources::Generator::with_seed(*seed)); + app.add_systems(Startup, terrain_systems::setup_world_system); + } + TerrainStrategy::LoadFromSave(world_save) => { + app.insert_resource(resources::AutoSaveName::with_name(world_save.name.clone())); + app.insert_resource(ChunkManager::with_chunks(world_save.chunks.clone())); + app.insert_resource(world_save.generator.clone()); + } + } + app.add_message::(); - app.insert_resource(resources::PastBlockUpdates::new()); - app.add_systems(Startup, terrain_systems::setup_world_system); + app.insert_resource(resources::PastBlockUpdates::default()); + app.insert_resource(resources::WorldBackupTimer::default()); + app.insert_resource(resources::WorldSaveTimer::default()); app.add_systems(Update, terrain_systems::process_user_chunk_requests_system); - app.insert_resource(resources::Generator::default()); + app.add_systems(Update, terrain_systems::save_world_system); + app.add_systems(Update, terrain_systems::backup_world_system); + app.add_systems(Last, terrain_systems::save_world_on_shutdown_system); app.insert_resource(resources::ClientChunkRequests::default()); #[cfg(feature = "generator_visualizer")] diff --git a/src/server/terrain/persistence.rs b/src/server/terrain/persistence.rs new file mode 100644 index 00000000..775346a8 --- /dev/null +++ b/src/server/terrain/persistence.rs @@ -0,0 +1,169 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use std::{ + error::Error, + fmt::Display, + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, +}; + +use crate::{prelude::*, terrain::resources::Generator}; + +const BACKUPS_DIR: &str = "backups/"; +const WORLDS_DIR: &str = "worlds/"; +const WORLD_EXTENSION: &str = ".rsmcw"; + +#[derive(Serialize, Deserialize, Default)] +pub struct WorldSave { + pub name: String, + pub generator: Generator, + pub chunks: Vec, +} + +impl Display for WorldSave { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}[{} chunks]", self.name, self.chunks.len()) + } +} + +mod path_helpers { + use super::*; + + impl WorldSave { + pub fn backup_path(&self) -> PathBuf { + path_for_world_backup(&self.name, Utc::now()) + } + + pub fn save_path(&self) -> PathBuf { + path_for_world(&self.name) + } + } + + pub fn path_for_world(world_name: &str) -> PathBuf { + let file_name = format!("{}{}", world_name, WORLD_EXTENSION); + PathBuf::from(WORLDS_DIR).join(file_name) + } + + pub fn path_for_world_backup(world_name: &str, timestamp: DateTime) -> PathBuf { + let file_name = format!( + "{}_{}{}.bak", + world_name, + timestamp.format("%Y%m%d%H%M%S%3f"), + WORLD_EXTENSION, + ); + PathBuf::from(BACKUPS_DIR).join(file_name) + } +} + +fn create_or_update_file(world_save: &WorldSave, path: &Path) -> Result<(), Box> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let mut file = File::create(path)?; + let serialized = bincode::serialize(world_save)?; + file.write_all(&serialized)?; + file.flush()?; + + Ok(()) +} + +fn build_world_save_from_resources( + name: &str, + chunk_manager: &ChunkManager, + generator: &Generator, +) -> WorldSave { + let chunks = chunk_manager.all_chunks().into_iter().copied().collect(); + let generator = generator.clone(); + + WorldSave { + name: String::from(name), + generator, + chunks, + } +} + +fn create_backup(world_save: &WorldSave) -> Result<(), Box> { + let path = world_save.backup_path(); + create_or_update_file(world_save, &path)?; + println!("Saved world backup to: '{}'", path.display()); + Ok(()) +} + +fn update_world_file(world_save: &WorldSave) -> Result<(), Box> { + let path = world_save.save_path(); + create_or_update_file(world_save, &path)?; + println!("Updated world file: '{}'", path.display()); + Ok(()) +} + +pub use ecs_api::*; +pub mod ecs_api { + use super::*; + + pub fn world_save_exists(name: &str) -> bool { + let path = path_helpers::path_for_world(name); + Path::new(&path).is_file() + } + + pub fn save_world( + name: &str, + chunk_manager: &ChunkManager, + generator: &Generator, + ) -> Result<(), Box> { + let world_save = build_world_save_from_resources(name, chunk_manager, generator); + update_world_file(&world_save) + } + + pub fn backup_world( + name: &str, + chunk_manager: &ChunkManager, + generator: &Generator, + ) -> Result<(), Box> { + let world_save = build_world_save_from_resources(name, chunk_manager, generator); + create_backup(&world_save) + } + + pub fn read_world_save_by_name(name: &str) -> Result { + let path = path_helpers::path_for_world(name); + read_world_by_path(&path) + } +} + +fn read_world_by_path(path: &Path) -> Result { + let buffer = std::fs::read(path)?; + let world_save: WorldSave = + bincode::deserialize(&buffer).expect("World Save should to be deserializable"); + + Ok(world_save) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_save_and_read_generated_world_from_disk() { + let mut generator = Generator::with_seed(0); + generator.params.density.squash_factor = 6.7; + + let mut chunk_manager = ChunkManager::new(); + let mut chunks = ChunkManager::instantiate_chunks(IVec3::ZERO, IVec3::ONE); + + assert!(!chunks.is_empty()); + + chunks.par_iter_mut().for_each(|chunk| { + generator.generate_chunk(chunk); + }); + + chunk_manager.insert_chunks(chunks); + save_world("my_world", &chunk_manager, &generator).unwrap(); + + let world = read_world_save_by_name("my_world").unwrap(); + + assert!(!world.chunks.is_empty()); + assert_eq!(world.name, "my_world"); + assert_eq!(world.generator.params.density.squash_factor, 6.7); + } +} diff --git a/src/server/terrain/resources.rs b/src/server/terrain/resources.rs index 8215373d..5881d34a 100644 --- a/src/server/terrain/resources.rs +++ b/src/server/terrain/resources.rs @@ -2,6 +2,9 @@ use std::collections::VecDeque; use crate::prelude::*; +use chrono::{DateTime, TimeDelta, Utc}; +use rand::distr::{Alphanumeric, SampleString}; +use serde::{Deserialize, Serialize}; use terrain_events::BlockUpdateEvent; #[derive(Resource, Default)] @@ -30,57 +33,131 @@ impl ClientChunkRequests { } #[derive(Resource)] -pub struct PastBlockUpdates { - pub updates: Vec, -} +pub struct AutoSaveName(pub String); -impl Default for PastBlockUpdates { - fn default() -> Self { - Self::new() +impl AutoSaveName { + pub fn with_name(name: String) -> Self { + Self(name) + } + + pub fn with_random() -> Self { + Self(Alphanumeric.sample_string(&mut rand::rng(), 16)) } } -impl PastBlockUpdates { - pub fn new() -> Self { - Self { - updates: Vec::new(), +#[derive(Resource)] +struct SaveTimer { + pub last_autosave_timestamp: DateTime, + interval: chrono::TimeDelta, +} + +impl SaveTimer { + pub fn new(interval: TimeDelta) -> SaveTimer { + SaveTimer { + last_autosave_timestamp: Utc::now(), + interval, } } } #[derive(Resource)] +pub struct WorldBackupTimer(SaveTimer); + +impl WorldBackupTimer { + pub fn reset(&mut self) { + self.0.reset() + } + + pub fn is_ready(&self) -> bool { + self.0.is_ready() + } +} + +impl Default for WorldBackupTimer { + fn default() -> Self { + Self(SaveTimer::new(TimeDelta::seconds(180))) + } +} + +#[derive(Resource)] +pub struct WorldSaveTimer(SaveTimer); + +impl WorldSaveTimer { + pub fn reset(&mut self) { + self.0.reset() + } + + pub fn is_ready(&self) -> bool { + self.0.is_ready() + } +} + +impl Default for WorldSaveTimer { + fn default() -> Self { + Self(SaveTimer::new(TimeDelta::seconds(30))) + } +} + +impl SaveTimer { + pub fn reset(&mut self) { + self.last_autosave_timestamp = Utc::now(); + } + + pub fn is_ready(&self) -> bool { + let timer_ready_timestamp = self + .last_autosave_timestamp + .checked_add_signed(self.interval) + .expect("Time should never be out of range"); + timer_ready_timestamp < Utc::now() + } +} + +#[derive(Resource, Default)] +pub struct PastBlockUpdates { + pub updates: Vec, +} + +#[derive(Resource, Clone, Serialize, Deserialize)] pub struct Generator { pub seed: u32, - pub perlin: Perlin, pub params: TerrainGeneratorParams, + + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + pub perlin: Perlin, } +#[derive(Clone, Serialize, Deserialize)] pub struct HeightParams { pub noise: NoiseFunctionParams, pub splines: Vec, } +#[derive(Clone, Serialize, Deserialize)] pub struct DensityParams { pub noise: NoiseFunctionParams, pub squash_factor: f64, pub height_offset: f64, } +#[derive(Clone, Serialize, Deserialize)] pub struct CaveParams { pub noise: NoiseFunctionParams, pub base_value: f64, pub threshold: f64, } +#[derive(Clone, Serialize, Deserialize)] pub struct HeightAdjustParams { pub noise: NoiseFunctionParams, } +#[derive(Clone, Serialize, Deserialize)] pub struct GrassParams { pub frequency: u32, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct NoiseFunctionParams { pub octaves: u32, pub height: f64, @@ -90,6 +167,7 @@ pub struct NoiseFunctionParams { pub persistence: f64, } +#[derive(Clone, Serialize, Deserialize)] pub struct TreeParams { pub spawn_attempts_per_chunk: u32, pub min_stump_height: u32, @@ -104,6 +182,13 @@ impl Default for Generator { } } +impl Generator { + pub fn with_seed(seed: u32) -> Self { + Self::new(seed) + } +} + +#[derive(Clone, Serialize, Deserialize)] pub struct TerrainGeneratorParams { pub height: HeightParams, pub height_adjust: HeightAdjustParams, diff --git a/src/server/terrain/systems.rs b/src/server/terrain/systems.rs index fed5a7ef..599c40b5 100644 --- a/src/server/terrain/systems.rs +++ b/src/server/terrain/systems.rs @@ -1,12 +1,14 @@ +use crate::{ + prelude::*, + terrain::{persistence::*, resources::Generator}, +}; use std::cmp::min; -use crate::prelude::*; - pub fn setup_world_system( mut chunk_manager: ResMut, generator: Res, ) { - let render_distance = IVec3::new(8, 3, 8); + let render_distance = IVec3::new(4, 3, 4); info!("Generating chunks"); @@ -39,7 +41,7 @@ pub fn process_user_chunk_requests_system( let chunks = positions_to_process .into_par_iter() .map(|position| { - let chunk = chunk_manager.get_chunk(position); + let chunk = chunk_manager.get_chunk(&position); match chunk { Some(chunk) => *chunk, @@ -64,6 +66,49 @@ pub fn process_user_chunk_requests_system( }); } +pub fn save_world_system( + chunk_manager: Res, + generator: Res, + world_name: ResMut, + mut timer: ResMut, +) { + if timer.is_ready() { + info!("Saving world..."); + if save_world(&world_name.0, &chunk_manager, &generator).is_ok() { + timer.reset(); + } + } +} + +pub fn backup_world_system( + chunk_manager: Res, + generator: Res, + world_name: ResMut, + mut timer: ResMut, +) { + if timer.is_ready() { + println!("Backing up world..."); + if backup_world(&world_name.0, &chunk_manager, &generator).is_ok() { + timer.reset(); + } + } +} + +pub fn save_world_on_shutdown_system( + chunk_manager: Res, + generator: Res, + world_name: ResMut, + mut exit_events: MessageReader, +) { + if exit_events.read().count() != 0 { + match save_world(&world_name.0, &chunk_manager, &generator) { + Ok(_) => println!("Saved world before exiting"), + Err(err) => eprintln!("Error saving world: {}", err), + } + } +} + +use bevy::app::AppExit; #[cfg(feature = "generator_visualizer")] pub use visualizer::*; diff --git a/src/shared/chunk/manager.rs b/src/shared/chunk/manager.rs index 28009961..7425055b 100644 --- a/src/shared/chunk/manager.rs +++ b/src/shared/chunk/manager.rs @@ -22,6 +22,15 @@ impl ChunkManager { } } + pub fn with_chunks(chunks: Vec) -> Self { + let chunks: HashMap = chunks + .into_iter() + .map(|chunk| (chunk.position, chunk)) + .collect(); + + Self { chunks } + } + pub fn instantiate_chunks(position: IVec3, render_distance: IVec3) -> Vec { let render_distance_x = render_distance.x; let render_distance_y = render_distance.y; @@ -73,8 +82,8 @@ impl ChunkManager { self.chunks.insert(position, chunk); } - pub fn get_chunk(&self, position: IVec3) -> Option<&Chunk> { - self.chunks.get(&position) + pub fn get_chunk(&self, position: &IVec3) -> Option<&Chunk> { + self.chunks.get(position) } pub fn get_chunk_mut(&mut self, position: &IVec3) -> Option<&mut Chunk> { @@ -177,6 +186,10 @@ impl ChunkManager { .map(|key| IVec3::new(key[0], key[1], key[2])) .collect() } + + pub fn all_chunks(&self) -> Vec<&Chunk> { + self.chunks.values().collect() + } } #[cfg(test)] diff --git a/src/shared/chunk_serializer.rs b/src/shared/chunk_serializer.rs index bdcb1e15..e6e6d067 100644 --- a/src/shared/chunk_serializer.rs +++ b/src/shared/chunk_serializer.rs @@ -20,8 +20,8 @@ impl Serialize for Chunk { block_byte }) .collect(); - let serialized_data = serialize_buffer(data_as_u8); let mut state = serializer.serialize_struct("Chunk", 2)?; + let serialized_data = serialize_buffer(data_as_u8); state.serialize_field("data", &serialized_data)?; state.serialize_field("position", &self.position)?; state.end()