diff --git a/.gitignore b/.gitignore index 2dfe7084fd..f2ba2f0490 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ run.sh maps screenshots todo.txt +userdata # Export data *.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e37153835..dcaeaea728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Overhauled representation of blocks to permit fluid and sprite coexistence - Overhauled sword - Reworked healing sceptre +- Split out the sections of the server settings that can be edtited and saved by the server. +- Revamped structure of where settings, logs, and game saves are stored so that almost everything is in one place. ### Removed diff --git a/Cargo.lock b/Cargo.lock index e45c6f8c12..95b1746eff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4643,6 +4643,7 @@ dependencies = [ "authc", "criterion", "crossbeam", + "directories-next", "dot_vox", "enum-iterator", "hashbrown", diff --git a/common/Cargo.toml b/common/Cargo.toml index 6217569469..1152da8da9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -15,6 +15,7 @@ specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git", branch = "spec roots = "0.0.6" specs = { git = "https://github.com/amethyst/specs.git", features = ["serde", "storage-event-control"], rev = "7a2e348ab2223818bad487695c66c43db88050a5" } vek = { version = "0.12.0", features = ["platform_intrinsics", "serde"] } +directories-next = "1.0.1" dot_vox = "4.0" image = { version = "0.23.8", default-features = false, features = ["png"] } serde = { version = "1.0.110", features = ["derive", "rc"] } diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index 0167fda1ce..8c4578d4e9 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -1,6 +1,7 @@ mod color; mod dir; mod option; +pub mod userdata_dir; pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash")); pub const GIT_TAG: &str = include_str!(concat!(env!("OUT_DIR"), "/gittag")); diff --git a/common/src/util/userdata_dir.rs b/common/src/util/userdata_dir.rs new file mode 100644 index 0000000000..24b7b304e4 --- /dev/null +++ b/common/src/util/userdata_dir.rs @@ -0,0 +1,76 @@ +use std::path::PathBuf; + +const VELOREN_USERDATA_ENV: &'static str = "VELOREN_USERDATA"; + +// TODO: consider expanding this to a general install strategy variable that is also used for +// finding assets +/// # `VELOREN_USERDATA_STRATEGY` environment variable +/// Read during compilation +/// Useful to set when compiling for distribution +/// "system" => system specific project data directory +/// "executable" => /userdata +/// Note: case insensitive + +/// Determines common user data directory used by veloren frontends +/// The first specified in this list is used +/// 1. The VELOREN_USERDATA environment variable +/// 2. The VELOREN_USERDATA_STRATEGY environment variable +/// 3. The CARGO_MANIFEST_DIR/userdata or CARGO_MANIFEST_DIR/../userdata depending on if a +/// workspace if being used +pub fn userdata_dir(workspace: bool, strategy: Option<&str>, manifest_dir: &str) -> PathBuf { + // 1. The VELOREN_USERDATA environment variable + std::env::var_os(VELOREN_USERDATA_ENV) + .map(PathBuf::from) + // 2. The VELOREN_USERDATA_STRATEGY environment variable + .or_else(|| match strategy { + // "system" => system specific project data directory + Some(s) if s.eq_ignore_ascii_case("system") => Some(directories_next::ProjectDirs::from("net", "veloren", "userdata") + .expect("System's $HOME directory path not found!") + .data_dir() + .to_owned() + ), + // "executable" => /userdata + Some(s) if s.eq_ignore_ascii_case("executable") => { + let mut path = std::env::current_exe() + .expect("Failed to retrieve executable directory!"); + path.pop(); + path.push("userdata"); + Some(path) + }, + Some(_) => None, // TODO: panic? catch during compilation? + _ => None, + + }) + // 3. The CARGO_MANIFEST_DIR/userdata or CARGO_MANIFEST_DIR/../userdata depending on if a + // workspace if being used + .unwrap_or_else(|| { + let mut path = PathBuf::from(manifest_dir); + if workspace { + path.pop(); + } + path.push("userdata"); + path + }) +} + +#[macro_export] +macro_rules! userdata_dir_workspace { + () => { + $crate::util::userdata_dir::userdata_dir( + true, + option_env!("VELOREN_USERDATA_STRATEGY"), env!("CARGO_MANIFEST_DIR") + ) + }; +} + +#[macro_export] +macro_rules! userdata_dir_no_workspace { + () => { + $crate::util::userdata_dir::userdata_dir( + false, + option_env!("VELOREN_USERDATA_STRATEGY"), env!("CARGO_MANIFEST_DIR") + ) + }; +} + + diff --git a/server-cli/src/logging.rs b/server-cli/src/logging.rs new file mode 100644 index 0000000000..d3fb675587 --- /dev/null +++ b/server-cli/src/logging.rs @@ -0,0 +1,58 @@ +use crate::tuilog::TuiLog; +use tracing::Level; +use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; +#[cfg(feature = "tracy")] +use tracing_subscriber::{layer::SubscriberExt, prelude::*}; + +const RUST_LOG_ENV: &str = "RUST_LOG"; + +lazy_static::lazy_static! { + pub static ref LOG: TuiLog<'static> = TuiLog::default(); +} + +pub fn init(basic: bool) { + // Init logging + let base_exceptions = |env: EnvFilter| { + env.add_directive("veloren_world::sim=info".parse().unwrap()) + .add_directive("veloren_world::civ=info".parse().unwrap()) + .add_directive("uvth=warn".parse().unwrap()) + .add_directive("tiny_http=warn".parse().unwrap()) + .add_directive("mio::sys::windows=debug".parse().unwrap()) + .add_directive(LevelFilter::INFO.into()) + }; + + #[cfg(not(feature = "tracy"))] + let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { + Some(Ok(env)) => { + let mut filter = base_exceptions(EnvFilter::new("")); + for s in env.split(',').into_iter() { + match s.parse() { + Ok(d) => filter = filter.add_directive(d), + Err(err) => println!("WARN ignoring log directive: `{}`: {}", s, err), + }; + } + filter + }, + _ => base_exceptions(EnvFilter::from_env(RUST_LOG_ENV)), + }; + + #[cfg(feature = "tracy")] + tracing_subscriber::registry() + .with(tracing_tracy::TracyLayer::new().with_stackdepth(0)) + .init(); + + #[cfg(not(feature = "tracy"))] + // TODO: when tracing gets per Layer filters re-enable this when the tracy feature is being + // used (and do the same in voxygen) + { + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::ERROR) + .with_env_filter(filter); + + if basic { + subscriber.init(); + } else { + subscriber.with_writer(|| LOG.clone()).init(); + } + } +} diff --git a/server-cli/src/main.rs b/server-cli/src/main.rs index 7723f1ec91..6cf6cf3641 100644 --- a/server-cli/src/main.rs +++ b/server-cli/src/main.rs @@ -2,39 +2,28 @@ #![deny(clippy::clone_on_ref_ptr)] #![feature(bool_to_option)] +mod logging; mod shutdown_coordinator; mod tui_runner; mod tuilog; -#[macro_use] extern crate lazy_static; - use crate::{ shutdown_coordinator::ShutdownCoordinator, tui_runner::{Message, Tui}, - tuilog::TuiLog, }; +use clap::{App, Arg}; use common::clock::Clock; -use server::{Event, Input, Server, ServerSettings}; +use server::{DataDir, Event, Input, Server, ServerSettings}; #[cfg(any(target_os = "linux", target_os = "macos"))] use signal_hook::SIGUSR1; -use tracing::{info, Level}; -use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; -#[cfg(feature = "tracy")] -use tracing_subscriber::{layer::SubscriberExt, prelude::*}; - -use clap::{App, Arg}; use std::{ io, sync::{atomic::AtomicBool, mpsc, Arc}, time::Duration, }; +use tracing::info; const TPS: u64 = 30; -const RUST_LOG_ENV: &str = "RUST_LOG"; - -lazy_static! { - static ref LOG: TuiLog<'static> = TuiLog::default(); -} fn main() -> io::Result<()> { let matches = App::new("Veloren server cli") @@ -67,50 +56,7 @@ fn main() -> io::Result<()> { #[cfg(any(target_os = "linux", target_os = "macos"))] let _ = signal_hook::flag::register(SIGUSR1, Arc::clone(&sigusr1_signal)); - // Init logging - let base_exceptions = |env: EnvFilter| { - env.add_directive("veloren_world::sim=info".parse().unwrap()) - .add_directive("veloren_world::civ=info".parse().unwrap()) - .add_directive("uvth=warn".parse().unwrap()) - .add_directive("tiny_http=warn".parse().unwrap()) - .add_directive("mio::sys::windows=debug".parse().unwrap()) - .add_directive(LevelFilter::INFO.into()) - }; - - #[cfg(not(feature = "tracy"))] - let filter = match std::env::var_os(RUST_LOG_ENV).map(|s| s.into_string()) { - Some(Ok(env)) => { - let mut filter = base_exceptions(EnvFilter::new("")); - for s in env.split(',').into_iter() { - match s.parse() { - Ok(d) => filter = filter.add_directive(d), - Err(err) => println!("WARN ignoring log directive: `{}`: {}", s, err), - }; - } - filter - }, - _ => base_exceptions(EnvFilter::from_env(RUST_LOG_ENV)), - }; - - #[cfg(feature = "tracy")] - tracing_subscriber::registry() - .with(tracing_tracy::TracyLayer::new().with_stackdepth(0)) - .init(); - - #[cfg(not(feature = "tracy"))] - // TODO: when tracing gets per Layer filters re-enable this when the tracy feature is being - // used (and do the same in voxygen) - { - let subscriber = FmtSubscriber::builder() - .with_max_level(Level::ERROR) - .with_env_filter(filter); - - if basic { - subscriber.init(); - } else { - subscriber.with_writer(|| LOG.clone()).init(); - } - } + logging::init(basic); // Panic hook to ensure that console mode is set back correctly if in non-basic // mode @@ -127,17 +73,25 @@ fn main() -> io::Result<()> { // Set up an fps clock let mut clock = Clock::start(); + // Determine folder to save server data in + let server_data_dir = DataDir::from({ + let mut path = common::userdata_dir_workspace!(); + path.push(server::DEFAULT_DATA_DIR_NAME); + path + }); + // Load settings - let mut settings = ServerSettings::load(); - // TODO: make settings file immutable so that this does not overwrite the - // settings + let mut settings = ServerSettings::load(server_data_dir.as_ref()); + if no_auth { settings.auth_server_address = None; } + let server_port = &settings.gameserver_address.port(); let metrics_port = &settings.metrics_address.port(); // Create server - let mut server = Server::new(settings).expect("Failed to create server instance!"); + let mut server = + Server::new(settings, server_data_dir).expect("Failed to create server instance!"); info!( ?server_port, diff --git a/server-cli/src/tui_runner.rs b/server-cli/src/tui_runner.rs index b867d949e8..1c17788ecd 100644 --- a/server-cli/src/tui_runner.rs +++ b/server-cli/src/tui_runner.rs @@ -1,4 +1,4 @@ -use crate::LOG; +use crate::logging::LOG; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture}, execute, diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5daa4bd384..925102da86 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -2,7 +2,7 @@ //! To implement a new command, add an instance of `ChatCommand` to //! `CHAT_COMMANDS` and provide a handler function. -use crate::{client::Client, Server, StateExt}; +use crate::{client::Client, settings::EditableSetting, Server, StateExt}; use chrono::{NaiveTime, Timelike}; use common::{ cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS}, @@ -266,7 +266,7 @@ fn handle_motd( ) { server.notify_client( client, - ChatType::CommandError.server_msg(server.settings().server_description.clone()), + ChatType::CommandError.server_msg((**server.server_description()).clone()), ); } @@ -277,18 +277,21 @@ fn handle_set_motd( args: String, action: &ChatCommand, ) { + let data_dir = server.data_dir(); match scan_fmt!(&args, &action.arg_fmt(), String) { Ok(msg) => { server - .settings_mut() - .edit(|s| s.server_description = msg.clone()); + .server_description_mut() + .edit(data_dir.as_ref(), |d| **d = msg.clone()); server.notify_client( client, ChatType::CommandError.server_msg(format!("Server description set to \"{}\"", msg)), ); }, Err(_) => { - server.settings_mut().edit(|s| s.server_description.clear()); + server + .server_description_mut() + .edit(data_dir.as_ref(), |d| d.clear()); server.notify_client( client, ChatType::CommandError.server_msg("Removed server description".to_string()), @@ -1825,17 +1828,18 @@ fn handle_whitelist( if let Ok((whitelist_action, username)) = scan_fmt!(&args, &action.arg_fmt(), String, String) { if whitelist_action.eq_ignore_ascii_case("add") { server - .settings_mut() - .edit(|s| s.whitelist.push(username.clone())); + .whitelist_mut() + .edit(server.data_dir().as_ref(), |w| w.push(username.clone())); server.notify_client( client, ChatType::CommandInfo.server_msg(format!("\"{}\" added to whitelist", username)), ); } else if whitelist_action.eq_ignore_ascii_case("remove") { - server.settings_mut().edit(|s| { - s.whitelist - .retain(|x| !x.eq_ignore_ascii_case(&username.clone())) - }); + server + .whitelist_mut() + .edit(server.data_dir().as_ref(), |w| { + w.retain(|x| !x.eq_ignore_ascii_case(&username.clone())) + }); server.notify_client( client, ChatType::CommandInfo @@ -1926,16 +1930,15 @@ fn handle_ban( .username_to_uuid(&target_alias); if let Ok(uuid) = uuid_result { - if server.settings().banlist.contains_key(&uuid) { + if server.banlist().contains_key(&uuid) { server.notify_client( client, ChatType::CommandError .server_msg(format!("{} is already on the banlist", target_alias)), ) } else { - server.settings_mut().edit(|s| { - s.banlist - .insert(uuid, (target_alias.clone(), reason.clone())); + server.banlist_mut().edit(server.data_dir().as_ref(), |b| { + b.insert(uuid, (target_alias.clone(), reason.clone())); }); server.notify_client( client, @@ -1987,8 +1990,8 @@ fn handle_unban( .username_to_uuid(&username); if let Ok(uuid) = uuid_result { - server.settings_mut().edit(|s| { - s.banlist.remove(&uuid); + server.banlist_mut().edit(server.data_dir().as_ref(), |b| { + b.remove(&uuid); }); server.notify_client( client, diff --git a/server/src/data_dir.rs b/server/src/data_dir.rs new file mode 100644 index 0000000000..c934638d7c --- /dev/null +++ b/server/src/data_dir.rs @@ -0,0 +1,16 @@ +use std::path::{Path, PathBuf}; + +/// Used so that different server frontends can share the same server saves, +/// etc. +pub const DEFAULT_DATA_DIR_NAME: &'static str = "server"; + +/// Indicates where maps, saves, and server_config folders are to be stored +pub struct DataDir { + pub path: PathBuf, +} +impl> From for DataDir { + fn from(t: T) -> Self { Self { path: t.into() } } +} +impl AsRef for DataDir { + fn as_ref(&self) -> &Path { &self.path } +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 3ed6a917dd..21d16408cc 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -6,6 +6,7 @@ pub mod alias_validator; mod character_creator; +mod data_dir; pub mod chunk_generator; pub mod client; pub mod cmd; @@ -21,7 +22,7 @@ pub mod sys; #[cfg(not(feature = "worldgen"))] mod test_world; // Reexports -pub use crate::{error::Error, events::Event, input::Input, settings::ServerSettings}; +pub use crate::{error::Error, events::Event, input::Input, settings::ServerSettings, data_dir::{DataDir, DEFAULT_DATA_DIR_NAME}}; use crate::{ alias_validator::AliasValidator, @@ -29,6 +30,7 @@ use crate::{ client::{Client, RegionSubscription}, cmd::ChatCommandExt, login_provider::LoginProvider, + settings::{Banlist, EditableSetting, ServerDescription, Whitelist}, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedComps}, }; @@ -102,10 +104,15 @@ impl Server { /// Create a new `Server` #[allow(clippy::expect_fun_call)] // TODO: Pending review in #587 #[allow(clippy::needless_update)] // TODO: Pending review in #587 - pub fn new(settings: ServerSettings) -> Result { + pub fn new(settings: ServerSettings, data_dir: DataDir) -> Result { + info!("Server is data dir is: {}", data_dir.path.display()); + + // persistence_db_dir is relative to data_dir + let persistence_db_dir = data_dir.path.join(&settings.persistence_db_dir); + // Run pending DB migrations (if any) debug!("Running DB migrations..."); - if let Some(e) = persistence::run_migrations(&settings.persistence_db_dir).err() { + if let Some(e) = persistence::run_migrations(&persistence_db_dir).err() { panic!("Migration error: {:?}", e); } @@ -116,6 +123,10 @@ impl Server { let mut state = State::default(); state.ecs_mut().insert(settings.clone()); + state.ecs_mut().insert(Whitelist::load(&data_dir.path)); + state.ecs_mut().insert(Banlist::load(&data_dir.path)); + state.ecs_mut().insert(ServerDescription::load(&data_dir.path)); + state.ecs_mut().insert(data_dir); state.ecs_mut().insert(EventBus::::default()); state .ecs_mut() @@ -128,10 +139,10 @@ impl Server { .insert(ChunkGenerator::new(chunk_gen_metrics)); state .ecs_mut() - .insert(CharacterUpdater::new(settings.persistence_db_dir.clone())?); + .insert(CharacterUpdater::new(&persistence_db_dir)?); state .ecs_mut() - .insert(CharacterLoader::new(settings.persistence_db_dir.clone())?); + .insert(CharacterLoader::new(&persistence_db_dir)?); state .ecs_mut() .insert(comp::AdminList(settings.admins.clone())); @@ -340,9 +351,10 @@ impl Server { pub fn get_server_info(&self) -> ServerInfo { let settings = self.state.ecs().fetch::(); + let server_description = self.state.ecs().fetch::(); ServerInfo { name: settings.server_name.clone(), - description: settings.server_description.clone(), + description: (**server_description).clone(), git_hash: common::util::GIT_HASH.to_string(), git_date: common::util::GIT_DATE.to_string(), auth_provider: settings.auth_server_address.clone(), @@ -360,8 +372,38 @@ impl Server { } /// Get a mutable reference to the server's settings - pub fn settings_mut(&mut self) -> impl DerefMut + '_ { - self.state.ecs_mut().fetch_mut::() + pub fn settings_mut(&self) -> impl DerefMut + '_ { + self.state.ecs().fetch_mut::() + } + + /// Get a mutable reference to the server's whitelist + pub fn whitelist_mut(&self) -> impl DerefMut + '_ { + self.state.ecs().fetch_mut::() + } + + /// Get a reference to the server's banlist + pub fn banlist(&self) -> impl Deref + '_ { + self.state.ecs().fetch::() + } + + /// Get a mutable reference to the server's banlist + pub fn banlist_mut(&self) -> impl DerefMut + '_ { + self.state.ecs().fetch_mut::() + } + + /// Get a reference to the server's description + pub fn server_description(&self) -> impl Deref + '_ { + self.state.ecs().fetch::() + } + + /// Get a mutable reference to the server's description + pub fn server_description_mut(&self) -> impl DerefMut + '_ { + self.state.ecs().fetch_mut::() + } + + /// Get path to the directory that the server info into + pub fn data_dir(&self) -> impl Deref + '_ { + self.state.ecs().fetch::() } /// Get a reference to the server's game state. diff --git a/server/src/persistence/character_loader.rs b/server/src/persistence/character_loader.rs index d0b667fb93..049a0c47c1 100644 --- a/server/src/persistence/character_loader.rs +++ b/server/src/persistence/character_loader.rs @@ -5,6 +5,7 @@ use crate::persistence::{ }; use common::character::{CharacterId, CharacterItem}; use crossbeam::{channel, channel::TryIter}; +use std::path::Path; use tracing::error; pub(crate) type CharacterListResult = Result, Error>; @@ -64,11 +65,11 @@ pub struct CharacterLoader { } impl CharacterLoader { - pub fn new(db_dir: String) -> diesel::QueryResult { + pub fn new(db_dir: &Path) -> diesel::QueryResult { let (update_tx, internal_rx) = channel::unbounded::(); let (internal_tx, update_rx) = channel::unbounded::(); - let mut conn = establish_connection(&db_dir)?; + let mut conn = establish_connection(db_dir)?; std::thread::spawn(move || { for request in internal_rx { diff --git a/server/src/persistence/character_updater.rs b/server/src/persistence/character_updater.rs index cb8b8ebe6f..493042d854 100644 --- a/server/src/persistence/character_updater.rs +++ b/server/src/persistence/character_updater.rs @@ -3,7 +3,7 @@ use common::{character::CharacterId, comp::item::ItemId}; use crate::persistence::{establish_connection, VelorenConnection}; use crossbeam::channel; -use std::sync::Arc; +use std::{path::Path, sync::Arc}; use tracing::{error, trace}; pub type CharacterUpdateData = (comp::Stats, comp::Inventory, comp::Loadout); @@ -19,11 +19,11 @@ pub struct CharacterUpdater { } impl CharacterUpdater { - pub fn new(db_dir: String) -> diesel::QueryResult { + pub fn new(db_dir: &Path) -> diesel::QueryResult { let (update_tx, update_rx) = channel::unbounded::>(); - let mut conn = establish_connection(&db_dir)?; + let mut conn = establish_connection(db_dir)?; let handle = std::thread::spawn(move || { while let Ok(updates) = update_rx.recv() { diff --git a/server/src/persistence/mod.rs b/server/src/persistence/mod.rs index b7a87c199a..2c01dc38ce 100644 --- a/server/src/persistence/mod.rs +++ b/server/src/persistence/mod.rs @@ -19,7 +19,10 @@ extern crate diesel; use common::comp; use diesel::{connection::SimpleConnection, prelude::*}; use diesel_migrations::embed_migrations; -use std::{env, fs, path::PathBuf}; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; use tracing::{info, warn}; /// A tuple of the components that are persisted to the DB for each character @@ -45,9 +48,9 @@ impl std::io::Write for TracingOut { } /// Runs any pending database migrations. This is executed during server startup -pub fn run_migrations(db_dir: &str) -> Result<(), diesel_migrations::RunMigrationsError> { +pub fn run_migrations(db_dir: &Path) -> Result<(), diesel_migrations::RunMigrationsError> { let db_dir = &apply_saves_dir_override(db_dir); - let _ = fs::create_dir(format!("{}/", db_dir)); + let _ = fs::create_dir(format!("{}/", db_dir.display())); embedded_migrations::run_with_output( &establish_connection(db_dir) @@ -91,9 +94,9 @@ impl<'a> core::ops::Deref for VelorenTransaction<'a> { fn deref(&self) -> &Self::Target { &self.0 } } -pub fn establish_connection(db_dir: &str) -> QueryResult { +pub fn establish_connection(db_dir: &Path) -> QueryResult { let db_dir = &apply_saves_dir_override(db_dir); - let database_url = format!("{}/db.sqlite", db_dir); + let database_url = format!("{}/db.sqlite", db_dir.display()); let connection = SqliteConnection::establish(&database_url) .unwrap_or_else(|_| panic!("Error connecting to {}", database_url)); @@ -117,16 +120,13 @@ pub fn establish_connection(db_dir: &str) -> QueryResult { Ok(VelorenConnection(connection)) } -fn apply_saves_dir_override(db_dir: &str) -> String { +fn apply_saves_dir_override(db_dir: &Path) -> PathBuf { if let Some(saves_dir) = env::var_os("VELOREN_SAVES_DIR") { - let path = PathBuf::from(saves_dir.clone()); + let path = PathBuf::from(&saves_dir); if path.exists() || path.parent().map(|x| x.exists()).unwrap_or(false) { - // Only allow paths with valid unicode characters - if let Some(path) = path.to_str() { - return path.to_owned(); - } + return path; } warn!(?saves_dir, "VELOREN_SAVES_DIR points to an invalid path."); } - db_dir.to_string() + db_dir.to_owned() } diff --git a/server/src/settings.rs b/server/src/settings.rs index 6f32d83d51..bea63f1f31 100644 --- a/server/src/settings.rs +++ b/server/src/settings.rs @@ -1,12 +1,28 @@ +mod editable; + +pub use editable::EditableSetting; + use authc::Uuid; use hashbrown::HashMap; use portpicker::pick_unused_port; use serde::{Deserialize, Serialize}; -use std::{fs, io::prelude::*, net::SocketAddr, path::PathBuf, time::Duration}; +use std::{ + fs, + net::SocketAddr, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::Duration, +}; use tracing::{error, warn}; use world::sim::FileOpts; const DEFAULT_WORLD_SEED: u32 = 59686; +//const CONFIG_DIR_ENV: &'static str = "VELOREN_SERVER_CONFIG"; +const /*DEFAULT_*/CONFIG_DIR: &'static str = "server_config"; +const SETTINGS_FILENAME: &'static str = "settings.ron"; +const WHITELIST_FILENAME: &'static str = "whitelist.ron"; +const BANLIST_FILENAME: &'static str = "banlist.ron"; +const SERVER_DESCRIPTION_FILENAME: &'static str = "description.ron"; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(default)] @@ -18,14 +34,12 @@ pub struct ServerSettings { pub world_seed: u32, //pub pvp_enabled: bool, pub server_name: String, - pub server_description: String, pub start_time: f64, pub admins: Vec, - pub whitelist: Vec, - pub banlist: HashMap, /// When set to None, loads the default map file (if available); otherwise, /// uses the value of the file options to decide how to proceed. pub map_file: Option, + /// Relative paths are relative to the server data dir pub persistence_db_dir: String, pub max_view_distance: Option, pub banned_words_files: Vec, @@ -42,15 +56,12 @@ impl Default for ServerSettings { metrics_address: SocketAddr::from(([0; 4], 14005)), auth_server_address: Some("https://auth.veloren.net".into()), world_seed: DEFAULT_WORLD_SEED, - server_name: "Veloren Alpha".to_owned(), - server_description: "This is the best Veloren server.".to_owned(), + server_name: "Veloren Alpha".into(), max_players: 100, start_time: 9.0 * 3600.0, map_file: None, admins: Vec::new(), - whitelist: Vec::new(), - banlist: HashMap::new(), - persistence_db_dir: "saves".to_owned(), + persistence_db_dir: "saves".into(), max_view_distance: Some(30), banned_words_files: Vec::new(), max_player_group_size: 6, @@ -62,41 +73,53 @@ impl Default for ServerSettings { } impl ServerSettings { - #[allow(clippy::single_match)] // TODO: Pending review in #587 - pub fn load() -> Self { - let path = ServerSettings::get_settings_path(); + /// path: Directory that contains the server config directory + pub fn load(path: &Path) -> Self { + let path = Self::get_settings_path(path); - if let Ok(file) = fs::File::open(path) { + if let Ok(file) = fs::File::open(&path) { match ron::de::from_reader(file) { Ok(x) => x, Err(e) => { - warn!(?e, "Failed to parse setting file! Fallback to default"); - Self::default() + warn!( + ?e, + "Failed to parse setting file! Falling back to default settings and \ + creating a template file for you to migrate your current settings file" + ); + let default_settings = Self::default(); + let template_path = path.with_extension("template.ron"); + if let Err(e) = default_settings.save_to_file(&template_path) { + error!(?e, "Failed to create template settings file") + } + default_settings }, } } else { let default_settings = Self::default(); - match default_settings.save_to_file() { - Err(e) => error!(?e, "Failed to create default setting file!"), - _ => {}, + if let Err(e) = default_settings.save_to_file(&path) { + error!(?e, "Failed to create default settings file!"); } default_settings } } - pub fn save_to_file(&self) -> std::io::Result<()> { - let path = ServerSettings::get_settings_path(); - let mut config_file = fs::File::create(path)?; - - let s: &str = &ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()) + fn save_to_file(&self, path: &Path) -> std::io::Result<()> { + // Create dir if it doesn't exist + if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()) .expect("Failed serialize settings."); - config_file.write_all(s.as_bytes())?; + + fs::write(path, ron.as_bytes())?; + Ok(()) } - pub fn singleplayer(persistence_db_dir: String) -> Self { - let load = Self::load(); + /// path: Directory that contains the server config directory + pub fn singleplayer(path: &Path) -> Self { + let load = Self::load(&path); Self { //BUG: theoretically another process can grab the port between here and server // creation, however the timewindow is quite small @@ -116,24 +139,90 @@ impl ServerSettings { DEFAULT_WORLD_SEED }, server_name: "Singleplayer".to_owned(), - server_description: "Who needs friends anyway?".to_owned(), + //server_description: "Who needs friends anyway?".to_owned(), max_players: 100, start_time: 9.0 * 3600.0, admins: vec!["singleplayer".to_string()], /* TODO: Let the player choose if they want * to use admin commands or not */ - persistence_db_dir, max_view_distance: None, client_timeout: Duration::from_secs(180), ..load // Fill in remaining fields from server_settings.ron. } } - fn get_settings_path() -> PathBuf { PathBuf::from(r"server_settings.ron") } - - pub fn edit(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { - let r = f(self); - self.save_to_file() - .unwrap_or_else(|err| warn!("Failed to save settings: {:?}", err)); - r + fn get_settings_path(path: &Path) -> PathBuf { + let mut path = with_config_dir(path); + path.push(SETTINGS_FILENAME); + path } } + +fn with_config_dir(path: &Path) -> PathBuf { + let mut path = PathBuf::from(path); + //if let Some(path) = std::env::var_os(CONFIG_DIR_ENV) { + // let config_dir = PathBuf::from(path); + // if config_dir.exists() { + // return config_dir; + // } + // warn!(?path, "VELROREN_SERVER_CONFIG points to invalid path."); + //} + path.push(/* DEFAULT_ */ CONFIG_DIR); + //PathBuf::from(DEFAULT_CONFIG_DIR) + path +} + +#[derive(Deserialize, Serialize, Default)] +#[serde(transparent)] +pub struct Whitelist(Vec); +#[derive(Deserialize, Serialize, Default)] +#[serde(transparent)] +pub struct Banlist(HashMap); +#[derive(Deserialize, Serialize)] +#[serde(transparent)] +pub struct ServerDescription(String); + +impl Default for ServerDescription { + fn default() -> Self { Self("This is the best Veloren server".into()) } +} + +impl EditableSetting for Whitelist { + const FILENAME: &'static str = WHITELIST_FILENAME; +} + +impl EditableSetting for Banlist { + const FILENAME: &'static str = BANLIST_FILENAME; +} + +impl EditableSetting for ServerDescription { + const FILENAME: &'static str = SERVER_DESCRIPTION_FILENAME; +} + +impl Deref for Whitelist { + type Target = Vec; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Whitelist { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Deref for Banlist { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for Banlist { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} + +impl Deref for ServerDescription { + type Target = String; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl DerefMut for ServerDescription { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } +} diff --git a/server/src/settings/editable.rs b/server/src/settings/editable.rs new file mode 100644 index 0000000000..ce8ff36431 --- /dev/null +++ b/server/src/settings/editable.rs @@ -0,0 +1,89 @@ +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fs, + path::{Path, PathBuf}, +}; +use tracing::{error, warn}; + +pub trait EditableSetting: Serialize + DeserializeOwned + Default { + const FILENAME: &'static str; + + fn load(data_dir: &Path) -> Self { + let path = Self::get_path(data_dir); + + if let Ok(file) = fs::File::open(&path) { + match ron::de::from_reader(file) { + Ok(setting) => setting, + Err(e) => { + warn!( + ?e, + "Failed to parse setting file! Falling back to default and moving \ + existing file to a .invalid" + ); + + // Rename existing file to .invalid.ron + let mut new_path = path.with_extension("invalid.ron"); + + // If invalid path already exists append number + for i in 1.. { + if !new_path.exists() { + break; + } + + warn!( + ?new_path, + "Path to move invalid settings exists, appending number" + ); + new_path = path.with_extension(format!("invalid{}.ron", i)); + } + + warn!("Renaming invalid settings file to: {}", path.display()); + if let Err(e) = fs::rename(&path, &new_path) { + warn!(?e, ?path, ?new_path, "Failed to rename settings file."); + } + + create_and_save_default(&path) + }, + } + } else { + create_and_save_default(&path) + } + } + + fn edit(&mut self, data_dir: &Path, f: impl FnOnce(&mut Self) -> R) -> R { + let path = Self::get_path(data_dir); + + let r = f(self); + save_to_file(&*self, &path) + .unwrap_or_else(|err| warn!("Failed to save setting: {:?}", err)); + r + } + + fn get_path(data_dir: &Path) -> PathBuf { + let mut path = super::with_config_dir(data_dir); + path.push(Self::FILENAME); + path + } +} + +fn save_to_file(setting: &S, path: &Path) -> std::io::Result<()> { + // Create dir if it doesn't exist + if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + + let ron = ron::ser::to_string_pretty(setting, ron::ser::PrettyConfig::default()) + .expect("Failed serialize setting."); + + fs::write(path, ron.as_bytes())?; + + Ok(()) +} + +fn create_and_save_default(path: &Path) -> S { + let default = S::default(); + if let Err(e) = save_to_file(&default, path) { + error!(?e, "Failed to create default setting file!"); + } + default +} diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs index 34fcd9fad8..557f8b603b 100644 --- a/server/src/sys/message.rs +++ b/server/src/sys/message.rs @@ -6,6 +6,7 @@ use crate::{ login_provider::LoginProvider, metrics::{NetworkRequestMetrics, PlayerMetrics}, persistence::character_loader::CharacterLoader, + settings::{Banlist, ServerDescription, Whitelist}, ServerSettings, }; use common::{ @@ -66,6 +67,9 @@ impl Sys { controllers: &mut WriteStorage<'_, Controller>, settings: &Read<'_, ServerSettings>, alias_validator: &ReadExpect<'_, AliasValidator>, + whitelist: &Whitelist, + banlist: &Banlist, + server_description: &ServerDescription, ) -> Result<(), crate::error::Error> { loop { let msg = client.recv().await?; @@ -96,17 +100,14 @@ impl Sys { view_distance, token_or_username, } => { - let (username, uuid) = match login_provider.try_login( - &token_or_username, - &settings.whitelist, - &settings.banlist, - ) { - Err(err) => { - client.error_state(RequestStateError::RegisterDenied(err)); - break Ok(()); - }, - Ok((username, uuid)) => (username, uuid), - }; + let (username, uuid) = + match login_provider.try_login(&token_or_username, &whitelist, &banlist) { + Err(err) => { + client.error_state(RequestStateError::RegisterDenied(err)); + break Ok(()); + }, + Ok((username, uuid)) => (username, uuid), + }; let vd = view_distance.map(|vd| vd.min(settings.max_view_distance.unwrap_or(vd))); @@ -206,10 +207,9 @@ impl Sys { }); // Give the player a welcome message - if !settings.server_description.is_empty() { + if !server_description.is_empty() { client.notify( - ChatType::CommandInfo - .server_msg(settings.server_description.clone()), + ChatType::CommandInfo.server_msg(String::from(&**server_description)), ); } @@ -450,6 +450,11 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Controller>, Read<'a, ServerSettings>, ReadExpect<'a, AliasValidator>, + ( + ReadExpect<'a, Whitelist>, + ReadExpect<'a, Banlist>, + ReadExpect<'a, ServerDescription> + ), ); #[allow(clippy::match_ref_pats)] // TODO: Pending review in #587 @@ -483,6 +488,11 @@ impl<'a> System<'a> for Sys { mut controllers, settings, alias_validator, + ( + whitelist, + banlist, + server_description, + ), ): Self::SystemData, ) { span!(_guard, "run", "message::Sys::run"); @@ -543,6 +553,9 @@ impl<'a> System<'a> for Sys { &mut controllers, &settings, &alias_validator, + &whitelist, + &banlist, + &server_description, ); select!( _ = Delay::new(std::time::Duration::from_micros(20)).fuse() => Ok(()), diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index a6a0cbad51..ceb2da01c2 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -53,6 +53,7 @@ chrono = "0.4.9" cpal = "0.11" crossbeam = "=0.7.2" deunicode = "1.0" +# TODO: remove directories-next = "1.0.1" dot_vox = "4.0" enum-iterator = "0.6" diff --git a/voxygen/src/profile.rs b/voxygen/src/profile.rs index e94291404c..34990f1de2 100644 --- a/voxygen/src/profile.rs +++ b/voxygen/src/profile.rs @@ -1,9 +1,8 @@ -use crate::hud; +use crate::{hud, settings}; use common::character::CharacterId; -use directories_next::ProjectDirs; use hashbrown::HashMap; use serde_derive::{Deserialize, Serialize}; -use std::{fs, io::prelude::*, path::PathBuf}; +use std::{fs, io::Write, path::PathBuf}; use tracing::warn; /// Represents a character in the profile. @@ -177,13 +176,9 @@ impl Profile { warn!(?path, "VOXYGEN_CONFIG points to invalid path."); } - let proj_dirs = ProjectDirs::from("net", "veloren", "voxygen") - .expect("System's $HOME directory path not found!"); - - proj_dirs - .config_dir() - .join("profile.ron") - .with_extension("ron") + let mut path = settings::voxygen_data_dir(); + path.push("profile.ron"); + path } } diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 6d49acd4b3..5acdca42df 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -5,7 +5,7 @@ use crate::{ ui::ScaleMode, window::{FullScreenSettings, GameInput, KeyMouse}, }; -use directories_next::{ProjectDirs, UserDirs}; +use directories_next::UserDirs; use hashbrown::{HashMap, HashSet}; use serde_derive::{Deserialize, Serialize}; use std::{fs, io::prelude::*, path::PathBuf}; @@ -584,18 +584,18 @@ pub struct Log { } impl Default for Log { - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 fn default() -> Self { - let proj_dirs = ProjectDirs::from("net", "veloren", "voxygen") - .expect("System's $HOME directory path not found!"); - // Chooses a path to store the logs by the following order: // - The VOXYGEN_LOGS environment variable // - The ProjectsDirs data local directory // This only selects if there isn't already an entry in the settings file let logs_path = std::env::var_os("VOXYGEN_LOGS") .map(PathBuf::from) - .unwrap_or(proj_dirs.data_local_dir().join("logs")); + .unwrap_or_else(|| { + let mut path = voxygen_data_dir(); + path.push("logs"); + path + }); Self { log_to_file: true, @@ -718,8 +718,6 @@ pub struct Settings { } impl Default for Settings { - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 - fn default() -> Self { let user_dirs = UserDirs::new().expect("System's $HOME directory path not found!"); @@ -730,10 +728,12 @@ impl Default for Settings { // This only selects if there isn't already an entry in the settings file let screenshots_path = std::env::var_os("VOXYGEN_SCREENSHOT") .map(PathBuf::from) - .or(user_dirs.picture_dir().map(|dir| dir.join("veloren"))) - .or(std::env::current_exe() - .ok() - .and_then(|dir| dir.parent().map(PathBuf::from))) + .or_else(|| user_dirs.picture_dir().map(|dir| dir.join("veloren"))) + .or_else(|| { + std::env::current_exe() + .ok() + .and_then(|dir| dir.parent().map(PathBuf::from)) + }) .expect("Couldn't choose a place to store the screenshots"); Settings { @@ -766,7 +766,7 @@ impl Settings { let mut new_path = path.to_owned(); new_path.pop(); new_path.push("settings.invalid.ron"); - if let Err(e) = std::fs::rename(path.clone(), new_path.clone()) { + if let Err(e) = std::fs::rename(&path, &new_path) { warn!(?e, ?path, ?new_path, "Failed to rename settings file."); } }, @@ -800,18 +800,23 @@ impl Settings { pub fn get_settings_path() -> PathBuf { if let Some(path) = std::env::var_os("VOXYGEN_CONFIG") { - let settings = PathBuf::from(path.clone()).join("settings.ron"); + let settings = PathBuf::from(&path).join("settings.ron"); if settings.exists() || settings.parent().map(|x| x.exists()).unwrap_or(false) { return settings; } warn!(?path, "VOXYGEN_CONFIG points to invalid path."); } - let proj_dirs = ProjectDirs::from("net", "veloren", "voxygen") - .expect("System's $HOME directory path not found!"); - proj_dirs - .config_dir() - .join("settings") - .with_extension("ron") + let mut path = voxygen_data_dir(); + path.push("settings.ron"); + path } } + +pub fn voxygen_data_dir() -> PathBuf { + // Note: since voxygen is technically a lib we made need to lift this up to + // run.rs + let mut path = common::userdata_dir_workspace!(); + path.push("voxygen"); + path +} diff --git a/voxygen/src/singleplayer.rs b/voxygen/src/singleplayer.rs index a9892cf594..76d4e24549 100644 --- a/voxygen/src/singleplayer.rs +++ b/voxygen/src/singleplayer.rs @@ -1,7 +1,7 @@ use client::Client; use common::clock::Clock; use crossbeam::channel::{bounded, unbounded, Receiver, Sender, TryRecvError}; -use server::{Error as ServerError, Event, Input, Server, ServerSettings}; +use server::{DataDir, Error as ServerError, Event, Input, Server, ServerSettings}; use std::{ sync::{ atomic::{AtomicBool, Ordering}, @@ -32,15 +32,15 @@ impl Singleplayer { pub fn new(client: Option<&Client>) -> (Self, ServerSettings) { let (sender, receiver) = unbounded(); + // Determine folder to save server data in + let server_data_dir = DataDir::from({ + let mut path = common::userdata_dir_workspace!(); + path.push("singleplayer"); + path + }); + // Create server - let settings = ServerSettings::singleplayer( - crate::settings::Settings::get_settings_path() - .parent() - .unwrap() - .join("saves") - .to_string_lossy() - .to_string(), - ); + let settings = ServerSettings::singleplayer(server_data_dir.as_ref()); let thread_pool = client.map(|c| c.thread_pool().clone()); let settings2 = settings.clone(); @@ -52,7 +52,7 @@ impl Singleplayer { let thread = thread::spawn(move || { let mut server = None; - if let Err(e) = result_sender.send(match Server::new(settings2) { + if let Err(e) = result_sender.send(match Server::new(settings2, server_data_dir) { Ok(s) => { server = Some(s); Ok(())