//! RON content validator CLI (#611). //! //! Deserializes a RON file into the actual Rust structs and prints errors. //! This is the copy team's lint tool — run it to check RON files without //! needing to compile the full server. //! //! # Usage //! //! ```sh //! # Via wrapper script (recommended): //! tooling/validate-ron server/content/global/zone-identity-spec.example.ron zone //! tooling/validate-ron server/content/global/culture-van-maanens-star.example.ron culture //! //! # Direct: //! cargo run --bin validate_ron -- //! ``` use std::process; use clap::Parser; use settled_reach_server::npc::blueprint::{CultureProfile, ZoneSpec, ZoneTypeTemplate}; #[derive(Parser)] #[command( name = "validate_ron", about = "Validate RON content files against Rust struct schemas" )] struct Args { /// Path to the RON file to validate. file: String, /// Schema type: "zone" (ZoneSpec), "zone_type" (ZoneTypeTemplate), or "culture" (CultureProfile). schema: String, } fn main() { let args = Args::parse(); let content = match std::fs::read_to_string(&args.file) { Ok(c) => c, Err(e) => { eprintln!("Error reading {}: {}", args.file, e); process::exit(1); } }; match args.schema.as_str() { "zone" => match ron::from_str::(&content) { Ok(spec) => { println!("Valid ZoneSpec: {} ({})", spec.label, spec.zone_type); println!( " {} roles, {} social sites", spec.roles.len(), spec.social_sites.len() ); let mut warnings = 0; if spec.roles.is_empty() { eprintln!(" WARNING: no roles defined — generator will reject this"); warnings += 1; } if spec.population_density < 1 { eprintln!(" WARNING: population_density is 0 — generator requires >= 1"); warnings += 1; } for site in &spec.social_sites { if site.min_npcs > site.max_npcs { eprintln!( " WARNING: social site '{}' has min_npcs ({}) > max_npcs ({})", site.site_type, site.min_npcs, site.max_npcs ); warnings += 1; } } if warnings > 0 { process::exit(1); } } Err(e) => { eprintln!("Invalid ZoneSpec in {}:", args.file); eprintln!(" {}", e); process::exit(1); } }, "culture" => match ron::from_str::(&content) { Ok(profile) => { println!("Valid CultureProfile: {} ({})", profile.name, profile.id); println!( " {} given names, {} family names", profile.naming.given_names.len(), profile.naming.family_names.len() ); println!( " {} filler words, {} favored traits", profile.speech.filler_words.len(), profile.values.favored_traits.len() ); let mut warnings = 0; if profile.naming.given_names.is_empty() { eprintln!(" WARNING: no given_names — generator will reject this"); warnings += 1; } if warnings > 0 { process::exit(1); } } Err(e) => { eprintln!("Invalid CultureProfile in {}:", args.file); eprintln!(" {}", e); process::exit(1); } }, "zone_type" => match ron::from_str::(&content) { Ok(template) => { println!( "Valid ZoneTypeTemplate: {} ({})", template.label, template.id ); println!( " {} roles, {} social site types", template.roles.len(), template.social_site_types.len() ); let mut warnings = 0; if template.roles.is_empty() { eprintln!(" WARNING: no roles defined — generator will reject this"); warnings += 1; } let role_ids: Vec<&str> = template.roles.iter().map(|r| r.id.as_str()).collect(); for role in &template.roles { if role.behavior_primitives.is_empty() { eprintln!(" WARNING: role '{}' has no behavior_primitives", role.id); warnings += 1; } } for site in &template.social_site_types { for eligible in &site.eligible_roles { if !role_ids.contains(&eligible.as_str()) { eprintln!( " WARNING: social site '{}' references eligible_role '{}' which is not a defined role", site.site_type, eligible ); warnings += 1; } } } if warnings > 0 { process::exit(1); } } Err(e) => { eprintln!("Invalid ZoneTypeTemplate in {}:", args.file); eprintln!(" {}", e); process::exit(1); } }, other => { eprintln!( "Unknown schema type: '{}'. Use 'zone', 'zone_type', or 'culture'.", other ); process::exit(1); } } }