Focused Rust dependency-maintenance pass from the 2026-05-23 security/freshness review. No CVEs; one advisory cleared and one deprecated crate replaced. - rand 0.9.2 → 0.9.4 (lockfile): clears RUSTSEC-2026-0097 (unsound with a custom logger using rand::rng()). Semver-compatible; rand 0.10 is a separate major. - Compatible-update sweep: ~90 lockfile-only patch/minor bumps (bevy 0.18.0→ 0.18.1, clap 4.5→4.6, rayon 1.11→1.12, pathfinding 4.14→4.15, uuid 1.20→1.23, zerocopy, serde_json, tracing-subscriber, etc.). cargo test green. - serde_yaml 0.9 (deprecated/archived upstream) → serde_norway 0.9, an actively maintained drop-in fork. In the server it is test-only (poi.rs round-trip, trait_modifiers.rs fixture, tests/news_ticker.rs) so it moves to dev-dependencies; line-previewer parses dialogue/monologue pool YAML at runtime, so it keeps it as a normal dependency. API is identical (from_str/ to_string). news_ticker.rs also picks up its share of the #967 clippy sweep (HashSet/HashMap → BTree, doc-list indent) since it is the same file as the serde rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
7.1 KiB
Rust
235 lines
7.1 KiB
Rust
//! line-previewer: CLI tool for previewing dialogue/monologue line selection.
|
|
//!
|
|
//! Answers: "Given this NPC state and player state, what line fires?"
|
|
//! Authoring tool for content authors to test tag behavior without running the game.
|
|
//! Implements the 4-layer dialogue selection pipeline (D-028) in standalone mode.
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use std::path::PathBuf;
|
|
|
|
mod pipeline;
|
|
mod types;
|
|
|
|
use pipeline::{
|
|
evaluate_dialogue, evaluate_monologue, print_coverage_report, print_dialogue_results,
|
|
print_monologue_results, DialogueContext, MonologueContext,
|
|
};
|
|
use types::{DialoguePool, MonologuePool};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "line-previewer",
|
|
about = "Preview dialogue/monologue line selection"
|
|
)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// Preview dialogue line selection
|
|
Dialogue {
|
|
/// Path to dialogue YAML file
|
|
#[arg(short, long)]
|
|
file: PathBuf,
|
|
|
|
/// Access tier(s): public, insider, authority, peer, hostile
|
|
#[arg(short, long, value_delimiter = ',')]
|
|
access: Vec<String>,
|
|
|
|
/// Trust level: surface, real, secret
|
|
#[arg(short, long, default_value = "surface")]
|
|
trust: String,
|
|
|
|
/// Active situation(s): arrival, shift_start, shift_end, etc.
|
|
#[arg(short, long, value_delimiter = ',')]
|
|
situation: Vec<String>,
|
|
|
|
/// Topic filter(s): colleague, routine, cargo, etc.
|
|
#[arg(long, value_delimiter = ',')]
|
|
topic: Vec<String>,
|
|
|
|
/// Mood filter(s): fond, comfortable, worried, etc.
|
|
#[arg(long, value_delimiter = ',')]
|
|
mood: Vec<String>,
|
|
|
|
/// Show detailed filter pass/fail for each line
|
|
#[arg(short, long)]
|
|
verbose: bool,
|
|
|
|
/// RNG seed for weighted selection (default: 42)
|
|
#[arg(long, default_value = "42")]
|
|
seed: u64,
|
|
},
|
|
|
|
/// Preview monologue line selection
|
|
Monologue {
|
|
/// Path to monologue YAML file
|
|
#[arg(short, long)]
|
|
file: PathBuf,
|
|
|
|
/// Character: smuggler or detective
|
|
#[arg(short, long)]
|
|
character: String,
|
|
|
|
/// Trigger type: enter_location, observe_npc, etc.
|
|
#[arg(short, long)]
|
|
trigger: String,
|
|
|
|
/// Known fact IDs (for prerequisite evaluation)
|
|
#[arg(long, value_delimiter = ',')]
|
|
known_facts: Vec<String>,
|
|
|
|
/// Show detailed prerequisite evaluation for each line
|
|
#[arg(short, long)]
|
|
verbose: bool,
|
|
|
|
/// RNG seed for weighted selection (default: 42)
|
|
#[arg(long, default_value = "42")]
|
|
seed: u64,
|
|
},
|
|
|
|
/// Show coverage report — which filter combinations have zero eligible lines
|
|
Coverage {
|
|
/// Path to dialogue or monologue YAML file
|
|
#[arg(short, long)]
|
|
file: PathBuf,
|
|
},
|
|
|
|
/// Preview a sequence of monologue lines (walk-through simulation)
|
|
Sequence {
|
|
/// Path to monologue YAML file
|
|
#[arg(short, long)]
|
|
file: PathBuf,
|
|
|
|
/// Character: smuggler or detective
|
|
#[arg(short, long)]
|
|
character: String,
|
|
|
|
/// Trigger sequence (comma-separated): enter_location,observe_npc,time_idle
|
|
#[arg(short, long, value_delimiter = ',')]
|
|
triggers: Vec<String>,
|
|
|
|
/// RNG seed (default: 42)
|
|
#[arg(long, default_value = "42")]
|
|
seed: u64,
|
|
},
|
|
}
|
|
|
|
fn main() {
|
|
let cli = Cli::parse();
|
|
|
|
match cli.command {
|
|
Command::Dialogue {
|
|
file,
|
|
access,
|
|
trust,
|
|
situation,
|
|
topic,
|
|
mood,
|
|
verbose,
|
|
seed,
|
|
} => {
|
|
let pool = load_dialogue(&file);
|
|
let ctx = DialogueContext {
|
|
access,
|
|
trust,
|
|
situation,
|
|
topic,
|
|
mood,
|
|
seed,
|
|
};
|
|
let results = evaluate_dialogue(&pool, &ctx);
|
|
print_dialogue_results(&results, verbose, seed);
|
|
}
|
|
Command::Monologue {
|
|
file,
|
|
character,
|
|
trigger,
|
|
known_facts,
|
|
verbose,
|
|
seed,
|
|
} => {
|
|
let pool = load_monologue(&file);
|
|
if pool.character != character {
|
|
eprintln!(
|
|
"Warning: file character '{}' doesn't match requested '{}'",
|
|
pool.character, character
|
|
);
|
|
}
|
|
let ctx = MonologueContext {
|
|
trigger,
|
|
known_facts,
|
|
seed,
|
|
};
|
|
let results = evaluate_monologue(&pool, &ctx);
|
|
print_monologue_results(&results, verbose, seed);
|
|
}
|
|
Command::Coverage { file } => {
|
|
let yaml_str = std::fs::read_to_string(&file).unwrap_or_else(|e| {
|
|
eprintln!("Error reading {}: {e}", file.display());
|
|
std::process::exit(1);
|
|
});
|
|
print_coverage_report(&yaml_str, &file);
|
|
}
|
|
Command::Sequence {
|
|
file,
|
|
character,
|
|
triggers,
|
|
seed,
|
|
} => {
|
|
let pool = load_monologue(&file);
|
|
if pool.character != character {
|
|
eprintln!(
|
|
"Warning: file character '{}' doesn't match requested '{}'",
|
|
pool.character, character
|
|
);
|
|
}
|
|
println!("[Sequence: {} — {}]", file.display(), character);
|
|
let mut used_ids: Vec<String> = Vec::new();
|
|
for (i, trigger) in triggers.iter().enumerate() {
|
|
let ctx = MonologueContext {
|
|
trigger: trigger.clone(),
|
|
known_facts: vec![],
|
|
seed: seed.wrapping_add(i as u64),
|
|
};
|
|
let results = evaluate_monologue(&pool, &ctx);
|
|
// Filter out recently used lines
|
|
let eligible: Vec<_> = results
|
|
.iter()
|
|
.filter(|r| r.passed && !used_ids.contains(&r.line_id))
|
|
.collect();
|
|
if let Some(selected) = eligible.first() {
|
|
println!("{}. ({}) {:?}", i + 1, trigger, selected.text);
|
|
used_ids.push(selected.line_id.clone());
|
|
} else {
|
|
println!("{}. ({}) [no eligible line]", i + 1, trigger);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_dialogue(path: &PathBuf) -> DialoguePool {
|
|
let yaml_str = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
|
eprintln!("Error reading {}: {e}", path.display());
|
|
std::process::exit(1);
|
|
});
|
|
serde_norway::from_str(&yaml_str).unwrap_or_else(|e| {
|
|
eprintln!("Error parsing dialogue YAML: {e}");
|
|
std::process::exit(1);
|
|
})
|
|
}
|
|
|
|
fn load_monologue(path: &PathBuf) -> MonologuePool {
|
|
let yaml_str = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
|
eprintln!("Error reading {}: {e}", path.display());
|
|
std::process::exit(1);
|
|
});
|
|
serde_norway::from_str(&yaml_str).unwrap_or_else(|e| {
|
|
eprintln!("Error parsing monologue YAML: {e}");
|
|
std::process::exit(1);
|
|
})
|
|
}
|