//! Headless dedicated server. //! //! Configured by a TOML file (default `server.toml`, created on first start) and //! command line options, which override the file for one run. Run with `--help` //! for the options. Commands (`help`, `save`, `config set`, ...) can be typed into //! the terminal. use std::{path::PathBuf, time::Duration}; use bevy::{app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*, state::app::StatesPlugin}; use bevy_replicon::prelude::*; use bevy_replicon_renet::RepliconRenetPlugins; use clap::Parser; use game_core::CorePlugin; use game_server::{ GameServerPlugin, NetworkServerPlugin, config::{ConfigFile, DEFAULT_CONFIG_PATH, ServerConfig}, console::ConsolePlugin, }; /// Station dedicated server. #[derive(Parser, Debug)] #[command( version, about, after_help = "Options override the config file for this run only." )] struct Cli { /// Configuration file, created with defaults if missing. #[arg(short, long, value_name = "FILE", default_value = DEFAULT_CONFIG_PATH)] config: PathBuf, /// UDP port to listen on. #[arg(short, long)] port: Option, /// Password players need to join (empty for none). #[arg(long)] password: Option, #[arg(long, value_name = "N")] max_players: Option, /// Database URL: sqlite://FILE or postgres://USER:PASSWORD@HOST/DB. #[arg(long, value_name = "URL")] database: Option, /// World name (created if missing) or UUID. #[arg(long, value_name = "NAME|UUID")] world: Option, /// Don't persist the world. #[arg(long)] no_save: bool, /// Tiled map for new worlds, relative to the assets directory. #[arg(long)] map: Option, /// Seconds between autosaves. #[arg(long, value_name = "SECS")] autosave: Option, #[arg(long)] motd: Option, /// Grant admin rights to a player id (repeatable). #[arg(long, value_name = "ID")] admin: Vec, /// Only allow whitelisted players. #[arg(long)] whitelist: bool, /// Print the effective configuration and exit. #[arg(long)] print_config: bool, } impl Cli { fn apply(self, config: &mut ServerConfig) { let Cli { port, password, max_players, database, world, no_save, map, autosave, motd, admin, whitelist, .. } = self; macro_rules! set { ($($field:ident).+ = $value:expr) => { if let Some(value) = $value { config.$($field).+ = value; } }; } set!(port = port); set!(password = password); set!(max_players = max_players); set!(database.url = database); set!(database.world = world); set!(map = map); set!(autosave_secs = autosave); set!(motd = motd); if no_save { config.database.save = false; } if whitelist { config.access.whitelist_enabled = true; } config.access.admins.extend(admin); } } fn main() -> AppExit { let cli = Cli::parse(); let (file, mut config, created) = match ConfigFile::load_or_create(&cli.config) { Ok(loaded) => loaded, Err(e) => { eprintln!("error: {e}"); return AppExit::error(); } }; let print = cli.print_config; cli.apply(&mut config); if print { print!("{}", toml::to_string(&config).expect("config serializes")); return AppExit::Success; } let mut app = App::new(); app.add_plugins(( MinimalPlugins.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( 1.0 / 60.0, ))), StatesPlugin, LogPlugin::default(), bevy::app::TerminalCtrlCHandlerPlugin, RepliconPlugins, RepliconRenetPlugins, CorePlugin, GameServerPlugin { settings: config.settings(), }, NetworkServerPlugin { config }, ConsolePlugin, )); if created { info!("created config file `{}`", file.path.display()); } else { info!("loaded config file `{}`", file.path.display()); } app.insert_resource(file); app.run() }