veloren_voxygen/singleplayer/
mod.rs

1use common::{clock::Clock, match_some};
2use crossbeam_channel::{Receiver, Sender, TryRecvError, bounded, unbounded};
3use i18n::LocalizationHandle;
4use rand::seq::IteratorRandom;
5use server::{
6    Error as ServerError, Event, Input, Server, ServerInitStage,
7    persistence::{DatabaseSettings, SqlLogMode},
8    settings::server_description::ServerDescription,
9};
10
11use std::{
12    sync::{
13        Arc,
14        atomic::{AtomicBool, Ordering},
15    },
16    thread::{self, JoinHandle},
17    time::Duration,
18};
19use tokio::runtime::Runtime;
20use tracing::{error, info, trace, warn};
21
22mod singleplayer_world;
23pub use singleplayer_world::{SingleplayerWorld, SingleplayerWorlds};
24
25const TPS: u64 = 30;
26
27/// Used to start and stop the background thread running the server
28/// when in singleplayer mode.
29pub struct Singleplayer {
30    _server_thread: JoinHandle<()>,
31    stop_server_s: Sender<()>,
32    pub receiver: Receiver<Result<(), ServerError>>,
33    pub init_stage_receiver: Receiver<ServerInitStage>,
34    // Wether the server is stopped or not
35    paused: Arc<AtomicBool>,
36}
37
38impl Singleplayer {
39    /// Returns wether or not the server is paused
40    pub fn is_paused(&self) -> bool { self.paused.load(Ordering::SeqCst) }
41
42    /// Pauses if true is passed and unpauses if false (Does nothing if in that
43    /// state already)
44    pub fn pause(&self, state: bool) { self.paused.store(state, Ordering::SeqCst); }
45}
46
47impl Drop for Singleplayer {
48    fn drop(&mut self) {
49        // Ignore the result
50        let _ = self.stop_server_s.send(());
51    }
52}
53
54#[derive(Default)]
55pub enum SingleplayerState {
56    #[default]
57    None,
58    Init(SingleplayerWorlds),
59    Running(Singleplayer),
60}
61
62impl SingleplayerState {
63    pub fn init() -> Self {
64        let dir = common_base::userdata_dir();
65
66        Self::Init(SingleplayerWorlds::load(&dir))
67    }
68
69    pub fn run(
70        &mut self,
71        runtime: &Arc<Runtime>,
72        selected_language: &String,
73        i18n: &LocalizationHandle,
74    ) {
75        if let Self::Init(worlds) = self {
76            let Some(world) = worlds.current() else {
77                error!("Failed to get the current world.");
78                return;
79            };
80            let server_data_dir = world.path.clone();
81
82            let mut settings = server::Settings::singleplayer(&server_data_dir);
83            let mut editable_settings = server::EditableSettings::singleplayer(&server_data_dir);
84
85            let i18n = i18n.read();
86            let motd = ["hud-chat-singleplayer-motd1", "hud-chat-singleplayer-motd2"]
87                .iter()
88                .choose(&mut rand::rng())
89                .expect("Message of the day don't wanna play.");
90
91            editable_settings.server_description.descriptions.insert(
92                selected_language.to_string(),
93                ServerDescription {
94                    motd: i18n.get_msg(motd).to_string(),
95                    rules: None,
96                },
97            );
98
99            let file_opts = if let Some(gen_opts) = &world.gen_opts
100                && !world.is_generated
101            {
102                server::FileOpts::Save(world.map_path.clone(), gen_opts.clone())
103            } else {
104                if !world.is_generated && world.gen_opts.is_none() {
105                    world.copy_default_world();
106                }
107                server::FileOpts::Load(world.map_path.clone())
108            };
109
110            settings.map_file = Some(file_opts);
111            settings.world_seed = world.seed;
112            settings.day_length = world.day_length;
113
114            let (stop_server_s, stop_server_r) = unbounded();
115
116            let (server_stage_tx, server_stage_rx) = unbounded();
117
118            // Create server
119
120            // Relative to data_dir
121            const PERSISTENCE_DB_DIR: &str = "saves";
122
123            let database_settings = DatabaseSettings {
124                db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
125                sql_log_mode: SqlLogMode::Disabled, /* Voxygen doesn't take in command-line
126                                                     * arguments
127                                                     * so SQL logging can't be enabled for
128                                                     * singleplayer without changing this line
129                                                     * manually */
130            };
131
132            let paused = Arc::new(AtomicBool::new(false));
133            let paused1 = Arc::clone(&paused);
134
135            let (result_sender, result_receiver) = bounded(1);
136
137            let builder = thread::Builder::new().name("singleplayer-server-thread".into());
138            let runtime = Arc::clone(runtime);
139            let thread = builder
140                .spawn(move || {
141                    trace!("starting singleplayer server thread");
142
143                    let (server, init_result) = match Server::new(
144                        settings,
145                        editable_settings,
146                        database_settings,
147                        &server_data_dir,
148                        &|init_stage| {
149                            let _ = server_stage_tx.send(init_stage);
150                        },
151                        runtime,
152                    ) {
153                        Ok(server) => (Some(server), Ok(())),
154                        Err(err) => (None, Err(err)),
155                    };
156
157                    match (result_sender.send(init_result), server) {
158                        (Err(e), _) => warn!(
159                            ?e,
160                            "Failed to send singleplayer server initialization result. Most \
161                             likely the channel was closed by cancelling server creation. \
162                             Stopping Server"
163                        ),
164                        (Ok(()), None) => (),
165                        (Ok(()), Some(server)) => run_server(server, stop_server_r, paused1),
166                    }
167
168                    trace!("ending singleplayer server thread");
169                })
170                .unwrap();
171
172            *self = SingleplayerState::Running(Singleplayer {
173                _server_thread: thread,
174                stop_server_s,
175                init_stage_receiver: server_stage_rx,
176                receiver: result_receiver,
177                paused,
178            });
179        } else {
180            error!("SingleplayerState::run was called, but singleplayer is already running!");
181        }
182    }
183
184    pub fn as_running(&self) -> Option<&Singleplayer> {
185        match_some!(self, SingleplayerState::Running(s) => s)
186    }
187
188    pub fn as_init(&self) -> Option<&SingleplayerWorlds> {
189        match_some!(self, SingleplayerState::Init(s) => s)
190    }
191
192    pub fn is_running(&self) -> bool { matches!(self, SingleplayerState::Running(_)) }
193}
194
195fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc<AtomicBool>) {
196    info!("Starting server-cli...");
197
198    // Set up an fps clock
199    let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
200
201    loop {
202        // Check any event such as stopping and pausing
203        match stop_server_r.try_recv() {
204            Ok(()) => break,
205            Err(TryRecvError::Disconnected) => break,
206            Err(TryRecvError::Empty) => (),
207        }
208
209        // Wait for the next tick.
210        clock.tick();
211
212        // Skip updating the server if it's paused
213        if paused.load(Ordering::SeqCst) && server.number_of_players() < 2 {
214            continue;
215        } else if server.number_of_players() > 1 {
216            paused.store(false, Ordering::SeqCst);
217        }
218
219        let events = server
220            .tick(Input::default(), clock.dt())
221            .expect("Failed to tick server!");
222
223        for event in events {
224            match event {
225                Event::ClientConnected { .. } => info!("Client connected!"),
226                Event::ClientDisconnected { .. } => info!("Client disconnected!"),
227                Event::Chat { entity: _, msg } => info!("[Client] {}", msg),
228            }
229        }
230
231        // Clean up the server after a tick.
232        server.cleanup();
233    }
234}