use std::ffi::OsStr;
use std::fmt::{Debug, Display, Formatter};
use std::fs::File;
use std::io::{BufReader, Write};
use std::path::{Path, PathBuf};
use anyhow::Context;
use clap::{ValueEnum, Parser, Subcommand};
use tracing_subscriber::filter::LevelFilter;
use serde::{Serialize, Serializer};
use libre_pvz_resources::animation as packed;
use libre_pvz_resources::model;
use crate::reanim::Animation;
use crate::xml::Xml as XmlWrapper;
pub enum MaybePacked {
Plain(Animation),
Packed(packed::AnimDesc),
}
use MaybePacked::*;
impl MaybePacked {
pub fn is_packed(&self) -> bool { matches!(self, Packed(_)) }
pub fn into_packed(self, packed: bool) -> anyhow::Result<MaybePacked> {
Ok(match self {
Packed(_) if !packed => anyhow::bail!("unpacking not supported"),
Plain(anim) if packed => Packed(anim.into()),
_ => self,
})
}
}
impl Debug for MaybePacked {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Plain(anim) => anim.fmt(f),
Packed(anim) => anim.fmt(f),
}
}
}
impl Serialize for MaybePacked {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Plain(anim) => anim.serialize(serializer),
Packed(anim) => anim.serialize(serializer),
}
}
}
#[derive(Debug, Parser)]
#[clap(author, version, about)]
pub struct Cli {
#[clap(long, value_enum, global = true)]
pub verbose: Option<Option<Verbosity>>,
#[clap(subcommand)]
pub commands: Commands,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
#[allow(missing_docs)]
pub enum Verbosity { Off, Error, Warn, Info, Debug, Trace }
impl From<Verbosity> for LevelFilter {
fn from(verb: Verbosity) -> Self {
match verb {
Verbosity::Off => LevelFilter::OFF,
Verbosity::Error => LevelFilter::ERROR,
Verbosity::Warn => LevelFilter::WARN,
Verbosity::Info => LevelFilter::INFO,
Verbosity::Debug => LevelFilter::DEBUG,
Verbosity::Trace => LevelFilter::TRACE,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, ValueEnum)]
pub enum Format {
Internal,
Compiled,
Bin,
Xml,
Json,
Yaml,
}
use Format::*;
use libre_pvz_resources::dynamic::DynamicRegistry;
impl Display for Format {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Internal => "internal",
Compiled => "compiled",
Bin => "bin",
Xml => "xml",
Json => "json",
Yaml => "yaml",
})
}
}
impl Format {
pub fn infer_packed<P: AsRef<Path>>(path: P) -> bool {
let file = path.as_ref();
let ext = file.extension().and_then(OsStr::to_str);
let stem = file.file_stem().and_then(OsStr::to_str);
ext == Some("bin") || stem.map_or(false, |s| s.ends_with(".anim"))
}
pub fn infer<P: AsRef<Path>>(path: P) -> Option<Format> {
match path.as_ref().extension()?.to_str()? {
"txt" => Some(Internal),
"compiled" => Some(Compiled),
"bin" => Some(Bin),
"reanim" | "xml" => Some(Xml),
"json" => Some(Json),
"yaml" | "yml" => Some(Yaml),
_ => None,
}
}
pub fn decide<P: AsRef<Path>>(spec: Option<Format>, path: Option<P>, default: Format) -> Format {
spec.or_else(|| path.and_then(|p| Format::infer(p.as_ref()))).unwrap_or(default)
}
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Model {
input: PathBuf,
#[clap(short = 'I', long, value_enum)]
input_format: Option<Format>,
#[clap(short, long)]
output: Option<PathBuf>,
#[clap(short = 'O', long, value_enum)]
output_format: Option<Format>,
},
Anim {
input: PathBuf,
#[clap(short = 'I', long, value_enum)]
input_format: Option<Format>,
#[clap(long)]
pack_input: bool,
#[clap(short, long)]
output: Option<PathBuf>,
#[clap(short = 'O', long, value_enum)]
output_format: Option<Format>,
#[clap(long)]
pack_output: bool,
},
}
fn setup_logger(verbose: LevelFilter) {
tracing_subscriber::fmt::Subscriber::builder()
.with_target(true)
.without_time()
.with_max_level(verbose)
.init()
}
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
impl Cli {
pub fn run() -> anyhow::Result<()> {
let args = Cli::parse();
setup_logger(match args.verbose {
None => LevelFilter::WARN, Some(None) => LevelFilter::INFO, Some(Some(verbose)) => verbose.into(), });
DynamicRegistry::initialize_without_bevy();
match args.commands {
Commands::Model {
input, input_format,
output_format, output,
} => {
let input_format = Format::decide(input_format, Some(&input), Bin);
let input = File::open(&input).with_context(|| format!("failed to read file {input:?}"))?;
let mut input = BufReader::new(input);
let model: model::Model = match input_format {
Internal | Compiled | Xml => anyhow::bail!("unsupported input format: {input_format}"),
Bin => bincode::decode_from_std_read(&mut input, BINCODE_CONFIG)?,
Json => serde_json::from_reader(&mut input)?,
Yaml => serde_yaml::from_reader(&mut input)?,
};
let output_format = Format::decide(output_format, output.as_ref(), Internal);
if let Some(output) = output {
let context = || format!("failed to open output file {output:?}");
let output = File::create(&output).with_context(context)?;
encode_model(model, output_format, output)?;
} else {
encode_model(model, output_format, std::io::stdout().lock())?;
}
}
Commands::Anim {
input, input_format, mut pack_input,
output_format, output, mut pack_output,
} => {
pack_input |= Format::infer_packed(&input);
let input_format = Format::decide(input_format, Some(&input), Compiled);
let input = File::open(&input).with_context(|| format!("failed to read file {input:?}"))?;
let mut input = BufReader::new(input);
let anim = match input_format {
Internal | Xml => anyhow::bail!("unsupported input format: {input_format}"),
Bin => Packed(bincode::decode_from_std_read(&mut input, BINCODE_CONFIG)?),
Compiled => Plain(Animation::decompress_and_decode(&mut input)?),
Json if pack_input => Packed(serde_json::from_reader(&mut input)?),
Yaml if pack_input => Packed(serde_yaml::from_reader(&mut input)?),
Json => Plain(serde_json::from_reader(&mut input)?),
Yaml => Plain(serde_yaml::from_reader(&mut input)?),
};
pack_output |= output.as_ref().map_or(anim.is_packed(), Format::infer_packed);
let output_format = Format::decide(
output_format, output.as_ref(),
if pack_output { Internal } else { Xml },
);
let anim = anim.into_packed(pack_output)?;
if let Some(output) = output {
let context = || format!("failed to open output file {output:?}");
let output = File::create(&output).with_context(context)?;
encode_anim(anim, output_format, output)?;
} else {
encode_anim(anim, output_format, std::io::stdout().lock())?;
}
}
}
Ok(())
}
}
pub fn encode_anim(anim: MaybePacked, format: Format, mut output: impl Write) -> anyhow::Result<()> {
match (format, anim) {
(Internal, anim) => writeln!(output, "{anim:#?}")?,
(Bin, Packed(anim)) => {
bincode::encode_into_std_write(anim, &mut output, BINCODE_CONFIG)?;
}
(Xml, Plain(anim)) => write!(output, "{}", XmlWrapper(anim))?,
(Json, anim) => serde_json::to_writer_pretty(output, &anim)?,
(Yaml, anim) => serde_yaml::to_writer(output, &anim)?,
(format, anim) => {
anyhow::bail!("format '{format}' does not support 'packed={}'", anim.is_packed());
}
}
Ok(())
}
pub fn encode_model(model: model::Model, format: Format, mut output: impl Write) -> anyhow::Result<()> {
match format {
Compiled | Xml => anyhow::bail!("unsupported output format: '{format}'"),
Internal => writeln!(output, "{model:#?}")?,
Bin => { bincode::encode_into_std_write(&model, &mut output, BINCODE_CONFIG)?; }
Json => serde_json::to_writer_pretty(output, &model)?,
Yaml => serde_yaml::to_writer(output, &model)?,
}
Ok(())
}