Add world validation command to mudtool
Some checks failed
Smoke tests / Build and smoke test (push) Failing after 16s

- 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.
This commit is contained in:
AI Agent
2026-03-14 18:18:58 -06:00
parent 7c50bbf01a
commit 93862c3c34
2 changed files with 114 additions and 9 deletions

View File

@@ -12,7 +12,9 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust - name: Install Rust
run: sudo apt install -y cargo run: |
sudo apt update
sudo apt install -y cargo
- name: Build - name: Build
run: cargo build run: cargo build

View File

@@ -9,11 +9,12 @@ use ratatui::prelude::*;
use ratatui::widgets::*; use ratatui::widgets::*;
use mudserver::db::{GameDb, NpcAttitudeRow, SavedPlayer, SqliteDb}; use mudserver::db::{GameDb, NpcAttitudeRow, SavedPlayer, SqliteDb};
use mudserver::world::Attitude; use mudserver::world::{Attitude, World};
fn main() { fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let mut db_path = PathBuf::from("./mudserver.db"); let mut db_path = PathBuf::from("./mudserver.db");
let mut world_path = PathBuf::from("./world");
let mut cmd_args: Vec<String> = Vec::new(); let mut cmd_args: Vec<String> = Vec::new();
let mut i = 1; let mut i = 1;
@@ -23,6 +24,10 @@ fn main() {
i += 1; i += 1;
db_path = PathBuf::from(args.get(i).expect("--db requires a path")); 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" => { "--help" | "-h" => {
print_help(); print_help();
return; return;
@@ -32,6 +37,16 @@ fn main() {
i += 1; 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) { let db = match SqliteDb::open(&db_path) {
Ok(db) => db, Ok(db) => db,
Err(e) => { Err(e) => {
@@ -40,11 +55,6 @@ fn main() {
} }
}; };
if cmd_args.is_empty() {
print_help();
return;
}
match cmd_args[0].as_str() { match cmd_args[0].as_str() {
"tui" => run_tui(db), "tui" => run_tui(db),
"players" => cmd_players(&db, &cmd_args[1..]), "players" => cmd_players(&db, &cmd_args[1..]),
@@ -60,9 +70,10 @@ fn main() {
fn print_help() { fn print_help() {
eprintln!("mudtool - MUD Server Database Manager"); eprintln!("mudtool - MUD Server Database Manager");
eprintln!(); eprintln!();
eprintln!("Usage: mudtool [--db <path>] <command> [args...]"); eprintln!("Usage: mudtool [OPTIONS] <command> [args...]");
eprintln!(); eprintln!();
eprintln!("Commands:"); eprintln!("Commands:");
eprintln!(" validate Validate world data (schemas, references, values)");
eprintln!(" tui Interactive TUI editor"); eprintln!(" tui Interactive TUI editor");
eprintln!(" players list List all players"); eprintln!(" players list List all players");
eprintln!(" players show <name> Show player details"); eprintln!(" players show <name> Show player details");
@@ -76,6 +87,98 @@ fn print_help() {
eprintln!(); eprintln!();
eprintln!("Options:"); eprintln!("Options:");
eprintln!(" --db, -d <path> Database path (default: ./mudserver.db)"); eprintln!(" --db, -d <path> Database path (default: ./mudserver.db)");
eprintln!(" --world, -w <path> 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 ============ // ============ CLI Commands ============