mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Add self-validation framework for config
This commit is contained in:
@@ -18,6 +18,10 @@ pub struct CliArgs {
|
|||||||
/// User data root
|
/// User data root
|
||||||
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
|
#[arg(long, default_value_t = {"../data/robocraft".to_string()})]
|
||||||
pub data_robocraft: String,
|
pub data_robocraft: String,
|
||||||
|
|
||||||
|
/// Verify configuration and then exit
|
||||||
|
#[arg(long)]
|
||||||
|
pub validate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CliArgs {
|
impl CliArgs {
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ async fn index() -> impl Responder {
|
|||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let cli_args = cli::CliArgs::get();
|
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 cli_args2 = actix_web::web::Data::new(cli_args.clone());
|
||||||
let rc_preloaded = actix_web::web::Data::new(cli_args.clone().preloaded().await);
|
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)?);
|
let internal_auth = actix_web::web::Data::new(crate::robocraft::intercom::IntercomAuth::new(&cli_args.data_robocraft)?);
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ pub struct ChatConfig {
|
|||||||
pub can_create_channels: bool,
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ChatCommand {
|
pub struct ChatCommand {
|
||||||
pub regex: String,
|
pub regex: String,
|
||||||
|
|||||||
@@ -20,6 +20,22 @@ pub struct BattleConfig {
|
|||||||
pub energy: EnergyConfig,
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct AutoRegenHealth {
|
pub struct AutoRegenHealth {
|
||||||
pub wait_for_heal_s: f32,
|
pub wait_for_heal_s: f32,
|
||||||
@@ -117,6 +133,18 @@ pub struct GameEventSequence {
|
|||||||
pub modes: Vec<GameEvents>,
|
pub modes: Vec<GameEvents>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
|
||||||
pub enum GameRotationStrategy {
|
pub enum GameRotationStrategy {
|
||||||
Sequence,
|
Sequence,
|
||||||
@@ -139,6 +167,28 @@ pub struct GameEvents {
|
|||||||
pub duration_s: u64, // seconds
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct GameEvent {
|
pub struct GameEvent {
|
||||||
pub map: GameMap,
|
pub map: GameMap,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use serde::{Serialize, Deserialize};
|
|||||||
use polariton::operation::{Typed, Dict};
|
use polariton::operation::{Typed, Dict};
|
||||||
use polariton::serdes::TypePrefix;
|
use polariton::serdes::TypePrefix;
|
||||||
|
|
||||||
|
use crate::persist::config::SelfValidator;
|
||||||
|
|
||||||
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig};
|
use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig};
|
||||||
|
|
||||||
const CUBE_CONFIG_FILENAME: &str = "config.json";
|
const CUBE_CONFIG_FILENAME: &str = "config.json";
|
||||||
@@ -34,6 +36,41 @@ impl CubeConfig {
|
|||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs configuration checks
|
||||||
|
/// Returns true if validation succeeds, false if failed
|
||||||
|
pub fn self_validate(&self, data_path: impl AsRef<std::path::Path>) -> 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]
|
#[async_trait::async_trait]
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ pub use cubes_json::CubeConfig;
|
|||||||
mod traits;
|
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};
|
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;
|
pub type ConfigImpl = CubeConfig;
|
||||||
|
|
||||||
fn __must_impl<T: ConfigProvider<()>>() {}
|
fn __must_impl<T: ConfigProvider<()>>() {}
|
||||||
|
|||||||
93
rc_core/src/persist/config/validation.rs
Normal file
93
rc_core/src/persist/config/validation.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct ValidationInfo {
|
||||||
|
errors: Vec<ValidationMessage>,
|
||||||
|
warnings: Vec<ValidationMessage>,
|
||||||
|
infos: Vec<ValidationMessage>,
|
||||||
|
context: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidationMessage {
|
||||||
|
pub fn new(path: Vec<String>, message: String) -> Self {
|
||||||
|
Self {
|
||||||
|
path,
|
||||||
|
message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display(&self) -> String {
|
||||||
|
let path = if self.path.is_empty() {
|
||||||
|
"<root>".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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,45 @@ pub struct MultiplayerConfig {
|
|||||||
pub team_death_match: TeamDeathMatchConfig,
|
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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct NetworkConf {
|
pub struct NetworkConf {
|
||||||
pub network_channel_ty: String,
|
pub network_channel_ty: String,
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ pub struct Settings {
|
|||||||
pub server: ServerSettings,
|
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 {
|
fn default_gameplay_settings() -> super::GameplaySettings {
|
||||||
super::GameplaySettings {
|
super::GameplaySettings {
|
||||||
show_tutorial_after_date: "2030-01-01".to_owned(),
|
show_tutorial_after_date: "2030-01-01".to_owned(),
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Campaign {
|
pub struct Campaign {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ pub struct FactoryConfig {
|
|||||||
pub adapter: AdapterSettings,
|
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 {
|
fn default_variant() -> AdapterSettings {
|
||||||
AdapterSettings::None
|
AdapterSettings::None
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user