From 93862c3c340b8235008c056babcac682347ed726 Mon Sep 17 00:00:00 2001 From: AI Agent Date: Sat, 14 Mar 2026 18:18:58 -0600 Subject: [PATCH] Add world validation command to mudtool - Introduced a new command `validate` to check the integrity of world data, ensuring all referenced entities (NPCs, objects, guilds, races, classes, spells) exist and have valid attributes. - Updated help message to include usage of the new command and its options. - Added support for specifying a world directory via command line argument. --- .gitea/workflows/smoke-tests.yml | 4 +- src/bin/mudtool.rs | 119 ++++++++++++++++++++++++++++--- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/smoke-tests.yml b/.gitea/workflows/smoke-tests.yml index 12f703e..fd36b6b 100644 --- a/.gitea/workflows/smoke-tests.yml +++ b/.gitea/workflows/smoke-tests.yml @@ -12,7 +12,9 @@ jobs: uses: actions/checkout@v4 - name: Install Rust - run: sudo apt install -y cargo + run: | + sudo apt update + sudo apt install -y cargo - name: Build run: cargo build diff --git a/src/bin/mudtool.rs b/src/bin/mudtool.rs index f7d96f0..257c8ce 100644 --- a/src/bin/mudtool.rs +++ b/src/bin/mudtool.rs @@ -9,11 +9,12 @@ use ratatui::prelude::*; use ratatui::widgets::*; use mudserver::db::{GameDb, NpcAttitudeRow, SavedPlayer, SqliteDb}; -use mudserver::world::Attitude; +use mudserver::world::{Attitude, World}; fn main() { let args: Vec = std::env::args().collect(); let mut db_path = PathBuf::from("./mudserver.db"); + let mut world_path = PathBuf::from("./world"); let mut cmd_args: Vec = Vec::new(); let mut i = 1; @@ -23,6 +24,10 @@ fn main() { i += 1; db_path = PathBuf::from(args.get(i).expect("--db requires a path")); } + "--world" | "-w" => { + i += 1; + world_path = PathBuf::from(args.get(i).expect("--world requires a path")); + } "--help" | "-h" => { print_help(); return; @@ -32,6 +37,16 @@ fn main() { i += 1; } + if cmd_args.is_empty() { + print_help(); + return; + } + + if cmd_args[0] == "validate" { + cmd_validate(&world_path); + return; + } + let db = match SqliteDb::open(&db_path) { Ok(db) => db, Err(e) => { @@ -40,11 +55,6 @@ fn main() { } }; - if cmd_args.is_empty() { - print_help(); - return; - } - match cmd_args[0].as_str() { "tui" => run_tui(db), "players" => cmd_players(&db, &cmd_args[1..]), @@ -60,9 +70,10 @@ fn main() { fn print_help() { eprintln!("mudtool - MUD Server Database Manager"); eprintln!(); - eprintln!("Usage: mudtool [--db ] [args...]"); + eprintln!("Usage: mudtool [OPTIONS] [args...]"); eprintln!(); eprintln!("Commands:"); + eprintln!(" validate Validate world data (schemas, references, values)"); eprintln!(" tui Interactive TUI editor"); eprintln!(" players list List all players"); eprintln!(" players show Show player details"); @@ -75,7 +86,99 @@ fn print_help() { eprintln!(" attitudes set Set attitude value"); eprintln!(); eprintln!("Options:"); - eprintln!(" --db, -d Database path (default: ./mudserver.db)"); + eprintln!(" --db, -d Database path (default: ./mudserver.db)"); + eprintln!(" --world, -w World directory for validate (default: ./world)"); +} + +fn cmd_validate(world_path: &std::path::Path) { + let world = match World::load(world_path) { + Ok(w) => w, + Err(e) => { + eprintln!("Validation failed (load): {e}"); + std::process::exit(1); + } + }; + + let mut errors = Vec::new(); + + for npc in world.npcs.values() { + if !world.rooms.contains_key(&npc.room) { + errors.push(format!("NPC {} room '{}' does not exist", npc.id, npc.room)); + } + if let Some(ref rid) = npc.fixed_race { + if !world.races.iter().any(|r| r.id == *rid) { + errors.push(format!("NPC {} race '{}' does not exist", npc.id, rid)); + } + } + if let Some(ref cid) = npc.fixed_class { + if !world.classes.iter().any(|c| c.id == *cid) { + errors.push(format!("NPC {} class '{}' does not exist", npc.id, cid)); + } + } + if let Some(ref c) = npc.combat { + if c.max_hp <= 0 || c.attack < 0 || c.defense < 0 || c.xp_reward < 0 { + errors.push(format!("NPC {} has invalid combat stats (hp>0, atk/def/xp>=0)", npc.id)); + } + } + } + + for obj in world.objects.values() { + if let Some(ref rid) = obj.room { + if !world.rooms.contains_key(rid) { + errors.push(format!("Object {} room '{}' does not exist", obj.id, rid)); + } + } + } + + for guild in world.guilds.values() { + for sid in &guild.spells { + if !world.spells.contains_key(sid) { + errors.push(format!("Guild {} spell '{}' does not exist", guild.id, sid)); + } + } + for rid in &guild.race_restricted { + if !world.races.iter().any(|r| r.id == *rid) { + errors.push(format!("Guild {} race_restricted '{}' does not exist", guild.id, rid)); + } + } + if guild.resource != "mana" && guild.resource != "endurance" { + errors.push(format!("Guild {} resource '{}' must be 'mana' or 'endurance'", guild.id, guild.resource)); + } + } + + for class in &world.classes { + if let Some(ref gid) = class.guild { + if !world.guilds.contains_key(gid) { + errors.push(format!("Class {} guild '{}' does not exist", class.id, gid)); + } + } + } + + for race in &world.races { + if let Some(ref cid) = race.default_class { + if !world.classes.iter().any(|c| c.id == *cid) { + errors.push(format!("Race {} default_class '{}' does not exist", race.id, cid)); + } + } + } + + for spell in world.spells.values() { + if !["offensive", "heal", "utility"].contains(&spell.spell_type.as_str()) { + errors.push(format!("Spell {} spell_type '{}' must be offensive/heal/utility", spell.id, spell.spell_type)); + } + } + + if errors.is_empty() { + println!("World validation OK: {} rooms, {} npcs, {} objects, {} races, {} classes, {} guilds, {} spells", + world.rooms.len(), world.npcs.len(), world.objects.len(), + world.races.len(), world.classes.len(), world.guilds.len(), world.spells.len()); + } else { + for e in &errors { + eprintln!("Error: {e}"); + } + eprintln!("\n{} validation error(s)", errors.len()); + std::process::exit(1); + } } // ============ CLI Commands ============