diff --git a/rc_auth/src/cli.rs b/rc_auth/src/cli.rs index 1b57397..9f1a77b 100644 --- a/rc_auth/src/cli.rs +++ b/rc_auth/src/cli.rs @@ -18,6 +18,10 @@ pub struct CliArgs { /// User data root #[arg(long, default_value_t = {"../data/robocraft".to_string()})] pub data_robocraft: String, + + /// Verify configuration and then exit + #[arg(long)] + pub validate: bool, } impl CliArgs { diff --git a/rc_auth/src/main.rs b/rc_auth/src/main.rs index 801e977..b4f6f9d 100644 --- a/rc_auth/src/main.rs +++ b/rc_auth/src/main.rs @@ -18,6 +18,18 @@ async fn index() -> impl Responder { async fn main() -> std::io::Result<()> { env_logger::init(); let cli_args = cli::CliArgs::get(); + + if cli_args.validate { + let conf = oj_rc_core::ConfigImpl::load(cli_args.assets_robocraft)?; + let res = if conf.self_validate(&cli_args.data_robocraft) { + log::info!("Config validated successfully (exit success)"); + Ok(()) + } else { + Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Config failed validation (exit failure)")) + }; + return res; + } + let cli_args2 = actix_web::web::Data::new(cli_args.clone()); let rc_preloaded = actix_web::web::Data::new(cli_args.clone().preloaded().await); let internal_auth = actix_web::web::Data::new(crate::robocraft::intercom::IntercomAuth::new(&cli_args.data_robocraft)?); diff --git a/rc_core/src/persist/chat.rs b/rc_core/src/persist/chat.rs index b3aec2d..656efd3 100644 --- a/rc_core/src/persist/chat.rs +++ b/rc_core/src/persist/chat.rs @@ -13,6 +13,14 @@ pub struct ChatConfig { pub can_create_channels: bool, } +impl super::config::SelfValidator for ChatConfig { + type Context = crate::ConfigImpl; + fn validate(&self, _info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + // TODO + true + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ChatCommand { pub regex: String, diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index d6e5c92..6dc0b2b 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -20,6 +20,22 @@ pub struct BattleConfig { pub energy: EnergyConfig, } +impl super::config::SelfValidator for BattleConfig { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, ctx: &Self::Context) -> bool { + let mut is_ok = true; + // TODO regen + // TODO votes + // TODO games + is_ok &= self.singleplayer.validate_in(info, ctx, "singleplayer"); + is_ok &= self.rotation.validate_in(info, ctx, "rotation"); + // TODO multiplayer + // TODO maps + // TODO energy + is_ok + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct AutoRegenHealth { pub wait_for_heal_s: f32, @@ -117,6 +133,18 @@ pub struct GameEventSequence { pub modes: Vec, } +impl super::config::SelfValidator for GameEventSequence { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, ctx: &Self::Context) -> bool { + // TODO + let mut is_ok = true; + for (i, mode) in self.modes.iter().enumerate() { + is_ok &= mode.validate_in(info, ctx, &format!("modes[{}]", i)); + } + is_ok + } +} + #[derive(Serialize, Deserialize, Clone, Debug, Copy)] pub enum GameRotationStrategy { Sequence, @@ -139,6 +167,28 @@ pub struct GameEvents { pub duration_s: u64, // seconds } +impl super::config::SelfValidator for GameEvents { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + // TODO + let mut is_ok = true; + if !matches!(self.singleplayer.mode, GameType::SuddenDeath) { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["singleplayer".to_owned(), "mode".to_owned()], + message: format!("Singleplayer game mode {:?} will be overidden by the client", self.singleplayer.mode), + }); + } + if self.duration_s == 0 { + info.error(crate::persist::config::ValidationMessage { + path: vec!["duration_s".to_owned()], + message: "Duration cannot be zero".to_owned(), + }); + is_ok = false; + } + is_ok + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct GameEvent { pub map: GameMap, diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 2122a00..3b0d9fb 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -5,6 +5,8 @@ use serde::{Serialize, Deserialize}; use polariton::operation::{Typed, Dict}; use polariton::serdes::TypePrefix; +use crate::persist::config::SelfValidator; + use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig}; const CUBE_CONFIG_FILENAME: &str = "config.json"; @@ -34,6 +36,41 @@ impl CubeConfig { } Ok(result) } + + /// Performs configuration checks + /// Returns true if validation succeeds, false if failed + pub fn self_validate(&self, data_path: impl AsRef) -> bool { + let mut validation_info = super::ValidationInfo::default(); + validation_info.info(super::ValidationMessage { + path: vec![], + message: format!("Validation started at {}", chrono::Utc::now()), + }); + let token_path = data_path.as_ref().join(crate::persist::user::TOKEN_SECRET_FILENAME); + if !token_path.exists() { + validation_info.error(crate::persist::config::ValidationMessage { + path: vec![], + message: format!("Token secret file does not exist; create it at {}", token_path.display()), + }); + } + // TODO cubes + // TODO movement + let battle_res = self.battle.validate_in(&mut validation_info, self, "battle"); + let chat_res = self.chat.validate_in(&mut validation_info, self, "chat"); + let factory_res = self.factory.validate_in(&mut validation_info, self, "factory"); + let settings_res = self.settings.validate_in(&mut validation_info, self, "settings"); + + validation_info.info(super::ValidationMessage { + path: vec![], + message: format!("Validation ended at {}", chrono::Utc::now()), + }); + + validation_info.print_messages(); + battle_res + && chat_res + && factory_res + && settings_res + && validation_info.is_ok() + } } #[async_trait::async_trait] diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index fedb2c4..7e32e6e 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -4,6 +4,9 @@ pub use cubes_json::CubeConfig; mod traits; pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings}; +mod validation; +pub use validation::{SelfValidator, ValidationInfo, ValidationMessage}; + pub type ConfigImpl = CubeConfig; fn __must_impl>() {} diff --git a/rc_core/src/persist/config/validation.rs b/rc_core/src/persist/config/validation.rs new file mode 100644 index 0000000..7444dce --- /dev/null +++ b/rc_core/src/persist/config/validation.rs @@ -0,0 +1,93 @@ +#[derive(Debug, Default)] +pub struct ValidationInfo { + errors: Vec, + warnings: Vec, + infos: Vec, + context: Vec, +} + +impl ValidationInfo { + pub fn error(&mut self, mut msg: ValidationMessage) { + self.prepend_context_path(&mut msg); + self.errors.push(msg); + } + + pub fn warn(&mut self, mut msg: ValidationMessage) { + self.prepend_context_path(&mut msg); + self.warnings.push(msg); + } + + pub fn info(&mut self, mut msg: ValidationMessage) { + self.prepend_context_path(&mut msg); + self.infos.push(msg); + } + + pub fn push_context(&mut self, path: &str) { + self.context.push(path.to_owned()); + } + + pub fn pop_context(&mut self) -> String { + self.context.pop().expect("Bad validation context state") + } + + fn prepend_context_path(&self, msg: &mut ValidationMessage) { + let mut new_path = self.context.clone(); + new_path.append(&mut msg.path); + msg.path = new_path; + } + + pub(super) fn print_messages(&self) { + const ERR_LABEL: &str = "ERROR"; + const WARN_LABEL: &str = "WARN"; + const INFO_LABEL: &str = "INFO"; + Self::print_infos(&self.infos, INFO_LABEL); + Self::print_infos(&self.warnings, WARN_LABEL); + Self::print_infos(&self.errors, ERR_LABEL); + } + + fn print_infos(infos: &[ValidationMessage], label: &str) { + for info in infos { + println!("[{}]{}", label, info.display()); + } + } + + pub(super) fn is_ok(&self) -> bool { + self.errors.is_empty() + } +} + +#[derive(Debug)] +pub struct ValidationMessage { + pub path: Vec, + pub message: String, +} + +impl ValidationMessage { + pub fn new(path: Vec, message: String) -> Self { + Self { + path, + message, + } + } + + fn display(&self) -> String { + let path = if self.path.is_empty() { + "".to_owned() + } else { + self.path.join(".") + }; + format!("{}: {}", path, self.message) + } +} + +pub trait SelfValidator { + type Context: ?Sized; + fn validate(&self, info: &mut ValidationInfo, ctx: &Self::Context) -> bool; + + fn validate_in(&self, info: &mut ValidationInfo, ctx: &Self::Context, path: &str) -> bool { + info.push_context(path); + let res = self.validate(info, ctx); + assert_eq!(info.pop_context(), path); + res + } +} diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs index a92b885..a150c44 100644 --- a/rc_core/src/persist/multiplayer.rs +++ b/rc_core/src/persist/multiplayer.rs @@ -16,6 +16,45 @@ pub struct MultiplayerConfig { pub team_death_match: TeamDeathMatchConfig, } +impl super::config::SelfValidator for MultiplayerConfig { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + let mut is_ok = true; + if !self.enabled { + info.info(super::config::ValidationMessage { + path: vec!["enabled".to_owned()], + message: "Multiplayer is disabled so validation is skipped".to_owned(), + }); + return true; + } + if self.players_per_game == 0 { + info.error(super::config::ValidationMessage { + path: vec!["players_per_game".to_owned()], + message: "Game match must have at least one player to start".to_owned(), + }); + is_ok = false; + } else if self.players_per_game == 1 { + if self.fakes.iter().any(|fake| fake.team != 0 && matches!(fake.implementation, ClientEmulation::ClientAI)) { + info.error(crate::persist::config::ValidationMessage { + path: vec!["players_per_game".to_owned()], + message: "Game match cannot have enemy ClientAI fakes when there are no real enemies".to_owned(), + }); + is_ok = false; + } else { + info.warn(super::config::ValidationMessage { + path: vec!["players_per_game".to_owned()], + message: "Game match may be lonely with only one player".to_owned(), + }); + } + + } + // TODO campaigns + // TODO vehicles + // TODO + is_ok + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct NetworkConf { pub network_channel_ty: String, diff --git a/rc_core/src/persist/settings.rs b/rc_core/src/persist/settings.rs index e8800b0..d3b6d03 100644 --- a/rc_core/src/persist/settings.rs +++ b/rc_core/src/persist/settings.rs @@ -12,6 +12,14 @@ pub struct Settings { pub server: ServerSettings, } +impl super::config::SelfValidator for Settings { + type Context = crate::ConfigImpl; + fn validate(&self, _info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + // TODO + true + } +} + fn default_gameplay_settings() -> super::GameplaySettings { super::GameplaySettings { show_tutorial_after_date: "2030-01-01".to_owned(), diff --git a/rc_core/src/persist/singleplayer.rs b/rc_core/src/persist/singleplayer.rs index 43d20cf..b76a9b6 100644 --- a/rc_core/src/persist/singleplayer.rs +++ b/rc_core/src/persist/singleplayer.rs @@ -27,6 +27,28 @@ impl SingleplayerConfig { } } +impl super::config::SelfValidator for SingleplayerConfig { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + //let mut is_ok = true; + // TODO campaigns + // TODO vehicles + if self.max_teammates == 0 { + info.warn(super::config::ValidationMessage { + path: vec!["max_teammates".to_owned()], + message: "Player's team may be lonely with zero teammates".to_owned(), + }); + } + if self.max_enemies == 0 { + info.warn(super::config::ValidationMessage { + path: vec!["max_enemies".to_owned()], + message: "Singleplayer game may be boring with zero enemies".to_owned(), + }); + } + true + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Campaign { pub id: String, diff --git a/rc_core/src/persist/vehicle_factory.rs b/rc_core/src/persist/vehicle_factory.rs index 04bbbab..039d763 100644 --- a/rc_core/src/persist/vehicle_factory.rs +++ b/rc_core/src/persist/vehicle_factory.rs @@ -6,6 +6,14 @@ pub struct FactoryConfig { pub adapter: AdapterSettings, } +impl super::config::SelfValidator for FactoryConfig { + type Context = crate::ConfigImpl; + fn validate(&self, _info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + // TODO + true + } +} + fn default_variant() -> AdapterSettings { AdapterSettings::None }