From 40c2d2188e6fd2d06bfe69a7027902fabb725e63 Mon Sep 17 00:00:00 2001 From: Jupeyy Date: Mon, 6 Jan 2025 14:57:23 +0100 Subject: [PATCH] Add `exec` & `load` for command line parsing in server. Rename server-bin to server. --- Cargo.lock | 111 +-- Cargo.toml | 8 +- game/game-config-fs/Cargo.toml | 3 + game/game-config-fs/src/fs.rs | 14 +- game/game-config/src/config.rs | 10 +- game/{server => game-server}/Cargo.toml | 2 +- .../src/auto_map_votes.rs | 0 game/{server => game-server}/src/client.rs | 0 game/{server => game-server}/src/lib.rs | 0 .../src/local_server.rs | 4 +- game/{server => game-server}/src/map_votes.rs | 0 .../src/network_plugins/accounts_only.rs | 0 .../src/network_plugins/cert_ban.rs | 0 .../src/network_plugins/mod.rs | 0 game/{server => game-server}/src/rcon.rs | 5 + game/{server => game-server}/src/server.rs | 760 +++++++++++------- .../src/server_game.rs | 0 .../src/spatial_chat.rs | 0 lib/config-fs/Cargo.toml | 2 + lib/config-fs/src/lib.rs | 10 +- lib/config/src/config.rs | 10 +- misc | 2 +- src/client/client.rs | 6 +- src/client/game/active.rs | 26 +- src/{server-bin => server}/Cargo.toml | 7 +- src/{server-bin => server}/src/main.rs | 30 +- 26 files changed, 592 insertions(+), 418 deletions(-) rename game/{server => game-server}/Cargo.toml (99%) rename game/{server => game-server}/src/auto_map_votes.rs (100%) rename game/{server => game-server}/src/client.rs (100%) rename game/{server => game-server}/src/lib.rs (100%) rename game/{server => game-server}/src/local_server.rs (97%) rename game/{server => game-server}/src/map_votes.rs (100%) rename game/{server => game-server}/src/network_plugins/accounts_only.rs (100%) rename game/{server => game-server}/src/network_plugins/cert_ban.rs (100%) rename game/{server => game-server}/src/network_plugins/mod.rs (100%) rename game/{server => game-server}/src/rcon.rs (91%) rename game/{server => game-server}/src/server.rs (90%) rename game/{server => game-server}/src/server_game.rs (100%) rename game/{server => game-server}/src/spatial_chat.rs (100%) rename src/{server-bin => server}/Cargo.toml (64%) rename src/{server-bin => server}/src/main.rs (53%) diff --git a/Cargo.lock b/Cargo.lock index 56304cb..6e78782 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1975,6 +1975,7 @@ dependencies = [ name = "config-fs" version = "0.1.0" dependencies = [ + "anyhow", "base-io", "config", ] @@ -2722,6 +2723,7 @@ dependencies = [ "game-config-fs", "game-interface", "game-network", + "game-server", "game-state-wasm", "ghost", "graphics", @@ -2749,7 +2751,6 @@ dependencies = [ "serde", "serde-value", "serde_json", - "server", "sound", "sound-backend", "steam", @@ -4041,6 +4042,7 @@ dependencies = [ name = "game-config-fs" version = "0.1.0" dependencies = [ + "anyhow", "base-io", "game-config", ] @@ -4133,6 +4135,58 @@ dependencies = [ "tokio", ] +[[package]] +name = "game-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "base", + "base-fs", + "base-http", + "base-io", + "base-io-traits", + "bin-patch", + "bincode", + "cache", + "chrono", + "command-parser", + "config", + "config-fs", + "ddnet-account-client-http-fs", + "ddnet-account-game-server", + "ddnet-accounts-shared", + "demo", + "difference", + "ed25519-dalek", + "either", + "futures", + "game-base", + "game-config", + "game-config-fs", + "game-database", + "game-database-backend", + "game-interface", + "game-network", + "game-state-wasm", + "http-accounts", + "log", + "map", + "map-convert-lib", + "master-server-types", + "network", + "pool", + "rand", + "rayon", + "rustc-hash 2.1.0", + "serde", + "serde_json", + "sql", + "thread-priority", + "vanilla", + "x509-cert", +] + [[package]] name = "game-state-wasm" version = "0.1.0" @@ -8503,67 +8557,14 @@ dependencies = [ [[package]] name = "server" version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "base", - "base-fs", - "base-http", - "base-io", - "base-io-traits", - "bin-patch", - "bincode", - "cache", - "chrono", - "command-parser", - "config", - "config-fs", - "ddnet-account-client-http-fs", - "ddnet-account-game-server", - "ddnet-accounts-shared", - "demo", - "difference", - "ed25519-dalek", - "either", - "futures", - "game-base", - "game-config", - "game-config-fs", - "game-database", - "game-database-backend", - "game-interface", - "game-network", - "game-state-wasm", - "http-accounts", - "log", - "map", - "map-convert-lib", - "master-server-types", - "network", - "pool", - "rand", - "rayon", - "rustc-hash 2.1.0", - "serde", - "serde_json", - "sql", - "thread-priority", - "vanilla", - "x509-cert", -] - -[[package]] -name = "server-bin" -version = "0.1.0" dependencies = [ "base", - "clap 4.5.23", "env_logger", "game-base", "game-config", + "game-server", "network", "serde_json", - "server", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 105ddef..b280f55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ members = [ "examples/wasm-modules/graphics", "examples/wasm-modules/mainmenu", "examples/wasm-modules/ingame_menu", - "src/server-bin", + "src/server", "src/emoticon-convert", "src/game-convert", "src/hud-convert", @@ -67,7 +67,7 @@ members = [ "game/client-types", "game/client-containers", "game/client-ui", - "game/server", + "game/game-server", "lib/game-database", "game/game-base", "game/vanilla", @@ -166,7 +166,7 @@ client-demo = { path = "game/client-demo" } client-replay = { path = "game/client-replay" } ghost = { path = "game/ghost" } client-ghost = { path = "game/client-ghost" } -server = { path = "game/server", default-features = false } +game-server = { path = "game/game-server", default-features = false } game-base = { path = "game/game-base" } game-interface = { path = "game/game-interface" } game-network = { path = "game/game-network" } @@ -231,7 +231,7 @@ opt-level = 3 [features] bundled_data_dir = ["base-fs/bundled_data_dir"] -legacy = ["server/legacy", "editor-wasm/legacy", "editor/legacy"] +legacy = ["game-server/legacy", "editor-wasm/legacy", "editor/legacy"] enable_steam = ["steam/runtime"] microphone = ["microphone/cpal_opus"] ffmpeg = ["client-demo/ffmpeg"] diff --git a/game/game-config-fs/Cargo.toml b/game/game-config-fs/Cargo.toml index 6cdd894..66c8101 100644 --- a/game/game-config-fs/Cargo.toml +++ b/game/game-config-fs/Cargo.toml @@ -5,4 +5,7 @@ edition = "2021" [dependencies] base-io = { path = "../../lib/base-io" } + game-config = { path = "../game-config" } + +anyhow = { version = "1.0.95", features = ["backtrace"] } diff --git a/game/game-config-fs/src/fs.rs b/game/game-config-fs/src/fs.rs index 49ebebd..c6162f8 100644 --- a/game/game-config-fs/src/fs.rs +++ b/game/game-config-fs/src/fs.rs @@ -18,20 +18,16 @@ pub fn save(config: &ConfigGame, io: &Io) { } } -pub fn load_in(io: &IoFileSys, path: &Path) -> ConfigGame { +pub fn load_in(io: &IoFileSys, path: &Path) -> anyhow::Result { let fs = io.fs.clone(); let path = path.to_path_buf(); let config_file = io .rt - .spawn(async move { Ok(fs.read_file(path.as_ref()).await) }); - let res = config_file.get_storage().unwrap(); - match res { - Ok(file) => ConfigGame::from_json_string(String::from_utf8(file).unwrap().as_str()) - .unwrap_or_default(), - Err(_) => ConfigGame::new(), - } + .spawn(async move { Ok(fs.read_file(path.as_ref()).await?) }); + let res = config_file.get_storage()?; + ConfigGame::from_json_slice(&res) } -pub fn load(io: &IoFileSys) -> ConfigGame { +pub fn load(io: &IoFileSys) -> anyhow::Result { load_in(io, "cfg_game.json".as_ref()) } diff --git a/game/game-config/src/config.rs b/game/game-config/src/config.rs index 9440eee..f119fab 100644 --- a/game/game-config/src/config.rs +++ b/game/game-config/src/config.rs @@ -755,13 +755,15 @@ impl ConfigGame { } pub fn to_json_string(&self) -> anyhow::Result { - let res = serde_json::to_string_pretty(self)?; - Ok(res) + Ok(serde_json::to_string_pretty(self)?) } pub fn from_json_string(json_str: &str) -> anyhow::Result { - let res = serde_json::from_str(json_str)?; - Ok(res) + Ok(serde_json::from_str(json_str)?) + } + + pub fn from_json_slice(json: &[u8]) -> anyhow::Result { + Ok(serde_json::from_slice(json)?) } } diff --git a/game/server/Cargo.toml b/game/game-server/Cargo.toml similarity index 99% rename from game/server/Cargo.toml rename to game/game-server/Cargo.toml index 1cf39ed..7a02d44 100644 --- a/game/server/Cargo.toml +++ b/game/game-server/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "server" +name = "game-server" version = "0.1.0" edition = "2021" diff --git a/game/server/src/auto_map_votes.rs b/game/game-server/src/auto_map_votes.rs similarity index 100% rename from game/server/src/auto_map_votes.rs rename to game/game-server/src/auto_map_votes.rs diff --git a/game/server/src/client.rs b/game/game-server/src/client.rs similarity index 100% rename from game/server/src/client.rs rename to game/game-server/src/client.rs diff --git a/game/server/src/lib.rs b/game/game-server/src/lib.rs similarity index 100% rename from game/server/src/lib.rs rename to game/game-server/src/lib.rs diff --git a/game/server/src/local_server.rs b/game/game-server/src/local_server.rs similarity index 97% rename from game/server/src/local_server.rs rename to game/game-server/src/local_server.rs index 8d339bd..66d5633 100644 --- a/game/server/src/local_server.rs +++ b/game/game-server/src/local_server.rs @@ -2,9 +2,9 @@ use std::sync::{atomic::AtomicBool, Arc}; use base::{join_thread::JoinThread, system::System}; use config::config::ConfigEngine; +use game_base::local_server_info::{LocalServerInfo, LocalServerState, LocalServerThread}; use game_config::config::ConfigGame; use network::network::utils::create_certifified_keys; -use game_base::local_server_info::{LocalServerInfo, LocalServerState, LocalServerThread}; use crate::server::ddnet_server_main; @@ -37,7 +37,7 @@ pub fn start_local_server( (cert, private_key), server_is_open_clone, shared_info_thread, - None, + Default::default(), Some((config_engine, config_game)), ) }) diff --git a/game/server/src/map_votes.rs b/game/game-server/src/map_votes.rs similarity index 100% rename from game/server/src/map_votes.rs rename to game/game-server/src/map_votes.rs diff --git a/game/server/src/network_plugins/accounts_only.rs b/game/game-server/src/network_plugins/accounts_only.rs similarity index 100% rename from game/server/src/network_plugins/accounts_only.rs rename to game/game-server/src/network_plugins/accounts_only.rs diff --git a/game/server/src/network_plugins/cert_ban.rs b/game/game-server/src/network_plugins/cert_ban.rs similarity index 100% rename from game/server/src/network_plugins/cert_ban.rs rename to game/game-server/src/network_plugins/cert_ban.rs diff --git a/game/server/src/network_plugins/mod.rs b/game/game-server/src/network_plugins/mod.rs similarity index 100% rename from game/server/src/network_plugins/mod.rs rename to game/game-server/src/network_plugins/mod.rs diff --git a/game/server/src/rcon.rs b/game/game-server/src/rcon.rs similarity index 91% rename from game/server/src/rcon.rs rename to game/game-server/src/rcon.rs index 9534032..cf8a9b6 100644 --- a/game/server/src/rcon.rs +++ b/game/game-server/src/rcon.rs @@ -63,5 +63,10 @@ pub enum ServerRconCommand { KickId, Status, ConfVariable, + /// Executes a command line like file + /// and parses it in the command chain. + Exec, + /// Loads server config from a specific path + Load, RecordDemo, } diff --git a/game/server/src/server.rs b/game/game-server/src/server.rs similarity index 90% rename from game/server/src/server.rs rename to game/game-server/src/server.rs index 8acdaf8..faa1d04 100644 --- a/game/server/src/server.rs +++ b/game/game-server/src/server.rs @@ -4,7 +4,7 @@ use std::{ net::IpAddr, num::NonZeroUsize, ops::Range, - path::Path, + path::PathBuf, sync::{atomic::AtomicBool, Arc, Weak}, time::Duration, }; @@ -165,6 +165,8 @@ enum GameServerDb { Account(GameServerDbAccount), } +type ReponsesAndCmds = (Vec>, Vec); + pub struct Server { pub clients: Clients, pub player_count_of_all_clients: usize, @@ -352,6 +354,224 @@ impl Server { ) } + fn new_rcon_cmd_chain() -> CommandChain { + let mut rcon_cmds = vec![ + ( + "ban_id".try_into().unwrap(), + Command { + rcon: RconCommand { + args: vec![CommandArg { + ty: CommandArgType::Number, + user_ty: Some("PLAYER_ID".try_into().unwrap()), + }], + description: "Ban a user with the given player id".try_into().unwrap(), + usage: "ban_id ".try_into().unwrap(), + }, + cmd: ServerRconCommand::BanId, + }, + ), + ( + "kick_id".try_into().unwrap(), + Command { + rcon: RconCommand { + args: vec![CommandArg { + ty: CommandArgType::Number, + user_ty: Some("PLAYER_ID".try_into().unwrap()), + }], + description: "Kick a user with the given player id".try_into().unwrap(), + usage: "kick_id ".try_into().unwrap(), + }, + cmd: ServerRconCommand::KickId, + }, + ), + ( + "status".try_into().unwrap(), + Command { + rcon: RconCommand { + args: Default::default(), + description: + "List information about this player such as the connected clients" + .try_into() + .unwrap(), + usage: "status".try_into().unwrap(), + }, + cmd: ServerRconCommand::Status, + }, + ), + ( + "record_demo".try_into().unwrap(), + Command { + rcon: RconCommand { + args: Default::default(), + description: "Start to record a server side demo.".try_into().unwrap(), + usage: "record_demo".try_into().unwrap(), + }, + cmd: ServerRconCommand::RecordDemo, + }, + ), + ( + "exec".try_into().unwrap(), + Command { + rcon: RconCommand { + args: vec![CommandArg { + ty: CommandArgType::Text, + user_ty: None, + }], + description: "Executes a file of command lines.".try_into().unwrap(), + usage: "exec ".try_into().unwrap(), + }, + cmd: ServerRconCommand::Exec, + }, + ), + ( + "load".try_into().unwrap(), + Command { + rcon: RconCommand { + args: vec![CommandArg { + ty: CommandArgType::Text, + user_ty: None, + }], + description: "Loads a json config file.".try_into().unwrap(), + usage: "load ".try_into().unwrap(), + }, + cmd: ServerRconCommand::Load, + }, + ), + ]; + + config::parsing::parse_conf_values_as_str_list( + "sv".into(), + &mut |add, _| { + rcon_cmds.push(( + add.name.try_into().unwrap(), + Command { + rcon: RconCommand { + args: add.args, + usage: add.usage.as_str().try_into().unwrap(), + description: add.description.as_str().try_into().unwrap(), + }, + cmd: ServerRconCommand::ConfVariable, + }, + )); + }, + ConfigServer::conf_value(), + "".into(), + Default::default(), + ); + + CommandChain::new(rcon_cmds.into_iter().collect()) + } + + fn handle_load_config( + config: &mut ConfigGame, + io: &Io, + cmd: parser::Command, + ) -> anyhow::Result { + let Syn::Text(file_path) = &cmd.args[0].0 else { + panic!("Command parser returned a non requested command arg"); + }; + *config = game_config_fs::fs::load_in(&io.clone().into(), file_path.as_ref())?; + Ok(format!("New config file was loaded from {file_path}")) + } + + fn handle_exec( + config: &mut ConfigGame, + io: &Io, + rcon_chain: &CommandChain, + cmd: parser::Command, + ) -> anyhow::Result { + let Syn::Text(file_path) = &cmd.args[0].0 else { + panic!("Command parser returned a non requested command arg"); + }; + let file_path: PathBuf = file_path.into(); + let fs = io.fs.clone(); + let cmds_file = io + .rt + .spawn(async move { + fs.read_file(&file_path) + .await + .map_err(|err| anyhow!(err)) + .and_then(|file| String::from_utf8(file).map_err(|err| anyhow!(err))) + }) + .get_storage()?; + + Ok(Self::handle_config_cmd_lines( + config, + io, + rcon_chain, + cmds_file.lines().map(|s| s.to_string()).collect(), + )) + } + + /// Handles config variable from the cmd lines. + /// Returns the reponses of the cmds and + /// returns all cmds that are not related to config variables. + fn handle_config_cmd_lines( + config: &mut ConfigGame, + io: &Io, + rcon_chain: &CommandChain, + lines: Vec, + ) -> ReponsesAndCmds { + let mut remaining_cmds = Vec::default(); + let mut responses = Vec::default(); + if !lines.is_empty() { + let cmds = command_parser::parser::parse( + &lines.join(" "), + &rcon_chain.parser, + &mut Default::default(), + ); + + for cmd in cmds { + let handle_cmd = || match cmd { + CommandType::Full(cmd) => { + let Some(chain_cmd) = rcon_chain.cmds.get(&cmd.ident) else { + return Err(anyhow!("Command {} not found", cmd.ident)); + }; + + match chain_cmd.cmd { + ServerRconCommand::ConfVariable => { + Self::handle_config_variable_cmd(&cmd, config) + } + ServerRconCommand::Exec => { + match Self::handle_exec(config, io, rcon_chain, cmd) { + Ok((mut res, mut res_remaining_cmds)) => { + responses.append(&mut res); + remaining_cmds.append(&mut res_remaining_cmds); + Ok("".to_string()) + } + Err(err) => Err(err), + } + } + ServerRconCommand::Load => Self::handle_load_config(config, io, cmd), + _ => { + remaining_cmds.push(cmd); + Ok("".to_string()) + } + } + } + CommandType::Partial(cmd) => { + let Some(cmd) = cmd.ref_cmd_partial() else { + return Err(anyhow!("This command was invalid: {cmd}")); + }; + let Some(chain_cmd) = rcon_chain.cmds.get(&cmd.ident) else { + return Err(anyhow!("Command {} not found", cmd.ident)); + }; + + if let ServerRconCommand::ConfVariable = chain_cmd.cmd { + Self::handle_config_variable_cmd(cmd, config) + } else { + Err(anyhow!("This command was invalid: {cmd}")) + } + } + }; + + let cmd_res = handle_cmd(); + responses.push(cmd_res.map_err(|err| err.to_string())); + } + } + (responses, remaining_cmds) + } + pub fn new( sys: System, is_open: Arc, @@ -363,6 +583,7 @@ impl Server { config_game: ConfigGame, thread_pool: Arc, io: Io, + rcon_chain: CommandChain, ) -> anyhow::Result { let config_db = config_game.sv.db.clone(); let accounts_enabled = !config_db.enable_accounts.is_empty(); @@ -593,84 +814,6 @@ impl Server { } } - let mut rcon_cmds = vec![ - ( - "ban_id".try_into().unwrap(), - Command { - rcon: RconCommand { - args: vec![CommandArg { - ty: CommandArgType::Number, - user_ty: Some("PLAYER_ID".try_into().unwrap()), - }], - description: "Ban a user with the given player id".try_into().unwrap(), - usage: "ban_id ".try_into().unwrap(), - }, - cmd: ServerRconCommand::BanId, - }, - ), - ( - "kick_id".try_into().unwrap(), - Command { - rcon: RconCommand { - args: vec![CommandArg { - ty: CommandArgType::Number, - user_ty: Some("PLAYER_ID".try_into().unwrap()), - }], - description: "Kick a user with the given player id".try_into().unwrap(), - usage: "kick_id ".try_into().unwrap(), - }, - cmd: ServerRconCommand::KickId, - }, - ), - ( - "status".try_into().unwrap(), - Command { - rcon: RconCommand { - args: Default::default(), - description: - "List information about this player such as the connected clients" - .try_into() - .unwrap(), - usage: "status".try_into().unwrap(), - }, - cmd: ServerRconCommand::Status, - }, - ), - ( - "record_demo".try_into().unwrap(), - Command { - rcon: RconCommand { - args: Default::default(), - description: "Start to record a server side demo.".try_into().unwrap(), - usage: "record_demo".try_into().unwrap(), - }, - cmd: ServerRconCommand::RecordDemo, - }, - ), - ]; - - config::parsing::parse_conf_values_as_str_list( - "sv".into(), - &mut |add, _| { - rcon_cmds.push(( - add.name.try_into().unwrap(), - Command { - rcon: RconCommand { - args: add.args, - usage: add.usage.as_str().try_into().unwrap(), - description: add.description.as_str().try_into().unwrap(), - }, - cmd: ServerRconCommand::ConfVariable, - }, - )); - }, - ConfigServer::conf_value(), - "".into(), - Default::default(), - ); - - let rcon_chain = CommandChain::new(rcon_cmds.into_iter().collect()); - Ok(Self { clients: Clients::new( config_game.sv.max_players as usize, @@ -1558,12 +1701,10 @@ impl Server { ); } - fn handle_rcon_commands( - &mut self, - _player_id: Option<&PlayerId>, - _auth: AuthLevel, - cmds: Vec, - ) -> Vec> { + fn handle_config_variable_cmd( + cmd: &parser::Command, + config: &mut ConfigGame, + ) -> anyhow::Result { fn syn_vec_to_config_val(args: &[(Syn, Range)]) -> Option { args.first().map(|(arg, _)| match arg { parser::Syn::Command(cmd) => cmd.cmd_text.clone(), @@ -1578,202 +1719,211 @@ impl Server { parser::Syn::JsonArrayLike(obj) => obj.clone(), }) } - let mut res: Vec> = Default::default(); - for cmd in cmds { - let handle_cmd = || { - match cmd { - CommandType::Full(cmd) => { - let Some(chain_cmd) = self.rcon_chain.cmds.get(&cmd.ident) else { - return Err(anyhow!("Command {} not found", cmd.ident)); - }; + Ok(config.try_set_from_str( + cmd.ident.clone(), + None, + syn_vec_to_config_val(&cmd.args), + None, + config::traits::ConfigFromStrOperation::Set, + )?) + } - fn ban_or_kick( - cmd: &parser::Command, - game_server: &ServerGame, - clients: &mut Clients, - process: impl FnOnce(&mut ServerClient, NetworkConnectionId), - ) -> anyhow::Result<()> { - let Syn::Number(num) = &cmd.args[0].0 else { - panic!("Command parser returned a non requested command arg"); - }; - let ban_id: GameEntityId = num.parse()?; - let ban_id: PlayerId = ban_id.into(); - if let Some((client, network_id)) = - game_server.players.get(&ban_id).and_then(|player| { - clients - .clients - .get_mut(&player.network_id) - .map(|c| (c, player.network_id)) - }) - { - process(client, network_id); - } + fn handle_cmd_full( + &mut self, + cmd: parser::Command, + responses: &mut Vec>, + remaining_cmds: &mut Vec, + ) -> anyhow::Result { + let Some(chain_cmd) = self.rcon_chain.cmds.get(&cmd.ident) else { + return Err(anyhow!("Command {} not found", cmd.ident)); + }; - Ok(()) - } + fn ban_or_kick( + cmd: &parser::Command, + game_server: &ServerGame, + clients: &mut Clients, + process: impl FnOnce(&mut ServerClient, NetworkConnectionId), + ) -> anyhow::Result<()> { + let Syn::Number(num) = &cmd.args[0].0 else { + panic!("Command parser returned a non requested command arg"); + }; + let ban_id: GameEntityId = num.parse()?; + let ban_id: PlayerId = ban_id.into(); + if let Some((client, network_id)) = + game_server.players.get(&ban_id).and_then(|player| { + clients + .clients + .get_mut(&player.network_id) + .map(|c| (c, player.network_id)) + }) + { + process(client, network_id); + } - match chain_cmd.cmd { - ServerRconCommand::BanId => { - let mut res = String::new(); - ban_or_kick( - &cmd, - &self.game_server, - &mut self.clients, - |client, _| { - let ty = BanType::Admin; - let until = None; - - client.drop_reason = Some(PlayerDropReason::Banned { - reason: PlayerBanReason::Rcon, - until, - }); + Ok(()) + } - // ban the player - let ids = self.connection_bans.ban_ip( - client.ip, - ty.clone(), - until, - ); - for id in &ids { - self.network.kick( - id, - KickType::Ban(Banned { - msg: ty.clone(), - until, - }), - ); - } - let text: String = ids - .into_iter() - .map(|id| id.to_string()) - .collect::>() - .join(", "); - res = format!("Banned the following id(s): {}", text); - }, - )?; - anyhow::Ok(res) - } - ServerRconCommand::KickId => { - let mut res = String::new(); - ban_or_kick( - &cmd, - &self.game_server, - &mut self.clients, - |c, network_id| { - c.drop_reason = - Some(PlayerDropReason::Kicked(PlayerKickReason::Rcon)); + match chain_cmd.cmd { + ServerRconCommand::BanId => { + let mut res = String::new(); + ban_or_kick(&cmd, &self.game_server, &mut self.clients, |client, _| { + let ty = BanType::Admin; + let until = None; - self.network.kick( - &network_id, - KickType::Kick("by admin".to_string()), - ); - let text: String = c - .players - .keys() - .map(|id| id.to_string()) - .collect::>() - .join(", "); - res = format!("Kicked the following id(s): {}", text); - }, - )?; - anyhow::Ok(res) - } - ServerRconCommand::Status => { - let mut res: Vec = Default::default(); - for client in self.clients.clients.values() { - res.push(format!("client ip: {}", client.ip)); - for (player_id, player) in client.players.iter() { - res.push(format!( - " player_id: {}, client_id: {}", - player_id, player.id - )); - } - } - Ok(res.join("\n")) - } - ServerRconCommand::ConfVariable => { - Ok(self.config_game.try_set_from_str( - cmd.ident, - None, - syn_vec_to_config_val(&cmd.args), - None, - config::traits::ConfigFromStrOperation::Set, - )?) - } - ServerRconCommand::RecordDemo => { - let had_demo_recorder = self.demo_recorder.is_some(); - self.demo_recorder = Some(DemoRecorder::new( - DemoRecorderCreateProps { - base: DemoRecorderCreatePropsBase { - map: self - .game_server - .map - .name - .as_str() - .try_into() - .unwrap(), - map_hash: generate_hash_for( - &self.game_server.map.map_file, - ), - game_options: GameStateCreateOptions { - hint_max_characters: Some( - self.config_game.sv.max_players as usize, - ), - account_db: None, - config: self.game_server.game.info.config.clone(), - }, - required_resources: Default::default(), /* TODO: */ - physics_module: self.game_server.game_mod.clone(), - render_module: self.game_server.render_mod.clone(), - physics_group_name: self - .game_server - .game - .info - .options - .physics_group_name - .clone(), - }, - io: self.io.clone(), - in_memory: None, - }, - self.game_server.game.info.ticks_in_a_second, - Some("server_demos".as_ref()), - None, - )); - Ok(format!( - "Started demo recording.{}", - if had_demo_recorder { - "\nA previous recording was stopped in that process." - } else { - "" - } - )) - } - } - } - CommandType::Partial(cmd) => { - let Some(cmd) = cmd.ref_cmd_partial() else { - return Err(anyhow!("This command was invalid: {cmd}")); - }; - let Some(chain_cmd) = self.rcon_chain.cmds.get(&cmd.ident) else { - return Err(anyhow!("Command {} not found", cmd.ident)); - }; + client.drop_reason = Some(PlayerDropReason::Banned { + reason: PlayerBanReason::Rcon, + until, + }); - if let ServerRconCommand::ConfVariable = chain_cmd.cmd { - Ok(self.config_game.try_set_from_str( - cmd.ident.clone(), - None, - syn_vec_to_config_val(&cmd.args), - None, - config::traits::ConfigFromStrOperation::Set, - )?) - } else { - Err(anyhow!("This command was invalid: {cmd}")) - } + // ban the player + let ids = self.connection_bans.ban_ip(client.ip, ty.clone(), until); + for id in &ids { + self.network.kick( + id, + KickType::Ban(Banned { + msg: ty.clone(), + until, + }), + ); + } + let text: String = ids + .into_iter() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + res = format!("Banned the following id(s): {}", text); + })?; + anyhow::Ok(res) + } + ServerRconCommand::KickId => { + let mut res = String::new(); + ban_or_kick( + &cmd, + &self.game_server, + &mut self.clients, + |c, network_id| { + c.drop_reason = Some(PlayerDropReason::Kicked(PlayerKickReason::Rcon)); + + self.network + .kick(&network_id, KickType::Kick("by admin".to_string())); + let text: String = c + .players + .keys() + .map(|id| id.to_string()) + .collect::>() + .join(", "); + res = format!("Kicked the following id(s): {}", text); + }, + )?; + anyhow::Ok(res) + } + ServerRconCommand::Status => { + let mut res: Vec = Default::default(); + for client in self.clients.clients.values() { + res.push(format!("client ip: {}", client.ip)); + for (player_id, player) in client.players.iter() { + res.push(format!( + " player_id: {}, client_id: {}", + player_id, player.id + )); } } - }; - match handle_cmd() { + Ok(res.join("\n")) + } + ServerRconCommand::ConfVariable => { + Self::handle_config_variable_cmd(&cmd, &mut self.config_game) + } + ServerRconCommand::Exec => { + match Self::handle_exec(&mut self.config_game, &self.io, &self.rcon_chain, cmd) { + Ok((mut res, mut res_remaining_cmds)) => { + responses.append(&mut res); + remaining_cmds.append(&mut res_remaining_cmds); + Ok("".to_string()) + } + Err(err) => Err(err), + } + } + ServerRconCommand::Load => { + Self::handle_load_config(&mut self.config_game, &self.io, cmd) + } + ServerRconCommand::RecordDemo => { + let had_demo_recorder = self.demo_recorder.is_some(); + self.demo_recorder = Some(DemoRecorder::new( + DemoRecorderCreateProps { + base: DemoRecorderCreatePropsBase { + map: self.game_server.map.name.as_str().try_into().unwrap(), + map_hash: generate_hash_for(&self.game_server.map.map_file), + game_options: GameStateCreateOptions { + hint_max_characters: Some(self.config_game.sv.max_players as usize), + account_db: None, + config: self.game_server.game.info.config.clone(), + }, + required_resources: Default::default(), /* TODO: */ + physics_module: self.game_server.game_mod.clone(), + render_module: self.game_server.render_mod.clone(), + physics_group_name: self + .game_server + .game + .info + .options + .physics_group_name + .clone(), + }, + io: self.io.clone(), + in_memory: None, + }, + self.game_server.game.info.ticks_in_a_second, + Some("server_demos".as_ref()), + None, + )); + Ok(format!( + "Started demo recording.{}", + if had_demo_recorder { + "\nA previous recording was stopped in that process." + } else { + "" + } + )) + } + } + } + + fn handle_cmd( + &mut self, + cmd: parser::CommandType, + responses: &mut Vec>, + remaining_cmds: &mut Vec, + ) -> anyhow::Result { + match cmd { + CommandType::Full(cmd) => self.handle_cmd_full(cmd, responses, remaining_cmds), + CommandType::Partial(cmd) => { + let Some(cmd) = cmd.ref_cmd_partial() else { + return Err(anyhow!("This command was invalid: {cmd}")); + }; + let Some(chain_cmd) = self.rcon_chain.cmds.get(&cmd.ident) else { + return Err(anyhow!("Command {} not found", cmd.ident)); + }; + + if let ServerRconCommand::ConfVariable = chain_cmd.cmd { + Self::handle_config_variable_cmd(cmd, &mut self.config_game) + } else { + Err(anyhow!("This command was invalid: {cmd}")) + } + } + } + } + + fn handle_rcon_commands( + &mut self, + _player_id: Option<&PlayerId>, + _auth: AuthLevel, + cmds: Vec, + ) -> Vec> { + let mut remaining_cmds = Vec::default(); + let mut responses = Vec::default(); + let mut res: Vec> = Default::default(); + for cmd in cmds { + match self.handle_cmd(cmd, &mut responses, &mut remaining_cmds) { Ok(msg) => { res.push(NetworkString::new_lossy(msg)); } @@ -1781,6 +1931,20 @@ impl Server { res.push(NetworkString::new_lossy(err.to_string())); } } + // directly add the current reponses after command handling + res.extend(responses.drain(..).map(|s| { + NetworkString::new_lossy(match s { + Ok(s) => s, + Err(s) => s, + }) + })); + } + if !remaining_cmds.is_empty() { + res.append(&mut self.handle_rcon_commands( + _player_id, + _auth, + remaining_cmds.into_iter().map(CommandType::Full).collect(), + )); } res } @@ -3153,7 +3317,7 @@ impl Server { } } -pub fn load_config(game_cfg_path: Option<&Path>) -> (Io, ConfigEngine, ConfigGame) { +pub fn load_config() -> (Io, ConfigEngine, ConfigGame) { let io = Io::new( |rt| { Arc::new( @@ -3165,13 +3329,13 @@ pub fn load_config(game_cfg_path: Option<&Path>) -> (Io, ConfigEngine, ConfigGam ); let config_engine = config_fs::load(&io.clone().into()); - let config_game = if let Some(game_cfg_path) = game_cfg_path { - game_config_fs::fs::load_in(&io.clone().into(), game_cfg_path) - } else { - game_config_fs::fs::load(&io.clone().into()) - }; + let config_game = game_config_fs::fs::load(&io.clone().into()); - (io, config_engine, config_game) + ( + io, + config_engine.unwrap_or_default(), + config_game.unwrap_or_default(), + ) } pub fn ddnet_server_main( @@ -3179,7 +3343,7 @@ pub fn ddnet_server_main( cert_and_private_key: (x509_cert::Certificate, SigningKey), is_open: Arc, shared_info: Arc, - game_cfg_path: Option<&Path>, + args: Vec, config_overwrite: Option<(ConfigEngine, ConfigGame)>, ) -> anyhow::Result<()> { let thread_pool = Arc::new( @@ -3202,14 +3366,25 @@ pub fn ddnet_server_main( .build()?, ); - let (io, config_engine, config_game) = load_config(game_cfg_path); + let (io, config_engine, config_game) = load_config(); + + let (config_engine, mut config_game) = + if let Some((config_engine, config_game)) = config_overwrite { + (config_engine, config_game) + } else { + (config_engine, config_game) + }; + + let rcon_chain = Server::new_rcon_cmd_chain(); - let (config_engine, config_game) = if let Some((config_engine, config_game)) = config_overwrite - { - (config_engine, config_game) - } else { - (config_engine, config_game) - }; + let (msgs, remaining_cmds) = + Server::handle_config_cmd_lines(&mut config_game, &io, &rcon_chain, args); + for msg in msgs { + match msg { + Ok(msg) => log::info!("{msg}"), + Err(err) => log::error!("{err}"), + } + } let mut server = Server::new( sys, @@ -3231,8 +3406,17 @@ pub fn ddnet_server_main( config_game, thread_pool, io, + rcon_chain, )?; + // Handle remaining args after the server started. + for cmd in remaining_cmds { + match server.handle_cmd_full(cmd, &mut Default::default(), &mut Default::default()) { + Ok(res) => log::info!("{res}"), + Err(err) => log::error!("{err}"), + } + } + server.run(); Ok(()) diff --git a/game/server/src/server_game.rs b/game/game-server/src/server_game.rs similarity index 100% rename from game/server/src/server_game.rs rename to game/game-server/src/server_game.rs diff --git a/game/server/src/spatial_chat.rs b/game/game-server/src/spatial_chat.rs similarity index 100% rename from game/server/src/spatial_chat.rs rename to game/game-server/src/spatial_chat.rs diff --git a/lib/config-fs/Cargo.toml b/lib/config-fs/Cargo.toml index a7bcb08..cb7eb4b 100644 --- a/lib/config-fs/Cargo.toml +++ b/lib/config-fs/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" [dependencies] base-io = { path = "../base-io" } config = { path = "../config" } + +anyhow = { version = "1.0.95", features = ["backtrace"] } diff --git a/lib/config-fs/src/lib.rs b/lib/config-fs/src/lib.rs index cd3d058..9140e82 100644 --- a/lib/config-fs/src/lib.rs +++ b/lib/config-fs/src/lib.rs @@ -16,15 +16,11 @@ pub fn save(config: &ConfigEngine, io: &Io) { } } -pub fn load(io: &IoFileSys) -> ConfigEngine { +pub fn load(io: &IoFileSys) -> anyhow::Result { let fs = io.fs.clone(); let config_file = io .rt - .spawn(async move { Ok(fs.read_file("cfg_engine.json".as_ref()).await) }); + .spawn(async move { Ok(fs.read_file("cfg_engine.json".as_ref()).await?) }); let res = config_file.get_storage().unwrap(); - match res { - Ok(file) => ConfigEngine::from_json_string(String::from_utf8(file).unwrap().as_str()) - .unwrap_or_default(), - Err(_) => ConfigEngine::new(), - } + ConfigEngine::from_json_slice(&res) } diff --git a/lib/config/src/config.rs b/lib/config/src/config.rs index 2a3edde..02e38c3 100644 --- a/lib/config/src/config.rs +++ b/lib/config/src/config.rs @@ -235,13 +235,15 @@ impl ConfigEngine { } pub fn to_json_string(&self) -> anyhow::Result { - let res = serde_json::to_string_pretty(self)?; - Ok(res) + Ok(serde_json::to_string_pretty(self)?) } pub fn from_json_string(json_str: &str) -> anyhow::Result { - let res = serde_json::from_str(json_str)?; - Ok(res) + Ok(serde_json::from_str(json_str)?) + } + + pub fn from_json_slice(json: &[u8]) -> anyhow::Result { + Ok(serde_json::from_slice(json)?) } } diff --git a/misc b/misc index f14a00e..31c39b9 160000 --- a/misc +++ b/misc @@ -1 +1 @@ -Subproject commit f14a00e0cd15eb6ce612e1b00b52a2e3d7de546f +Subproject commit 31c39b978117f12fa75f94578d6011afca1b505b diff --git a/src/client/client.rs b/src/client/client.rs index cd4f1a1..6452a06 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -101,6 +101,7 @@ use game_interface::{ }, votes::{VoteIdentifierType, VoteType, Voted}, }; +use game_server::{local_server::start_local_server, server::Server}; use graphics_types::rendering::ColorRgba; use input_binds::binds::{BindKey, Binds}; use math::math::{ @@ -121,7 +122,6 @@ use pool::{ pool::Pool, }; use rayon::ThreadPool; -use server::{local_server::start_local_server, server::Server}; use sound::{scene_object::SceneObject, sound::SoundManager}; use sound_backend::sound_backend::SoundBackend; use steam::{init_steam, traits::SteamRaii}; @@ -181,11 +181,11 @@ pub fn ddnet_main( ) }); - let config_engine = config_fs::load(&io); + let config_engine = config_fs::load(&io).unwrap_or_default(); let benchmark = Benchmark::new(config_engine.dbg.bench); - let config_game = game_config_fs::fs::load(&io); + let config_game = game_config_fs::fs::load(&io).unwrap_or_default(); benchmark.bench("loading client config"); let graphics_backend_io_loading = GraphicsBackendIoLoading::new(&config_engine.gfx, &io); diff --git a/src/client/game/active.rs b/src/client/game/active.rs index 4183ed2..aa94d8d 100644 --- a/src/client/game/active.rs +++ b/src/client/game/active.rs @@ -12,6 +12,14 @@ use demo::{ recorder::{DemoRecorder, DemoRecorderCreateProps}, DemoEvent, }; +use game_base::{ + game_types::time_until_tick, + local_server_info::LocalServerInfo, + network::messages::{ + MsgClInputPlayerChain, MsgClReadyResponse, MsgClSnapshotAck, MsgSvAddLocalPlayerResponse, + PlayerInputChainable, + }, +}; use game_config::config::ConfigGame; use game_interface::{ events::GameEvents, @@ -22,6 +30,11 @@ use game_interface::{ snapshot::SnapshotClientInfo, }, }; +use game_network::messages::{ + ClientToServerMessage, MsgSvLoadVotes, MsgSvStartVoteResult, ServerToClientMessage, +}; +use game_server::server::Server; +use game_state_wasm::game::state_wasm_manager::GameStateWasmManager; use ghost::recorder::GhostRecorder; use pool::{ datatypes::{PoolBTreeMap, PoolVec}, @@ -29,19 +42,6 @@ use pool::{ pool::Pool, rc::PoolRc, }; -use server::server::Server; -use game_base::{ - game_types::time_until_tick, - local_server_info::LocalServerInfo, - network::messages::{ - MsgClInputPlayerChain, MsgClReadyResponse, MsgClSnapshotAck, MsgSvAddLocalPlayerResponse, - PlayerInputChainable, - }, -}; -use game_state_wasm::game::state_wasm_manager::GameStateWasmManager; -use game_network::messages::{ - ClientToServerMessage, MsgSvLoadVotes, MsgSvStartVoteResult, ServerToClientMessage, -}; use url::Url; use crate::{ diff --git a/src/server-bin/Cargo.toml b/src/server/Cargo.toml similarity index 64% rename from src/server-bin/Cargo.toml rename to src/server/Cargo.toml index cab83ef..49915b4 100644 --- a/src/server-bin/Cargo.toml +++ b/src/server/Cargo.toml @@ -1,20 +1,19 @@ [package] -name = "server-bin" +name = "server" version = "0.1.0" edition = "2021" [dependencies] network = { path = "../../lib/network" } base = { path = "../../lib/base" } -server = { path = "../../game/server", default-features = false } +game-server = { path = "../../game/game-server", default-features = false } game-base = { path = "../../game/game-base" } game-config = { path = "../../game/game-config" } -clap = { version = "4.5.23", features = ["derive", "cargo"] } serde_json = "1.0.134" env_logger = "0.11.6" [features] -legacy = ["server/legacy"] +legacy = ["game-server/legacy"] default = ["legacy"] diff --git a/src/server-bin/src/main.rs b/src/server/src/main.rs similarity index 53% rename from src/server-bin/src/main.rs rename to src/server/src/main.rs index c20c9a2..9ea2bf1 100644 --- a/src/server-bin/src/main.rs +++ b/src/server/src/main.rs @@ -1,39 +1,23 @@ use std::sync::{atomic::AtomicBool, Arc}; use base::system::System; -use clap::{arg, command, Command}; -use game_config::config::ConfigGame; -use network::network::utils::create_certifified_keys; -use server::server::ddnet_server_main; use game_base::local_server_info::LocalServerInfo; +use game_server::server::ddnet_server_main; +use network::network::utils::create_certifified_keys; fn main() { - let matches = command!() - .subcommand(Command::new("default_config").about("Print the default config")) - .arg( - arg!(-c --config "A relative path to a config file, used instead of cfg_game.json."), - ) - .get_matches(); - let sys = System::new(); if std::env::var("RUST_LOG").is_err() { unsafe { std::env::set_var("RUST_LOG", "info") }; } env_logger::init(); - if matches - .subcommand_name() - .is_some_and(|cmd| cmd == "default_config") - { - println!( - "{}", - serde_json::to_string_pretty(&ConfigGame::default()).unwrap() - ); - return; + let mut args: Vec<_> = std::env::args().collect(); + // TODO: don't rely on first arg being executable + if !args.is_empty() { + args.remove(0); } - let cfg_game = matches.get_one::("config"); - let cert = create_certifified_keys(); let server_is_open = Arc::new(AtomicBool::new(true)); @@ -47,7 +31,7 @@ fn main() { cert, server_is_open_clone, shared_info, - cfg_game.map(|p| p.as_ref()), + args, None, ) .unwrap();