use common::clock::Clock; use crossbeam::channel::{bounded, unbounded, Receiver, Sender, TryRecvError}; use server::{ persistence::{DatabaseSettings, SqlLogMode}, Error as ServerError, Event, Input, Server, }; use std::{ sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, thread::{self, JoinHandle}, time::Duration, }; use tokio::runtime::Runtime; use tracing::{debug, error, info, trace, warn}; const TPS: u64 = 30; /// Used to start and stop the background thread running the server /// when in singleplayer mode. pub struct Singleplayer { _server_thread: JoinHandle<()>, stop_server_s: Sender<()>, pub receiver: Receiver, ServerError>>, // Wether the server is stopped or not paused: Arc, // Settings that the server was started with settings: server::Settings, } impl Singleplayer { #[allow(clippy::new_without_default)] pub fn new() -> Self { let (stop_server_s, stop_server_r) = unbounded(); // Determine folder to save server data in let server_data_dir = { let mut path = common_base::userdata_dir_workspace!(); path.push("singleplayer"); path }; // Copy saves from old folder if they don't exist in the new location (|| { let new_path = server_data_dir.join("saves"); if new_path.exists() { return; } let working_dir = std::path::PathBuf::from("saves"); let config_dir = directories_next::ProjectDirs::from("net", "veloren", "voxygen") .expect("System's $HOME directory path not found!") .config_dir() .join("saves"); let old_path = if working_dir.exists() { working_dir } else if config_dir.exists() { config_dir } else { return; }; info!( "Saves folder doesn't exist, but there is one in the old saves location ({}), \ copying it to the new location", old_path.display() ); if let Some(parent) = new_path.parent() { if let Err(e) = std::fs::create_dir_all(parent) { error!(?e, "Could not create folder to hold saves folder."); return; } } if let Err(e) = copy_dir::copy_dir(old_path, new_path) { error!(?e, "Failed to copy saves from the old location"); } })(); // Create server let settings = server::Settings::singleplayer(&server_data_dir); let editable_settings = server::EditableSettings::singleplayer(&server_data_dir); let cores = num_cpus::get(); debug!("Creating a new runtime for server"); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(if cores > 4 { cores - 1 } else { cores }) .thread_name_fn(|| { static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0); let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst); format!("tokio-sp-{}", id) }) .build() .unwrap(), ); let settings2 = settings.clone(); // Relative to data_dir const PERSISTENCE_DB_DIR: &str = "saves"; let database_settings = DatabaseSettings { db_dir: server_data_dir.join(PERSISTENCE_DB_DIR), sql_log_mode: SqlLogMode::Disabled, /* Voxygen doesn't take in command-line arguments * so SQL logging can't be enabled for * singleplayer without changing this line * manually */ }; let paused = Arc::new(AtomicBool::new(false)); let paused1 = Arc::clone(&paused); let (result_sender, result_receiver) = bounded(1); let builder = thread::Builder::new().name("singleplayer-server-thread".into()); let thread = builder .spawn(move || { trace!("starting singleplayer server thread"); let mut server = None; if let Err(e) = result_sender.send( match Server::new( settings2, editable_settings, database_settings, &server_data_dir, Arc::clone(&runtime), ) { Ok(s) => { server = Some(s); Ok(runtime) }, Err(e) => Err(e), }, ) { warn!( ?e, "Failed to send singleplayer server initialization result. Most likely \ the channel was closed by cancelling server creation. Stopping Server" ); return; }; let server = match server { Some(s) => s, None => return, }; run_server(server, stop_server_r, paused1); trace!("ending singleplayer server thread"); }) .unwrap(); Singleplayer { _server_thread: thread, stop_server_s, receiver: result_receiver, paused, settings, } } /// Returns reference to the settings the server was started with pub fn settings(&self) -> &server::Settings { &self.settings } /// Returns wether or not the server is paused pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) } /// Pauses if true is passed and unpauses if false (Does nothing if in that /// state already) pub fn pause(&self, state: bool) { self.paused.store(state, Ordering::SeqCst); } } impl Drop for Singleplayer { fn drop(&mut self) { // Ignore the result let _ = self.stop_server_s.send(()); } } fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc) { info!("Starting server-cli..."); // Set up an fps clock let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64)); loop { // Check any event such as stopping and pausing match stop_server_r.try_recv() { Ok(()) => break, Err(TryRecvError::Disconnected) => break, Err(TryRecvError::Empty) => (), } // Wait for the next tick. clock.tick(); // Skip updating the server if it's paused if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 { continue; } else if server.number_of_players() > 1 { paused.store(false, Ordering::SeqCst); } let events = server .tick(Input::default(), clock.dt()) .expect("Failed to tick server!"); for event in events { match event { Event::ClientConnected { .. } => info!("Client connected!"), Event::ClientDisconnected { .. } => info!("Client disconnected!"), Event::Chat { entity: _, msg } => info!("[Client] {}", msg), } } // Clean up the server after a tick. server.cleanup(); } }