diff --git a/.gitignore b/.gitignore index 832ccc66..bdf8d011 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,9 @@ tree-sitter-rust *-E +# default world locations backups/ worlds/ + +# local config files +server.toml diff --git a/Cargo.lock b/Cargo.lock index 207753ad..d0ffb3b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5363,6 +5363,7 @@ dependencies = [ "renet_visualizer", "serde", "serde-big-array", + "toml", ] [[package]] @@ -5587,6 +5588,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "servo_arc" version = "0.4.3" @@ -5966,6 +5976,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" @@ -5996,6 +6021,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tracing" version = "0.1.44" diff --git a/Cargo.toml b/Cargo.toml index 043012ef..9be615df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ serde-big-array = "0.5.1" chrono = "0.4.43" rayon = "1.10.0" clap = { version = "4.5.54", features = ["derive"] } +toml = "0.9.11" [patch.crates-io] # TODO: Remove patch once egui requirement is more flexible. diff --git a/src/server/config/commands.rs b/src/server/config/commands.rs new file mode 100644 index 00000000..b571b8a5 --- /dev/null +++ b/src/server/config/commands.rs @@ -0,0 +1,47 @@ +use crate::prelude::*; +use std::path::Path; + +#[derive(Debug, Subcommand)] +pub enum ConfigCommands { + #[command(about = "Initialize config file with default settings")] + Init, + #[command(about = "Show currently applied configuration")] + Show, + #[command(about = "Show defaults")] + Defaults, +} + +pub fn perform_command(commands: &ConfigCommands) { + match commands { + ConfigCommands::Init => { + if Path::new(&CONFIG_PATH).is_file() { + eprintln!("Config file is already initialized at '{CONFIG_PATH}'."); + eprintln!("If you want to reinitialize it with defaults, remove it:"); + eprintln!("rm '{CONFIG_PATH}'"); + } else { + let config = Config::default(); + let config_str = + toml::to_string(&config).expect("Default config should be serializable"); + + if let Err(err) = std::fs::write(CONFIG_PATH, config_str) { + eprintln!("Error writing to file: {err}"); + } else { + println!("Initialized config file '{CONFIG_PATH}'"); + } + } + } + ConfigCommands::Show => { + println!( + "{}", + toml::to_string(&*CONFIG).expect("Loaded config should always be serializable") + ); + } + ConfigCommands::Defaults => { + println!( + "{}", + toml::to_string(&Config::default()) + .expect("Default config should always be serializable") + ); + } + } +} diff --git a/src/server/config/mod.rs b/src/server/config/mod.rs new file mode 100644 index 00000000..08f3725e --- /dev/null +++ b/src/server/config/mod.rs @@ -0,0 +1,49 @@ +use std::sync::LazyLock; + +pub mod commands; + +pub const CONFIG_PATH: &str = "server.toml"; + +pub static CONFIG: LazyLock = LazyLock::new(|| { + #[cfg(test)] + { + Config::default() + } + + #[cfg(not(test))] + { + use std::path::PathBuf; + + match std::fs::read_to_string(PathBuf::from(CONFIG_PATH)) { + Ok(string) => match toml::from_str(&string) { + Ok(config) => config, + Err(err) => { + use std::process; + + eprintln!("Could not parse config file at '{CONFIG_PATH}'"); + eprintln!("Err: {err}"); + process::exit(1); + } + }, + Err(err) => { + eprintln!( + "Could not read config file at '{CONFIG_PATH}', proceeding with defaults" + ); + eprintln!("Err: {err}"); + Config::default() + } + } + } +}); + +use serde::Deserialize; +use serde::Serialize; + +use crate::prelude::*; + +#[derive(Serialize, Deserialize, Default)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + pub world: terrain_config::WorldConfig, + pub generator: terrain_config::TerrainGeneratorParams, +} diff --git a/src/server/main.rs b/src/server/main.rs index bdb0e22e..5ac4be70 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -1,11 +1,11 @@ pub mod chat; +pub mod config; pub mod networking; pub mod player; pub mod prelude; pub mod terrain; use bevy::app::TerminalCtrlCHandlerPlugin; -use clap::Parser; #[cfg(feature = "egui_layer")] use bevy::DefaultPlugins; @@ -22,7 +22,15 @@ use crate::prelude::*; #[command(long_about = None)] struct Cli { #[command(subcommand)] - world_commands: terrain_commands::WorldCommands, + commands: MainCommands, +} + +#[derive(Debug, Subcommand)] +enum MainCommands { + #[command(flatten)] + World(terrain_commands::WorldCommands), + #[command(subcommand, about = "Actions regarding server configuration")] + Config(config_commands::ConfigCommands), } fn main() { @@ -44,13 +52,18 @@ fn main() { } 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); + match args.commands { + MainCommands::World(world_commands) => { + match terrain::TerrainPlugin::from_command(world_commands) { + Ok(terrain_plugin) => app.add_plugins(terrain_plugin), + Err(error) => return eprintln!("Error: {}", error), + }; + } + MainCommands::Config(config_commands) => { + config_commands::perform_command(&config_commands); return; } - }; + } app.add_plugins(player::PlayerPlugin); app.add_plugins(networking::NetworkingPlugin); diff --git a/src/server/prelude.rs b/src/server/prelude.rs index dad65071..e864ca83 100644 --- a/src/server/prelude.rs +++ b/src/server/prelude.rs @@ -29,6 +29,7 @@ pub use rayon::iter::IntoParallelIterator; pub use rayon::iter::IntoParallelRefMutIterator; pub use rayon::iter::ParallelIterator; +pub use clap::{Parser, Subcommand}; pub use lib::*; pub use noise::NoiseFn; pub use noise::Perlin; @@ -42,6 +43,7 @@ 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::config as terrain_config; pub use crate::terrain::events as terrain_events; pub use crate::terrain::resources as terrain_resources; pub use crate::terrain::systems as terrain_systems; @@ -50,3 +52,6 @@ pub use crate::terrain::util as terrain_util; pub use crate::chat::events as chat_events; pub use crate::chat::resources as chat_resources; pub use crate::chat::systems as chat_systems; + +pub use crate::config::commands as config_commands; +pub use crate::config::{Config, CONFIG, CONFIG_PATH}; diff --git a/src/server/terrain/commands.rs b/src/server/terrain/commands.rs index ee072bfe..e946de10 100644 --- a/src/server/terrain/commands.rs +++ b/src/server/terrain/commands.rs @@ -1,7 +1,6 @@ -use clap::Subcommand; -use rand::RngCore; - +use crate::prelude::*; use crate::terrain::TerrainPlugin; +use rand::RngCore; #[derive(Debug, Subcommand)] pub enum WorldCommands { diff --git a/src/server/terrain/config.rs b/src/server/terrain/config.rs new file mode 100644 index 00000000..f8877ff1 --- /dev/null +++ b/src/server/terrain/config.rs @@ -0,0 +1,184 @@ +use crate::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct WorldConfig { + pub backups_dir: String, + pub worlds_dir: String, + pub world_extension: String, + pub world_save_interval_seconds: i64, + pub world_backup_interval_seconds: i64, + pub spawn_area_distance: IVec3, +} + +impl Default for WorldConfig { + fn default() -> Self { + Self { + backups_dir: String::from("backups/"), + worlds_dir: String::from("worlds/"), + world_extension: String::from(".rsmcw"), + world_save_interval_seconds: 30, + world_backup_interval_seconds: 180, + spawn_area_distance: IVec3::new(4, 3, 4), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +#[derive(Default)] +pub struct TerrainGeneratorParams { + pub height: HeightParams, + pub height_adjust: HeightAdjustParams, + pub density: DensityParams, + pub cave: CaveParams, + pub tree: TreeParams, + pub grass: GrassParams, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct HeightParams { + pub noise: NoiseFunctionParams, + pub splines: Vec, +} + +impl Default for HeightParams { + fn default() -> Self { + Self { + splines: vec![ + Vec2::new(-1.0, 4.0), + Vec2::new(0.0, 0.0), + Vec2::new(0.0, 0.0), + Vec2::new(0.05, 20.0), + Vec2::new(1.0, 35.0), + ], + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 300.0, + amplitude: 30.0, + persistence: 0.5, + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct HeightAdjustParams { + pub noise: NoiseFunctionParams, +} + +impl Default for HeightAdjustParams { + fn default() -> Self { + Self { + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 120.0, + amplitude: 30.0, + persistence: 0.5, + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct DensityParams { + pub noise: NoiseFunctionParams, + pub squash_factor: f64, + pub height_offset: f64, +} + +impl Default for DensityParams { + fn default() -> Self { + DensityParams { + squash_factor: 1.0 / 100.0, + height_offset: -20.0, + noise: NoiseFunctionParams { + octaves: 4, + height: 0.0, + lacuranity: 2.0, + frequency: 1.0 / 60.0, + amplitude: 10.0, + persistence: 0.5, + }, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct CaveParams { + pub noise: NoiseFunctionParams, + pub base_value: f64, + pub threshold: f64, +} + +impl Default for CaveParams { + fn default() -> Self { + Self { + noise: NoiseFunctionParams { + octaves: 2, + height: 0.0, + lacuranity: 0.03, + frequency: 1.0 / 20.0, + amplitude: 30.0, + persistence: 0.59, + }, + base_value: 0.0, + threshold: 0.25, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct TreeParams { + pub spawn_attempts_per_chunk: u32, + pub min_stump_height: u32, + pub max_stump_height: u32, + pub min_bush_radius: u32, + pub max_bush_radius: u32, +} + +impl Default for TreeParams { + fn default() -> Self { + Self { + spawn_attempts_per_chunk: 500, + min_stump_height: 2, + max_stump_height: 20, + min_bush_radius: 3, + max_bush_radius: 5, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct GrassParams { + pub frequency: u32, +} + +impl Default for GrassParams { + fn default() -> Self { + Self { frequency: 10 } + } +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +#[serde(default, deny_unknown_fields)] +#[derive(Default)] +pub struct NoiseFunctionParams { + pub octaves: u32, + pub height: f64, + pub lacuranity: f64, + pub frequency: f64, + pub amplitude: f64, + pub persistence: f64, +} diff --git a/src/server/terrain/mod.rs b/src/server/terrain/mod.rs index 9f0b0e89..a18156e0 100644 --- a/src/server/terrain/mod.rs +++ b/src/server/terrain/mod.rs @@ -3,6 +3,7 @@ use std::io::ErrorKind::{NotFound, PermissionDenied}; use crate::{prelude::*, terrain::persistence::WorldSave}; pub mod commands; +pub mod config; pub mod events; pub mod resources; pub mod systems; diff --git a/src/server/terrain/persistence.rs b/src/server/terrain/persistence.rs index d79cf14b..10a9eb07 100644 --- a/src/server/terrain/persistence.rs +++ b/src/server/terrain/persistence.rs @@ -10,10 +10,6 @@ use std::{ 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, @@ -28,6 +24,8 @@ impl Display for WorldSave { } mod path_helpers { + use crate::config::CONFIG; + use super::*; impl WorldSave { @@ -41,8 +39,8 @@ mod path_helpers { } pub fn path_for_world(world_name: &str) -> PathBuf { - let file_name = format!("{}{}", world_name, WORLD_EXTENSION); - PathBuf::from(WORLDS_DIR).join(file_name) + let file_name = format!("{}{}", world_name, CONFIG.world.world_extension); + PathBuf::from(&CONFIG.world.worlds_dir).join(file_name) } pub fn path_for_world_backup(world_name: &str, timestamp: DateTime) -> PathBuf { @@ -50,9 +48,9 @@ mod path_helpers { "{}_{}{}.bak", world_name, timestamp.format("%Y%m%d%H%M%S%3f"), - WORLD_EXTENSION, + CONFIG.world.world_extension, ); - PathBuf::from(BACKUPS_DIR).join(file_name) + PathBuf::from(&CONFIG.world.backups_dir).join(file_name) } } diff --git a/src/server/terrain/resources.rs b/src/server/terrain/resources.rs index 9f7591a1..1455bcb6 100644 --- a/src/server/terrain/resources.rs +++ b/src/server/terrain/resources.rs @@ -1,6 +1,6 @@ -use std::collections::VecDeque; +use crate::{prelude::*, terrain::config::TerrainGeneratorParams}; -use crate::prelude::*; +use std::collections::VecDeque; use chrono::{DateTime, TimeDelta, Utc}; use rand::distr::{Alphanumeric, SampleString}; @@ -75,7 +75,9 @@ impl WorldBackupTimer { impl Default for WorldBackupTimer { fn default() -> Self { - Self(SaveTimer::new(TimeDelta::seconds(180))) + Self(SaveTimer::new(TimeDelta::seconds( + CONFIG.world.world_backup_interval_seconds, + ))) } } @@ -94,7 +96,9 @@ impl WorldSaveTimer { impl Default for WorldSaveTimer { fn default() -> Self { - Self(SaveTimer::new(TimeDelta::seconds(30))) + Self(SaveTimer::new(TimeDelta::seconds( + CONFIG.world.world_save_interval_seconds, + ))) } } @@ -123,6 +127,18 @@ pub struct Generator { pub params: TerrainGeneratorParams, } +impl Default for Generator { + fn default() -> Self { + Self::new(0) + } +} + +impl Generator { + pub fn with_seed(seed: u32) -> Self { + Self::new(seed) + } +} + #[derive(Clone)] pub struct Noise { pub seed: u32, @@ -170,143 +186,6 @@ impl<'de> Deserialize<'de> for Noise { } } -#[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, Copy, Clone, Serialize, Deserialize)] -pub struct NoiseFunctionParams { - pub octaves: u32, - pub height: f64, - pub lacuranity: f64, - pub frequency: f64, - pub amplitude: f64, - pub persistence: f64, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct TreeParams { - pub spawn_attempts_per_chunk: u32, - pub min_stump_height: u32, - pub max_stump_height: u32, - pub min_bush_radius: u32, - pub max_bush_radius: u32, -} - -impl Default for Generator { - fn default() -> Self { - Self::new(0) - } -} - -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, - pub density: DensityParams, - pub cave: CaveParams, - pub tree: TreeParams, - pub grass: GrassParams, -} - -impl Default for TerrainGeneratorParams { - fn default() -> Self { - Self { - height: HeightParams { - splines: vec![ - Vec2::new(-1.0, 4.0), - Vec2::new(0.0, 0.0), - Vec2::new(0.0, 0.0), - Vec2::new(0.05, 20.0), - Vec2::new(1.0, 35.0), - ], - noise: NoiseFunctionParams { - octaves: 4, - height: 0.0, - lacuranity: 2.0, - frequency: 1.0 / 300.0, - amplitude: 30.0, - persistence: 0.5, - }, - }, - height_adjust: HeightAdjustParams { - noise: NoiseFunctionParams { - octaves: 4, - height: 0.0, - lacuranity: 2.0, - frequency: 1.0 / 120.0, - amplitude: 30.0, - persistence: 0.5, - }, - }, - density: DensityParams { - squash_factor: 1.0 / 100.0, - height_offset: -20.0, - noise: NoiseFunctionParams { - octaves: 4, - height: 0.0, - lacuranity: 2.0, - frequency: 1.0 / 60.0, - amplitude: 10.0, - persistence: 0.5, - }, - }, - cave: CaveParams { - noise: NoiseFunctionParams { - octaves: 2, - height: 0.0, - lacuranity: 0.03, - frequency: 1.0 / 20.0, - amplitude: 30.0, - persistence: 0.59, - }, - base_value: 0.0, - threshold: 0.25, - }, - tree: TreeParams { - spawn_attempts_per_chunk: 500, - min_stump_height: 2, - max_stump_height: 20, - min_bush_radius: 3, - max_bush_radius: 5, - }, - grass: GrassParams { frequency: 10 }, - } - } -} - #[cfg(feature = "generator_visualizer")] pub use visualizer::*; diff --git a/src/server/terrain/systems.rs b/src/server/terrain/systems.rs index bfbd6ffc..0fa1187d 100644 --- a/src/server/terrain/systems.rs +++ b/src/server/terrain/systems.rs @@ -8,11 +8,10 @@ pub fn setup_world_system( mut chunk_manager: ResMut, generator: Res, ) { - let render_distance = IVec3::new(4, 3, 4); - info!("Generating chunks"); - let mut chunks = ChunkManager::instantiate_chunks(IVec3::ZERO, render_distance); + let mut chunks = + ChunkManager::instantiate_chunks(IVec3::ZERO, CONFIG.world.spawn_area_distance); chunks.par_iter_mut().for_each(|chunk| { info!("Generating chunk at {:?}", chunk.position); @@ -145,9 +144,11 @@ mod visualizer { use renet::{DefaultChannel, RenetServer}; use rsmc::{Chunk, ChunkManager, NetworkingMessage, CHUNK_SIZE}; + use crate::terrain::config::NoiseFunctionParams; + use super::{ terrain_events, - terrain_resources::{self, NoiseFunctionParams, TextureType}, + terrain_resources::{self, TextureType}, }; fn map_range(value: f64, min: f64, max: f64, new_min: f64, new_max: f64) -> f64 { diff --git a/src/server/terrain/util/generator.rs b/src/server/terrain/util/generator.rs index c9fe1d8f..9cc0d803 100644 --- a/src/server/terrain/util/generator.rs +++ b/src/server/terrain/util/generator.rs @@ -1,8 +1,9 @@ -use terrain_resources::{Generator, NoiseFunctionParams, TerrainGeneratorParams}; - use crate::{ prelude::*, - terrain::resources::{Noise, NoiseSample}, + terrain::{ + config::NoiseFunctionParams, + resources::{Generator, Noise, NoiseSample}, + }, }; macro_rules! for_each_chunk_coordinate { @@ -36,7 +37,7 @@ impl Generator { pub fn new(seed: u32) -> Generator { Generator { noise: Noise::new(seed), - params: TerrainGeneratorParams::default(), + params: CONFIG.generator.clone(), } }