station/crates/game_server/src/main.rs

150 lines
4.3 KiB
Rust
Raw Normal View History

//! 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<u16>,
/// Password players need to join (empty for none).
#[arg(long)]
password: Option<String>,
#[arg(long, value_name = "N")]
max_players: Option<usize>,
/// Database URL: sqlite://FILE or postgres://USER:PASSWORD@HOST/DB.
#[arg(long, value_name = "URL")]
database: Option<String>,
/// World name (created if missing) or UUID.
#[arg(long, value_name = "NAME|UUID")]
world: Option<String>,
/// Don't persist the world.
#[arg(long)]
no_save: bool,
/// Tiled map for new worlds, relative to the assets directory.
#[arg(long)]
map: Option<String>,
/// Seconds between autosaves.
#[arg(long, value_name = "SECS")]
autosave: Option<u64>,
#[arg(long)]
motd: Option<String>,
/// Grant admin rights to a player id (repeatable).
#[arg(long, value_name = "ID")]
admin: Vec<String>,
/// 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()
}