use common::clock::Clock;
use crossbeam_channel::{bounded, unbounded, Receiver, Sender, TryRecvError};
use server::{
persistence::{DatabaseSettings, SqlLogMode},
Error as ServerError, Event, Input, Server, ServerInitStage,
};
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread::{self, JoinHandle},
time::Duration,
};
use tokio::runtime::Runtime;
use tracing::{error, info, trace, warn};
mod singleplayer_world;
pub use singleplayer_world::{SingleplayerWorld, SingleplayerWorlds};
const TPS: u64 = 30;
pub struct Singleplayer {
_server_thread: JoinHandle<()>,
stop_server_s: Sender<()>,
pub receiver: Receiver<Result<(), ServerError>>,
pub init_stage_receiver: Receiver<ServerInitStage>,
paused: Arc<AtomicBool>,
}
impl Singleplayer {
pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) }
pub fn pause(&self, state: bool) { self.paused.store(state, Ordering::SeqCst); }
}
impl Drop for Singleplayer {
fn drop(&mut self) {
let _ = self.stop_server_s.send(());
}
}
#[derive(Default)]
pub enum SingleplayerState {
#[default]
None,
Init(SingleplayerWorlds),
Running(Singleplayer),
}
impl SingleplayerState {
pub fn init() -> Self {
let dir = common_base::userdata_dir_workspace!();
Self::Init(SingleplayerWorlds::load(&dir))
}
pub fn run(&mut self, runtime: &Arc<Runtime>) {
if let Self::Init(worlds) = self {
let Some(world) = worlds.current() else {
error!("Failed to get the current world.");
return;
};
let server_data_dir = world.path.clone();
let mut settings = server::Settings::singleplayer(&server_data_dir);
let editable_settings = server::EditableSettings::singleplayer(&server_data_dir);
let file_opts = if let Some(gen_opts) = &world.gen_opts
&& !world.is_generated
{
server::FileOpts::Save(world.map_path.clone(), gen_opts.clone())
} else {
if !world.is_generated && world.gen_opts.is_none() {
world.copy_default_world();
}
server::FileOpts::Load(world.map_path.clone())
};
settings.map_file = Some(file_opts);
settings.world_seed = world.seed;
settings.day_length = world.day_length;
let (stop_server_s, stop_server_r) = unbounded();
let (server_stage_tx, server_stage_rx) = unbounded();
const PERSISTENCE_DB_DIR: &str = "saves";
let database_settings = DatabaseSettings {
db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
sql_log_mode: SqlLogMode::Disabled, };
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 runtime = Arc::clone(runtime);
#[allow(clippy::blocks_in_conditions)]
let thread = builder
.spawn(move || {
trace!("starting singleplayer server thread");
let (server, init_result) = match Server::new(
settings,
editable_settings,
database_settings,
&server_data_dir,
&|init_stage| {
let _ = server_stage_tx.send(init_stage);
},
runtime,
) {
Ok(server) => (Some(server), Ok(())),
Err(err) => (None, Err(err)),
};
match (result_sender.send(init_result), server) {
(Err(e), _) => warn!(
?e,
"Failed to send singleplayer server initialization result. Most \
likely the channel was closed by cancelling server creation. \
Stopping Server"
),
(Ok(()), None) => (),
(Ok(()), Some(server)) => run_server(server, stop_server_r, paused1),
}
trace!("ending singleplayer server thread");
})
.unwrap();
*self = SingleplayerState::Running(Singleplayer {
_server_thread: thread,
stop_server_s,
init_stage_receiver: server_stage_rx,
receiver: result_receiver,
paused,
});
} else {
error!("SingleplayerState::run was called, but singleplayer is already running!");
}
}
pub fn as_running(&self) -> Option<&Singleplayer> {
match self {
SingleplayerState::Running(s) => Some(s),
_ => None,
}
}
pub fn as_init(&self) -> Option<&SingleplayerWorlds> {
match self {
SingleplayerState::Init(s) => Some(s),
_ => None,
}
}
pub fn is_running(&self) -> bool { matches!(self, SingleplayerState::Running(_)) }
}
fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc<AtomicBool>) {
info!("Starting server-cli...");
let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
loop {
match stop_server_r.try_recv() {
Ok(()) => break,
Err(TryRecvError::Disconnected) => break,
Err(TryRecvError::Empty) => (),
}
clock.tick();
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),
}
}
server.cleanup();
}
}