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