1#![deny(unsafe_code)]
2#![deny(clippy::clone_on_ref_ptr)]
3
4#[cfg(all(
5 target_os = "windows",
6 not(feature = "hot-agent"),
7 not(feature = "hot-site"),
8))]
9#[global_allocator]
10static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
11
12mod cli;
15mod settings;
16mod shutdown_coordinator;
17mod tui_runner;
18mod tuilog;
19mod web;
20use crate::{
21 cli::{
22 Admin, ArgvApp, ArgvCommand, BenchParams, Message, MessageReturn, SharedCommand, Shutdown,
23 },
24 settings::Settings,
25 shutdown_coordinator::ShutdownCoordinator,
26 tui_runner::Tui,
27 tuilog::TuiLog,
28};
29use common::{
30 clock::Clock,
31 comp::{ChatType, Player},
32 consts::MIN_RECOMMENDED_TOKIO_THREADS,
33};
34use common_base::span;
35use core::sync::atomic::{AtomicUsize, Ordering};
36use server::{Event, Input, Server, persistence::DatabaseSettings, settings::Protocol};
37use std::{
38 io,
39 sync::{Arc, atomic::AtomicBool},
40 time::{Duration, Instant},
41};
42use tokio::sync::Notify;
43use tracing::{info, trace};
44
45lazy_static::lazy_static! {
46 pub static ref LOG: TuiLog<'static> = TuiLog::default();
47}
48const TPS: u64 = 30;
49
50fn main() -> io::Result<()> {
51 #[cfg(feature = "tracy")]
52 common_base::tracy_client::Client::start();
53
54 use clap::Parser;
55 let app = ArgvApp::parse();
56
57 let basic = !app.tui || app.command.is_some();
58 let noninteractive = app.non_interactive;
59 let no_auth = app.no_auth;
60 let sql_log_mode = app.sql_log_mode;
61
62 let basic = basic || noninteractive;
64
65 let shutdown_signal = Arc::new(AtomicBool::new(false));
66
67 let (_guards, _guards2) = if basic {
68 (Vec::new(), common_frontend::init_stdout(None))
69 } else {
70 (common_frontend::init(None, &|| LOG.clone()), Vec::new())
71 };
72
73 let settings = settings::Settings::load();
75
76 #[cfg(any(target_os = "linux", target_os = "macos"))]
77 {
78 for signal in &settings.shutdown_signals {
79 let _ = signal_hook::flag::register(signal.to_signal(), Arc::clone(&shutdown_signal));
80 }
81 }
82
83 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
84 if !settings.shutdown_signals.is_empty() {
85 tracing::warn!(
86 "Server configuration contains shutdown signals, but your platform does not support \
87 them"
88 );
89 }
90
91 let server_data_dir = {
93 let mut path = common_base::userdata_dir_workspace!();
94 info!("Using userdata folder at {}", path.display());
95 path.push(server::DEFAULT_DATA_DIR_NAME);
96 path
97 };
98
99 let runtime = Arc::new(
103 tokio::runtime::Builder::new_multi_thread()
104 .enable_all()
105 .worker_threads((num_cpus::get() / 4).max(MIN_RECOMMENDED_TOKIO_THREADS))
106 .thread_name_fn(|| {
107 static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
108 let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
109 format!("tokio-server-{}", id)
110 })
111 .build()
112 .unwrap(),
113 );
114
115 #[cfg(feature = "hot-agent")]
116 {
117 agent::init();
118 }
119 #[cfg(feature = "hot-site")]
120 {
121 world::init();
122 }
123
124 let mut server_settings = server::Settings::load(&server_data_dir);
126 let mut editable_settings = server::EditableSettings::load(&server_data_dir);
127
128 if no_auth {
130 server_settings.auth_server_address = None;
131 }
132
133 const PERSISTENCE_DB_DIR: &str = "saves";
135
136 let database_settings = DatabaseSettings {
137 db_dir: server_data_dir.join(PERSISTENCE_DB_DIR),
138 sql_log_mode,
139 };
140
141 let mut bench = None;
142 if let Some(command) = app.command {
143 match command {
144 ArgvCommand::Shared(SharedCommand::Admin { command }) => {
145 let login_provider = server::login_provider::LoginProvider::new(
146 server_settings.auth_server_address,
147 runtime,
148 );
149
150 return match command {
151 Admin::Add { username, role } => {
152 let _ = server::add_admin(
158 &username,
159 role,
160 &login_provider,
161 &mut editable_settings,
162 &server_data_dir,
163 );
164 Ok(())
165 },
166 Admin::Remove { username } => {
167 let _ = server::remove_admin(
173 &username,
174 &login_provider,
175 &mut editable_settings,
176 &server_data_dir,
177 );
178 Ok(())
179 },
180 };
181 },
182 ArgvCommand::Bench(params) => {
183 bench = Some(params);
184 server_settings.max_view_distance = None;
186 },
191 };
192 }
193
194 if !basic {
197 let hook = std::panic::take_hook();
198 std::panic::set_hook(Box::new(move |info| {
199 Tui::shutdown(basic);
200 hook(info);
201 }));
202 }
203
204 let tui = (!noninteractive).then(|| Tui::run(basic));
205
206 info!("Starting server...");
207
208 let protocols_and_addresses = server_settings.gameserver_protocols.clone();
209 let web_port = &settings.web_address.port();
210 #[cfg_attr(not(feature = "worldgen"), expect(unused_mut))]
212 let mut server = Server::new(
213 server_settings,
214 editable_settings,
215 database_settings,
216 &server_data_dir,
217 &|_| {},
218 Arc::clone(&runtime),
219 )
220 .expect("Failed to create server instance!");
221
222 let registry = Arc::clone(server.metrics_registry());
223 let chat = server.chat_cache().clone();
224 let metrics_shutdown = Arc::new(Notify::new());
225 let metrics_shutdown_clone = Arc::clone(&metrics_shutdown);
226 let web_chat_secret = settings.web_chat_secret.clone();
227 let ui_api_secret = settings.ui_api_secret.clone().unwrap_or_else(|| {
228 use rand::distributions::{Alphanumeric, DistString};
231 Alphanumeric.sample_string(&mut rand::thread_rng(), 32)
232 });
233
234 let (web_ui_request_s, web_ui_request_r) = tokio::sync::mpsc::channel(1000);
235
236 runtime.spawn(async move {
237 web::run(
238 registry,
239 chat,
240 web_chat_secret,
241 ui_api_secret,
242 web_ui_request_s,
243 settings.web_address,
244 metrics_shutdown_clone.notified(),
245 )
246 .await
247 });
248
249 let gameserver_addresses = protocols_and_addresses
251 .into_iter()
252 .map(|protocol| match protocol {
253 Protocol::Tcp { address } => ("TCP", address),
254 Protocol::Quic {
255 address,
256 cert_file_path: _,
257 key_file_path: _,
258 } => ("QUIC", address),
259 });
260
261 info!(
262 ?web_port,
263 ?gameserver_addresses,
264 "Server is ready to accept connections."
265 );
266
267 #[cfg(feature = "worldgen")]
268 if let Some(bench) = bench {
269 server.create_centered_persister(bench.view_distance);
270 }
271
272 server_loop(
273 server,
274 bench,
275 settings,
276 tui,
277 web_ui_request_r,
278 shutdown_signal,
279 )?;
280
281 metrics_shutdown.notify_one();
282
283 Ok(())
284}
285
286fn server_loop(
287 mut server: Server,
288 bench: Option<BenchParams>,
289 settings: Settings,
290 tui: Option<Tui>,
291 mut web_ui_request_r: tokio::sync::mpsc::Receiver<(
292 Message,
293 tokio::sync::oneshot::Sender<MessageReturn>,
294 )>,
295 shutdown_signal: Arc<AtomicBool>,
296) -> io::Result<()> {
297 let mut clock = Clock::new(Duration::from_secs_f64(1.0 / TPS as f64));
299 let mut shutdown_coordinator = ShutdownCoordinator::new(Arc::clone(&shutdown_signal));
300 let mut bench_exit_time = None;
301
302 let mut tick_no = 0u64;
303 'outer: loop {
304 span!(guard, "work");
305 if let Some(bench) = bench {
306 if let Some(t) = bench_exit_time {
307 if Instant::now() > t {
308 break;
309 }
310 } else if tick_no != 0 && !server.chunks_pending() {
311 println!("Chunk loading complete");
312 bench_exit_time = Some(Instant::now() + Duration::from_secs(bench.duration.into()));
313 }
314 };
315
316 tick_no += 1;
317 if shutdown_coordinator.check(&mut server, &settings) {
319 break;
320 }
321
322 let events = server
323 .tick(Input::default(), clock.dt())
324 .expect("Failed to tick server");
325
326 for event in events {
327 match event {
328 Event::ClientConnected { entity: _ } => info!("Client connected!"),
329 Event::ClientDisconnected { entity: _ } => info!("Client disconnected!"),
330 Event::Chat { entity: _, msg } => info!("[Client] {}", msg),
331 }
332 }
333
334 server.cleanup();
336
337 if tick_no.rem_euclid(1000) == 0 {
338 trace!(?tick_no, "keepalive")
339 }
340
341 let mut handle_msg = |msg, response: tokio::sync::oneshot::Sender<MessageReturn>| {
342 use specs::{Join, WorldExt};
343 match msg {
344 Message::Shutdown {
345 command: Shutdown::Cancel,
346 } => shutdown_coordinator.abort_shutdown(&mut server),
347 Message::Shutdown {
348 command: Shutdown::Graceful { seconds, reason },
349 } => {
350 shutdown_coordinator.initiate_shutdown(
351 &mut server,
352 Duration::from_secs(seconds),
353 reason,
354 );
355 },
356 Message::Shutdown {
357 command: Shutdown::Immediate,
358 } => {
359 return true;
360 },
361 Message::Shared(SharedCommand::Admin {
362 command: Admin::Add { username, role },
363 }) => {
364 server.add_admin(&username, role);
365 },
366 Message::Shared(SharedCommand::Admin {
367 command: Admin::Remove { username },
368 }) => {
369 server.remove_admin(&username);
370 },
371 #[cfg(feature = "worldgen")]
372 Message::LoadArea { view_distance } => {
373 server.create_centered_persister(view_distance);
374 },
375 Message::SqlLogMode { mode } => {
376 server.set_sql_log_mode(mode);
377 },
378 Message::DisconnectAllClients => {
379 server.disconnect_all_clients();
380 },
381 Message::ListPlayers => {
382 let players: Vec<String> = server
383 .state()
384 .ecs()
385 .read_storage::<Player>()
386 .join()
387 .map(|p| p.alias.clone())
388 .collect();
389 let _ = response.send(MessageReturn::Players(players));
390 },
391 Message::ListLogs => {
392 let log = LOG.inner.lock().unwrap();
393 let lines: Vec<_> = log
394 .lines
395 .iter()
396 .rev()
397 .take(30)
398 .map(|l| l.to_string())
399 .collect();
400 let _ = response.send(MessageReturn::Logs(lines));
401 },
402 Message::SendGlobalMsg { msg } => {
403 use server::state_ext::StateExt;
404 let msg = ChatType::Meta.into_plain_msg(msg);
405 server.state().send_chat(msg);
406 },
407 }
408 false
409 };
410
411 if let Some(tui) = tui.as_ref() {
412 while let Ok(msg) = tui.msg_r.try_recv() {
413 let (sender, mut recv) = tokio::sync::oneshot::channel();
414 if handle_msg(msg, sender) {
415 info!("Closing the server");
416 break 'outer;
417 }
418 if let Ok(msg_answ) = recv.try_recv() {
419 match msg_answ {
420 MessageReturn::Players(players) => info!("Players: {:?}", players),
421 MessageReturn::Logs(_) => info!("skipp sending logs to tui"),
422 };
423 }
424 }
425 }
426
427 while let Ok((msg, sender)) = web_ui_request_r.try_recv() {
428 if handle_msg(msg, sender) {
429 info!("Closing the server");
430 break 'outer;
431 }
432 }
433
434 drop(guard);
435 clock.tick();
437 #[cfg(feature = "tracy")]
438 common_base::tracy_client::frame_mark();
439 }
440 Ok(())
441}