use std::path::PathBuf;
use argh::FromArgs;
#[derive(FromArgs)]
pub(crate) struct Cli {
#[argh(subcommand)]
pub(crate) command: Command,
}
#[derive(FromArgs)]
#[argh(subcommand)]
pub(crate) enum Command {
Convert(ConvertCmd),
Inspect(InspectCmd),
Init(InitCmd),
New(NewCmd),
Pack(PackCmd),
Unpack(UnpackCmd),
Nwsync(NwsyncCmd),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "convert")]
pub(crate) struct ConvertCmd {
#[argh(switch, short = 'f')]
pub(crate) force: bool,
#[argh(option, default = "String::from(\"dxt5\")")]
pub(crate) dds_format: String,
#[argh(option)]
pub(crate) root: Option<PathBuf>,
#[argh(option)]
pub(crate) user: Option<PathBuf>,
#[argh(option, default = "String::from(\"english\")")]
pub(crate) language: String,
#[argh(switch)]
pub(crate) load_ovr: bool,
#[argh(option)]
pub(crate) animation: Option<String>,
#[argh(option)]
pub(crate) time: Option<f32>,
#[argh(switch)]
pub(crate) list_animations: bool,
#[argh(positional)]
pub(crate) input: PathBuf,
#[argh(positional)]
pub(crate) output: Option<PathBuf>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "inspect")]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct InspectCmd {
#[argh(switch)]
pub(crate) internal_names: bool,
#[argh(option, default = "15")]
pub(crate) max_string_length: usize,
#[argh(switch)]
pub(crate) require_ndb: bool,
#[argh(switch)]
pub(crate) no_ndb: bool,
#[argh(switch)]
pub(crate) no_source_weave: bool,
#[argh(switch)]
pub(crate) no_local_offsets: bool,
#[argh(switch)]
pub(crate) no_labels: bool,
#[argh(switch)]
pub(crate) no_offsets: bool,
#[argh(switch)]
pub(crate) no_langspec: bool,
#[argh(option)]
pub(crate) langspec: Option<PathBuf>,
#[argh(positional)]
pub(crate) path: PathBuf,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "unpack")]
pub(crate) struct UnpackCmd {
#[argh(option, short = 'd', default = "PathBuf::from(\".\")")]
pub(crate) directory: PathBuf,
#[argh(switch, short = 'f')]
pub(crate) force: bool,
#[argh(positional)]
pub(crate) input: PathBuf,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "init")]
pub(crate) struct InitCmd {
#[argh(option)]
pub(crate) kind: Option<String>,
#[argh(positional)]
pub(crate) path: Option<PathBuf>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "new")]
pub(crate) struct NewCmd {
#[argh(option)]
pub(crate) kind: Option<String>,
#[argh(positional)]
pub(crate) path: PathBuf,
}
#[derive(FromArgs, Clone)]
#[argh(subcommand, name = "pack")]
pub(crate) struct PackCmd {
#[argh(switch, short = 'f')]
pub(crate) force: bool,
#[argh(switch)]
pub(crate) debug: bool,
#[argh(switch)]
pub(crate) no_entrypoint_check: bool,
#[argh(option)]
pub(crate) langspec: Option<PathBuf>,
#[argh(option)]
pub(crate) include_dir: Vec<PathBuf>,
#[argh(option, default = "String::from(\"O0\")")]
pub(crate) optimization: String,
#[argh(option, short = 'j')]
pub(crate) jobs: Option<usize>,
#[argh(option, default = "String::from(\"V1\")")]
pub(crate) data_version: String,
#[argh(option, default = "String::from(\"none\")")]
pub(crate) data_compression: String,
#[argh(switch)]
pub(crate) no_squash: bool,
#[argh(switch)]
pub(crate) no_symlinks: bool,
#[argh(option, short = 'e')]
pub(crate) erf_type: Option<String>,
#[argh(option)]
pub(crate) root: Option<PathBuf>,
#[argh(option)]
pub(crate) language: Option<String>,
#[argh(positional)]
pub(crate) paths: Vec<PathBuf>,
}
pub(crate) struct KeyPackCmd {
pub(crate) data_version: String,
pub(crate) data_compression: String,
pub(crate) no_squash: bool,
pub(crate) no_symlinks: bool,
pub(crate) force: bool,
pub(crate) debug: bool,
pub(crate) no_entrypoint_check: bool,
pub(crate) langspec: Option<PathBuf>,
pub(crate) include_dir: Vec<PathBuf>,
pub(crate) optimization: String,
pub(crate) jobs: Option<usize>,
pub(crate) key: String,
pub(crate) source: PathBuf,
pub(crate) destination: PathBuf,
}
pub(crate) struct KeyUnpackCmd {
pub(crate) force: bool,
pub(crate) key: PathBuf,
pub(crate) destination: PathBuf,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "nwsync")]
pub(crate) struct NwsyncCmd {
#[argh(subcommand)]
pub(crate) command: NwsyncCommand,
}
#[derive(FromArgs)]
#[argh(subcommand)]
pub(crate) enum NwsyncCommand {
Print(NwsyncPrintCmd),
Fetch(NwsyncFetchCmd),
Prune(NwsyncPruneCmd),
Write(NwsyncWriteCmd),
}
#[derive(FromArgs)]
#[argh(subcommand, name = "print")]
pub(crate) struct NwsyncPrintCmd {
#[argh(option)]
pub(crate) manifest: Option<String>,
#[argh(positional)]
pub(crate) input: PathBuf,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "fetch")]
pub(crate) struct NwsyncFetchCmd {
#[argh(positional)]
pub(crate) url: String,
#[argh(option, short = 'o')]
pub(crate) output: Option<PathBuf>,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "prune")]
pub(crate) struct NwsyncPruneCmd {
#[argh(positional)]
pub(crate) repository: PathBuf,
#[argh(switch)]
pub(crate) dry_run: bool,
}
#[derive(FromArgs)]
#[argh(subcommand, name = "write")]
pub(crate) struct NwsyncWriteCmd {
#[argh(positional)]
pub(crate) input: PathBuf,
#[argh(positional)]
pub(crate) output: PathBuf,
#[argh(switch, short = 'f')]
pub(crate) force: bool,
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use argh::FromArgs;
use super::{Cli, Command, InspectCmd, NwsyncCommand};
#[test]
fn parses_pack_command_with_repeated_include_dirs() {
let cli = Cli::from_args(
&["nwnrs"],
&[
"pack",
"--debug",
"--no-entrypoint-check",
"--include-dir",
"inc/a",
"--include-dir",
"inc/b",
"--optimization",
"O2",
"-j",
"4",
"scripts/test.nss",
"out/test.ncs",
],
)
.unwrap_or_else(|error| panic!("parse pack args: {error:?}"));
let Command::Pack(cmd) = cli.command else {
panic!("expected pack command");
};
assert!(cmd.debug);
assert!(cmd.no_entrypoint_check);
assert_eq!(cmd.optimization, "O2");
assert_eq!(cmd.jobs, Some(4));
assert_eq!(
cmd.include_dir,
vec![PathBuf::from("inc/a"), PathBuf::from("inc/b")]
);
assert_eq!(
cmd.paths,
vec![
PathBuf::from("scripts/test.nss"),
PathBuf::from("out/test.ncs")
]
);
}
#[test]
fn parses_init_command_with_kind_and_path() {
let cli = Cli::from_args(&["nwnrs"], &["init", "--kind", "utc", "project"])
.unwrap_or_else(|error| panic!("parse init args: {error:?}"));
let Command::Init(cmd) = cli.command else {
panic!("expected init command");
};
assert_eq!(cmd.kind.as_deref(), Some("utc"));
assert_eq!(cmd.path, Some(PathBuf::from("project")));
}
#[test]
fn parses_new_command_with_kind_and_path() {
let cli = Cli::from_args(&["nwnrs"], &["new", "--kind", "mod", "my_mod"])
.unwrap_or_else(|error| panic!("parse new args: {error:?}"));
let Command::New(cmd) = cli.command else {
panic!("expected new command");
};
assert_eq!(cmd.kind.as_deref(), Some("mod"));
assert_eq!(cmd.path, PathBuf::from("my_mod"));
}
#[test]
fn parses_nwsync_fetch_command() {
let cli = Cli::from_args(
&["nwnrs"],
&[
"nwsync",
"fetch",
"https://example.invalid/manifest/abcd",
"-o",
"repo",
],
)
.unwrap_or_else(|error| panic!("parse nwsync args: {error:?}"));
let Command::Nwsync(cmd) = cli.command else {
panic!("expected nwsync command");
};
let NwsyncCommand::Fetch(cmd) = cmd.command else {
panic!("expected fetch subcommand");
};
assert_eq!(cmd.url, "https://example.invalid/manifest/abcd");
assert_eq!(cmd.output, Some(PathBuf::from("repo")));
}
#[test]
fn parses_convert_command_for_mdl_paths() {
let cli = Cli::from_args(
&["nwnrs"],
&["convert", "-f", "models/input.mdl", "models/output.mdl"],
)
.unwrap_or_else(|error| panic!("parse convert args: {error:?}"));
let Command::Convert(cmd) = cli.command else {
panic!("expected convert command");
};
assert!(cmd.force);
assert_eq!(cmd.root, None);
assert_eq!(cmd.user, None);
assert_eq!(cmd.language, "english");
assert!(!cmd.load_ovr);
assert_eq!(cmd.animation, None);
assert_eq!(cmd.time, None);
assert!(!cmd.list_animations);
assert_eq!(cmd.input, PathBuf::from("models/input.mdl"));
assert_eq!(cmd.output, Some(PathBuf::from("models/output.mdl")));
}
#[test]
fn parses_convert_command_for_animation_listing_without_output() {
let cli = Cli::from_args(
&["nwnrs"],
&[
"convert",
"--list-animations",
"--animation",
"default",
"--time",
"0.0",
"models/input.mdl",
],
)
.unwrap_or_else(|error| panic!("parse convert args: {error:?}"));
let Command::Convert(cmd) = cli.command else {
panic!("expected convert command");
};
assert!(cmd.list_animations);
assert_eq!(cmd.animation.as_deref(), Some("default"));
assert_eq!(cmd.time, Some(0.0));
assert_eq!(cmd.input, PathBuf::from("models/input.mdl"));
assert_eq!(cmd.output, None);
}
#[test]
fn parses_inspect_command_with_ncs_disassembly_options() {
let cli = Cli::from_args(
&["nwnrs"],
&[
"inspect",
"--internal-names",
"--max-string-length",
"42",
"--require-ndb",
"--no-source-weave",
"--no-local-offsets",
"--no-labels",
"--no-offsets",
"--no-langspec",
"--langspec",
"specs/custom.nss",
"scripts/test.ncs",
],
)
.unwrap_or_else(|error| panic!("parse inspect args: {error:?}"));
let Command::Inspect(InspectCmd {
internal_names,
max_string_length,
require_ndb,
no_ndb,
no_source_weave,
no_local_offsets,
no_labels,
no_offsets,
no_langspec,
langspec,
path,
}) = cli.command
else {
panic!("expected inspect command");
};
assert!(internal_names);
assert_eq!(max_string_length, 42);
assert!(require_ndb);
assert!(!no_ndb);
assert!(no_source_weave);
assert!(no_local_offsets);
assert!(no_labels);
assert!(no_offsets);
assert!(no_langspec);
assert_eq!(langspec, Some(PathBuf::from("specs/custom.nss")));
assert_eq!(path, PathBuf::from("scripts/test.ncs"));
}
#[test]
fn parses_pack_command_for_generic_packing() {
let cli = Cli::from_args(
&["nwnrs"],
&["pack", "--data-version", "E1", "input", "output.key"],
)
.unwrap_or_else(|error| panic!("parse pack args: {error:?}"));
let Command::Pack(cmd) = cli.command else {
panic!("expected pack command");
};
assert_eq!(cmd.data_version, "E1");
assert_eq!(
cmd.paths,
vec![PathBuf::from("input"), PathBuf::from("output.key")]
);
assert_eq!(cmd.root, None);
assert_eq!(cmd.language, None);
}
#[test]
fn parses_pack_command_for_install_packaging() {
let cli = Cli::from_args(
&["nwnrs"],
&[
"pack",
"-f",
"--root",
"/srv/nwn",
"--language",
"en",
"--data-version",
"E1",
"--data-compression",
"zstd",
"custom_base.key",
"docker/data/data",
],
)
.unwrap_or_else(|error| panic!("parse pack args: {error:?}"));
let Command::Pack(cmd) = cli.command else {
panic!("expected pack command");
};
assert!(cmd.force);
assert_eq!(cmd.root, Some(PathBuf::from("/srv/nwn")));
assert_eq!(cmd.language, Some("en".to_string()));
assert_eq!(cmd.data_version, "E1");
assert_eq!(cmd.data_compression, "zstd");
assert_eq!(
cmd.paths,
vec![
PathBuf::from("custom_base.key"),
PathBuf::from("docker/data/data")
]
);
}
}