diff --git a/Cargo.lock b/Cargo.lock index 2650e33..40753d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,12 +179,21 @@ dependencies = [ "csv", "encoding_rs", "lazy_static", + "msg_tool_macro", "serde", "serde_json", "unicode-segmentation", "windows-sys", ] +[[package]] +name = "msg_tool_macro" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "once_cell" version = "1.21.3" diff --git a/Cargo.toml b/Cargo.toml index cb8d90b..d2bf1fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.5", features = ["derive"] } csv = "1.3" encoding_rs = "0.8" lazy_static = "1.5.0" +msg_tool_macro = { path = "./msg_tool_macro" } serde = { version = "1", features = ["derive"] } serde_json = "1" unicode-segmentation = "1.12" diff --git a/msg_tool_macro/.gitignore b/msg_tool_macro/.gitignore new file mode 100644 index 0000000..03314f7 --- /dev/null +++ b/msg_tool_macro/.gitignore @@ -0,0 +1 @@ +Cargo.lock diff --git a/msg_tool_macro/Cargo.toml b/msg_tool_macro/Cargo.toml new file mode 100644 index 0000000..d37332e --- /dev/null +++ b/msg_tool_macro/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "msg_tool_macro" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" diff --git a/msg_tool_macro/src/lib.rs b/msg_tool_macro/src/lib.rs new file mode 100644 index 0000000..3cc9ddf --- /dev/null +++ b/msg_tool_macro/src/lib.rs @@ -0,0 +1,307 @@ +use proc_macro::TokenStream; +use syn::parse::discouraged::Speculative; +use syn::spanned::Spanned; + +enum PackStruct { + Enum(syn::ItemEnum), + Struct(syn::ItemStruct), +} + +impl syn::parse::Parse for PackStruct { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let fork = input.fork(); + + // Try to parse as a struct first + if let Ok(struct_item) = fork.parse::() { + // If successful, advance the original input stream and return + input.advance_to(&fork); + return Ok(PackStruct::Struct(struct_item)); + } + + // Try to parse as an enum + if let Ok(enum_item) = input.parse::() { + return Ok(PackStruct::Enum(enum_item)); + } + + // If neither worked, create a helpful error + Err(input.error("expected struct or enum")) + } +} + +#[proc_macro] +pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream { + let i = syn::parse_macro_input!(item as syn::Ident); + let output = quote::quote! { + impl StructUnpack for #i { + fn unpack(mut reader: R, big: bool, _encoding: Encoding) -> Result { + let mut buf = [0u8; std::mem::size_of::<#i>()]; + reader.read_exact(&mut buf)?; + Ok(if big { + #i::from_be_bytes(buf) + } else { + #i::from_le_bytes(buf) + }) + } + } + + impl StructPack for #i { + fn pack(&self, writer: &mut W, big: bool, _encoding: Encoding) -> Result<()> { + let bytes = if big { + self.to_be_bytes() + } else { + self.to_le_bytes() + }; + writer.write_all(&bytes)?; + Ok(()) + } + } + }; + output.into() +} + +/// Macro to derive `StructPack` trait for structs. +/// +/// * `skip_pack` attribute can be used to skip fields from packing. +/// * `fstring = ` attribute can be used to specify a fixed string length for String fields. +#[proc_macro_derive(StructPack, attributes(skip_pack, fstring))] +pub fn struct_pack_derive(input: TokenStream) -> TokenStream { + let a = syn::parse_macro_input!(input as PackStruct); + match a { + PackStruct::Struct(sut) => { + let name = sut.ident; + let mut ind = 0; + let fields = sut.fields.iter().map(|field| { + let mut skipped = false; + let mut fixed_string: Option = None; + for attr in &field.attrs { + let path = attr.path(); + if path.is_ident("skip_pack") { + skipped = true; + } else if path.is_ident("fstring") { + if let syn::Meta::NameValue(nv) = &attr.meta { + if let syn::Expr::Lit(lit) = &nv.value { + if let syn::Lit::Int(s) = &lit.lit { + fixed_string = Some(s.base10_parse().unwrap()); + } + } + } + } + } + if skipped { + return quote::quote! {}; + } + let field_name = match &field.ident { + Some(ident) => quote::quote! { #ident }, + None => { + let idx = syn::Index::from(ind); + ind += 1; + quote::quote! { #idx } + }, + }; + let field_type = &field.ty; + if let syn::Type::Path(type_path) = field_type { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "String" { + if let Some(fixed_string) = fixed_string { + return quote::quote! { + let s = encode_string(encoding, &self.#field_name, true)?; + let slen = s.len(); + if slen > #fixed_string { + return Err(anyhow::anyhow!("String length was too long for field '{}'", stringify!(#field_name))); + } + writer.write_all(&s)?; + for _ in slen..#fixed_string { + writer.write_all(&[0])?; + } + }; + } + } + } + } + quote::quote! { + self.#field_name.pack(writer, big, encoding)?; + } + }); + let output = quote::quote! { + impl StructPack for #name { + fn pack(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()> { + #(#fields)* + Ok(()) + } + } + }; + output.into() + } + PackStruct::Enum(item) => { + let ident = item.ident; + let variants = item.variants.iter().map(|variant| { + let mut skipped = false; + for attr in &variant.attrs { + let path = attr.path(); + if path.is_ident("skip_pack") { + skipped = true; + } + } + if skipped { + return quote::quote! {}; + } + let variant_name = &variant.ident; + let mut idents = Vec::new(); + let mut is_struct_like = true; + let fields: Vec<_> = variant.fields.iter().enumerate().map(|(idx, field)| { + let mut skipped = false; + let mut fixed_string: Option = None; + for attr in &field.attrs { + let path = attr.path(); + if path.is_ident("skip_pack") { + skipped = true; + } else if path.is_ident("fstring") { + if let syn::Meta::NameValue(nv) = &attr.meta { + if let syn::Expr::Lit(lit) = &nv.value { + if let syn::Lit::Int(s) = &lit.lit { + fixed_string = Some(s.base10_parse().unwrap()); + } + } + } + } + } + if skipped { + return quote::quote! {}; + } + let field_name = match &field.ident { + Some(ident) => quote::quote! { #ident }, + None => { + is_struct_like = false; + let idx = syn::Ident::new(&format!("index_{}", idx), field.span()); + quote::quote! { #idx } + }, + }; + idents.push(field_name.clone()); + let field_type = &field.ty; + if let syn::Type::Path(type_path) = field_type { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "String" { + if let Some(fixed_string) = fixed_string { + return quote::quote! { + let s = encode_string(encoding, &#field_name, true)?; + let slen = s.len(); + if slen > #fixed_string { + return Err(anyhow::anyhow!("String length was too long for field '{}'", stringify!(#field_name))); + } + writer.write_all(&s)?; + for _ in slen..#fixed_string { + writer.write_all(&[0])?; + } + }; + } + } + } + } + quote::quote! { + #field_name.pack(writer, big, encoding)?; + } + }).collect(); + let idents = if is_struct_like { + quote::quote! { { #(#idents),* } } + } else { + quote::quote! { (#(#idents),*) } + }; + quote::quote! { + #ident::#variant_name #idents => { + #(#fields)* + } + } + }); + let output = quote::quote! { + impl StructPack for #ident { + fn pack(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()> { + match self { + #(#variants)* + } + Ok(()) + } + } + }; + output.into() + } + } +} + +/// Macro to derive `StructUnpack` trait for structs. +/// * `skip_unpack` attribute can be used to skip fields from unpacking. +/// * `fstring = ` attribute can be used to specify a fixed string length for String fields. +/// * `fstring_no_trim` attribute can be used to disable trimming of fixed strings. +#[proc_macro_derive(StructUnpack, attributes(skip_unpack, fstring, fstring_no_trim))] +pub fn struct_unpack_derive(input: TokenStream) -> TokenStream { + let sut = syn::parse_macro_input!(input as syn::ItemStruct); + let name = sut.ident; + let mut fields = Vec::new(); + let mut is_tuple_struct = false; + let mut ind = 0; + let smts: Vec<_> = sut.fields.iter().map(|field| { + let mut skipped = false; + let mut fixed_string: Option = None; + let mut fstring_no_trim = false; + for attr in &field.attrs { + let path = attr.path(); + if path.is_ident("skip_unpack") { + skipped = true; + } else if path.is_ident("fstring") { + if let syn::Meta::NameValue(nv) = &attr.meta { + if let syn::Expr::Lit(lit) = &nv.value { + if let syn::Lit::Int(s) = &lit.lit { + fixed_string = Some(s.base10_parse().unwrap()); + } + } + } + } else if path.is_ident("fstring_no_trim") { + fstring_no_trim = true; + } + } + let field_name = match &field.ident { + Some(ident) => quote::quote! { #ident }, + None => { + is_tuple_struct = true; + let idx = syn::Ident::new(&format!("index_{}", ind), field.span()); + ind += 1; + quote::quote! { #idx } + }, + }; + fields.push(field_name.clone()); + if skipped { + return quote::quote! { + let #field_name = Default::default(); + }; + } + let field_type = &field.ty; + if let syn::Type::Path(type_path) = field_type { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "String" { + if let Some(fixed_string) = fixed_string { + let trim = syn::LitBool::new(!fstring_no_trim, field.span()); + return quote::quote! { + let #field_name = reader.read_fstring(#fixed_string, encoding, #trim)?; + }; + } + } + } + } + quote::quote! { + let #field_name = #field_type::unpack(&mut reader, big, encoding)?; + } + }).collect(); + let fields = if is_tuple_struct { + quote::quote! ((#(#fields),*)) + } else { + quote::quote! { { #(#fields),* } } + }; + let output = quote::quote! { + impl StructUnpack for #name { + fn unpack(mut reader: R, big: bool, encoding: Encoding) -> Result { + #(#smts)* + Ok(Self #fields) + } + } + }; + output.into() +} diff --git a/src/ext/io.rs b/src/ext/io.rs index ecaf7da..da11865 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1,3 +1,5 @@ +use crate::utils::encoding::decode_to_string; +use crate::{types::Encoding, utils::struct_pack::StructUnpack}; use std::{ffi::CString, io::*}; pub trait Peek { @@ -202,6 +204,20 @@ pub trait Peek { fn peek_cstring(&mut self) -> Result; fn peek_cstring_at(&mut self, offset: usize) -> Result; + + fn read_struct(&mut self, big: bool, encoding: Encoding) -> Result; + fn read_struct_vec( + &mut self, + count: usize, + big: bool, + encoding: Encoding, + ) -> Result> { + let mut vec = Vec::with_capacity(count); + for _ in 0..count { + vec.push(self.read_struct(big, encoding)?); + } + Ok(vec) + } } impl Peek for T { @@ -265,6 +281,11 @@ impl Peek for T { self.seek(SeekFrom::Start(current_pos))?; CString::new(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } + + fn read_struct(&mut self, big: bool, encoding: Encoding) -> Result { + S::unpack(self, big, encoding) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + } } pub trait ReadExt { @@ -288,6 +309,9 @@ pub trait ReadExt { fn read_i128_be(&mut self) -> Result; fn read_cstring(&mut self) -> Result; + fn read_fstring(&mut self, len: usize, encoding: Encoding, trim: bool) -> Result; + + fn read_exact_vec(&mut self, len: usize) -> Result>; } impl ReadExt for T { @@ -394,6 +418,25 @@ impl ReadExt for T { } CString::new(buf).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } + fn read_fstring(&mut self, len: usize, encoding: Encoding, trim: bool) -> Result { + let mut buf = vec![0u8; len]; + self.read_exact(&mut buf)?; + if trim { + let first_zero = buf.iter().position(|&b| b == 0); + if let Some(pos) = first_zero { + buf.truncate(pos); + } + } + let s = decode_to_string(encoding, &buf) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; + Ok(s) + } + + fn read_exact_vec(&mut self, len: usize) -> Result> { + let mut buf = vec![0u8; len]; + self.read_exact(&mut buf)?; + Ok(buf) + } } pub trait WriteExt { diff --git a/src/main.rs b/src/main.rs index a3e5a37..9ce5a54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -233,7 +233,13 @@ pub fn parse_script_from_archive( let encoding = get_encoding(arg, builder); let archive_encoding = get_archived_encoding(arg, builder, encoding); return Ok(( - builder.build_script(file.data().to_vec(), encoding, archive_encoding, config)?, + builder.build_script( + file.data().to_vec(), + file.name(), + encoding, + archive_encoding, + config, + )?, builder, )); } @@ -258,7 +264,13 @@ pub fn parse_script_from_archive( let encoding = get_encoding(arg, builder); let archive_encoding = get_archived_encoding(arg, builder, encoding); return Ok(( - builder.build_script(file.data().to_vec(), encoding, archive_encoding, config)?, + builder.build_script( + file.data().to_vec(), + file.name(), + encoding, + archive_encoding, + config, + )?, builder, )); } @@ -285,18 +297,12 @@ pub fn export_script( if script.is_archive() { let odir = match output.as_ref() { Some(output) => { - if is_dir { - let mut pb = std::path::PathBuf::from(output); - let filename = std::path::PathBuf::from(filename); - if let Some(fname) = filename.file_name() { - pb.push(fname); - } - pb.to_string_lossy().into_owned() - } else { - return Err(anyhow::anyhow!( - "A directory is required for archive export" - )); + let mut pb = std::path::PathBuf::from(output); + let filename = std::path::PathBuf::from(filename); + if let Some(fname) = filename.file_name() { + pb.push(fname); } + pb.to_string_lossy().into_owned() } None => { let mut pb = std::path::PathBuf::from(filename); @@ -311,28 +317,51 @@ pub fn export_script( let f = f?; if f.is_script() { let (script_file, _) = parse_script_from_archive(&f, arg, config)?; - let mes = match script_file.extract_messages() { - Ok(mes) => mes, - Err(e) => { - eprintln!("Error extracting messages from {}: {}", f.name(), e); - COUNTER.inc_error(); - if arg.backtrace { - eprintln!("Backtrace: {}", e.backtrace()); + let mut of = match &arg.output_type { + Some(t) => t.clone(), + None => script_file.default_output_script_type(), + }; + if !script_file.is_output_supported(of) { + of = script_file.default_output_script_type(); + } + let mes = if of.is_custom() { + Vec::new() + } else { + match script_file.extract_messages() { + Ok(mes) => mes, + Err(e) => { + eprintln!("Error extracting messages from {}: {}", f.name(), e); + COUNTER.inc_error(); + if arg.backtrace { + eprintln!("Backtrace: {}", e.backtrace()); + } + continue; } - continue; } }; - if mes.is_empty() { + if !of.is_custom() && mes.is_empty() { eprintln!("No messages found in {}", f.name()); COUNTER.inc(types::ScriptResult::Ignored); continue; } - let of = match &arg.output_type { - Some(t) => t.clone(), - None => script_file.default_output_script_type(), - }; let mut out_path = std::path::PathBuf::from(&odir).join(f.name()); - out_path.set_extension(of.as_ref()); + out_path.set_extension(if of.is_custom() { + script_file.custom_output_extension() + } else { + of.as_ref() + }); + match utils::files::make_sure_dir_exists(&out_path) { + Ok(_) => {} + Err(e) => { + eprintln!( + "Error creating parent directory for {}: {}", + out_path.display(), + e + ); + COUNTER.inc_error(); + continue; + } + } match of { types::OutputScriptType::Json => { let enc = get_output_encoding(arg); @@ -397,9 +426,29 @@ pub fn export_script( } } } + types::OutputScriptType::Custom => { + let enc = get_output_encoding(arg); + if let Err(e) = script_file.custom_export(&out_path, enc) { + eprintln!("Error exporting custom script: {}", e); + COUNTER.inc_error(); + continue; + } + } } } else { let out_path = std::path::PathBuf::from(&odir).join(f.name()); + match utils::files::make_sure_dir_exists(&out_path) { + Ok(_) => {} + Err(e) => { + eprintln!( + "Error creating parent directory for {}: {}", + out_path.display(), + e + ); + COUNTER.inc_error(); + continue; + } + } match utils::files::write_file(&out_path) { Ok(mut fi) => match fi.write_all(f.data()) { Ok(_) => {} @@ -420,18 +469,26 @@ pub fn export_script( } return Ok(types::ScriptResult::Ok); } - // println!("{:?}", script); - let mes = script.extract_messages()?; - // for m in mes.iter() { - // println!("{:?}", m); - // } - if mes.is_empty() { + let mut of = match &arg.output_type { + Some(t) => t.clone(), + None => script.default_output_script_type(), + }; + if !script.is_output_supported(of) { + of = script.default_output_script_type(); + } + let mes = if of.is_custom() { + Vec::new() + } else { + script.extract_messages()? + }; + if !of.is_custom() && mes.is_empty() { eprintln!("No messages found"); return Ok(types::ScriptResult::Ignored); } - let of = match &arg.output_type { - Some(t) => t.clone(), - None => script.default_output_script_type(), + let ext = if of.is_custom() { + script.custom_output_extension() + } else { + of.as_ref() }; let f = if filename == "-" { String::from("-") @@ -444,7 +501,7 @@ pub fn export_script( if let Some(fname) = f.file_name() { pb.push(fname); } - pb.set_extension(of.as_ref()); + pb.set_extension(ext); pb.to_string_lossy().into_owned() } else { output.clone() @@ -452,7 +509,7 @@ pub fn export_script( } None => { let mut pb = std::path::PathBuf::from(filename); - pb.set_extension(of.as_ref()); + pb.set_extension(ext); pb.to_string_lossy().into_owned() } } @@ -472,6 +529,11 @@ pub fn export_script( let mut f = utils::files::write_file(&f)?; f.write_all(&b)?; } + types::OutputScriptType::Custom => { + let enc = get_output_encoding(arg); + println!("f: {}", f); + script.custom_export(f.as_ref(), enc)?; + } } Ok(types::ScriptResult::Ok) } @@ -520,6 +582,10 @@ pub fn import_script( let mut parser = output_scripts::m3t::M3tParser::new(&s); parser.parse()? } + _ => { + eprintln!("Unsupported output script type for import: {:?}", of); + return Ok(types::ScriptResult::Ignored); + } }; if mes.is_empty() { eprintln!("No messages found"); diff --git a/src/scripts/base.rs b/src/scripts/base.rs index fa68249..f2dc865 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -1,5 +1,10 @@ use crate::types::*; use anyhow::Result; +use std::io::{Read, Seek}; + +pub trait ReadSeek: Read + Seek + std::fmt::Debug {} + +impl ReadSeek for T {} pub trait ScriptBuilder: std::fmt::Debug { fn default_encoding(&self) -> Encoding; @@ -15,6 +20,7 @@ pub trait ScriptBuilder: std::fmt::Debug { fn build_script( &self, buf: Vec, + filename: &str, encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, @@ -28,7 +34,22 @@ pub trait ScriptBuilder: std::fmt::Debug { config: &ExtraConfig, ) -> Result> { let data = crate::utils::files::read_file(filename)?; - self.build_script(data, encoding, archive_encoding, config) + self.build_script(data, filename, encoding, archive_encoding, config) + } + + fn build_script_from_reader( + &self, + mut reader: Box, + filename: &str, + encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + let mut data = Vec::new(); + reader + .read_to_end(&mut data) + .map_err(|e| anyhow::anyhow!("Failed to read from reader: {}", e))?; + self.build_script(data, filename, encoding, archive_encoding, config) } fn extensions(&self) -> &'static [&'static str]; @@ -53,6 +74,14 @@ pub trait ArchiveContent { pub trait Script: std::fmt::Debug { fn default_output_script_type(&self) -> OutputScriptType; + fn is_output_supported(&self, output: OutputScriptType) -> bool { + !matches!(output, OutputScriptType::Custom) + } + + fn custom_output_extension(&self) -> &'static str { + "" + } + fn default_format_type(&self) -> FormatOptions; fn extract_messages(&self) -> Result> { @@ -79,6 +108,12 @@ pub trait Script: std::fmt::Debug { Ok(()) } + fn custom_export(&self, _filename: &std::path::Path, _encoding: Encoding) -> Result<()> { + Err(anyhow::anyhow!( + "This script type does not support custom export." + )) + } + fn is_archive(&self) -> bool { false } diff --git a/src/scripts/bgi/script.rs b/src/scripts/bgi/script.rs index 12dae28..542ff43 100644 --- a/src/scripts/bgi/script.rs +++ b/src/scripts/bgi/script.rs @@ -21,6 +21,7 @@ impl ScriptBuilder for BGIScriptBuilder { fn build_script( &self, buf: Vec, + _filename: &str, encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, diff --git a/src/scripts/circus/script.rs b/src/scripts/circus/script.rs index 7a6d6cf..dbf9cd7 100644 --- a/src/scripts/circus/script.rs +++ b/src/scripts/circus/script.rs @@ -21,6 +21,7 @@ impl ScriptBuilder for CircusMesScriptBuilder { fn build_script( &self, buf: Vec, + _filename: &str, encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, diff --git a/src/scripts/escude/archive.rs b/src/scripts/escude/archive.rs index 2f872a3..af0a74a 100644 --- a/src/scripts/escude/archive.rs +++ b/src/scripts/escude/archive.rs @@ -27,6 +27,7 @@ impl ScriptBuilder for EscudeBinArchiveBuilder { fn build_script( &self, data: Vec, + _filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, @@ -63,6 +64,21 @@ impl ScriptBuilder for EscudeBinArchiveBuilder { } } + fn build_script_from_reader( + &self, + reader: Box, + _filename: &str, + _encoding: Encoding, + archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(EscudeBinArchive::new( + reader, + archive_encoding, + config, + )?)) + } + fn extensions(&self) -> &'static [&'static str] { &["bin"] } @@ -105,7 +121,7 @@ impl ArchiveContent for Entry { } fn is_script(&self) -> bool { - self.data.starts_with(b"ESCR1_00") + self.data.starts_with(b"ESCR1_00") || self.data.starts_with(b"LIST") } } diff --git a/src/scripts/escude/list.rs b/src/scripts/escude/list.rs new file mode 100644 index 0000000..02b5f8b --- /dev/null +++ b/src/scripts/escude/list.rs @@ -0,0 +1,252 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::encode_string; +use crate::utils::struct_pack::*; +use anyhow::Result; +use msg_tool_macro::*; +use serde::{Deserialize, Serialize}; +use std::io::{Read, Seek, Write}; + +#[derive(Debug)] +pub struct EscudeBinListBuilder {} + +impl EscudeBinListBuilder { + pub const fn new() -> Self { + EscudeBinListBuilder {} + } +} + +impl ScriptBuilder for EscudeBinListBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + data: Vec, + filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(EscudeBinList::new( + data, filename, encoding, config, + )?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["bin"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::EscudeList + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len > 4 && buf.starts_with(b"LIST") { + return Some(255); + } + None + } +} + +#[derive(Debug)] +pub struct EscudeBinList { + entries: Vec, +} + +impl EscudeBinList { + pub fn new( + data: Vec, + filename: &str, + encoding: Encoding, + _config: &ExtraConfig, + ) -> Result { + let mut reader = MemReader::new(data); + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if &magic != b"LIST" { + return Err(anyhow::anyhow!("Invalid Escude list file format")); + } + let wsize = reader.read_u32()?; + let mut entries = Vec::new(); + loop { + let current = reader.stream_position()?; + if current as usize >= wsize as usize + 8 { + break; + } + let id = reader.read_u32()?; + let size = reader.read_u32()?; + let data = reader.read_exact_vec(size as usize)?; + entries.push(ListEntry { + id: id, + data: ListData::Unknown(data), + }); + } + let mut s = EscudeBinList { entries }; + match s.try_decode(filename, encoding) { + Ok(_) => {} + Err(e) => { + eprintln!("WARN: Failed to decode Escude list: {}", e); + crate::COUNTER.inc_warning(); + } + } + Ok(s) + } + + pub fn try_decode(&mut self, filename: &str, encoding: Encoding) -> Result<()> { + let filename = std::path::Path::new(filename); + if let Some(filename) = filename.file_name() { + let filename = filename.to_ascii_lowercase(); + if filename == "enum_scr.bin" { + for ent in self.entries.iter_mut() { + let id = ent.id; + if let ListData::Unknown(unk) = &ent.data { + let mut reader = MemReader::new(unk.clone()); + let element_size = if id == 0 { + 132 + } else if id == 1 { + 100 + } else if id == 2 { + 36 + } else if id == 3 { + 104 + } else if id == 9999 { + 1 + } else { + return Err(anyhow::anyhow!("Unknown enum source ID: {}", id)); + }; + let len = unk.len(); + if len % element_size != 0 { + return Err(anyhow::anyhow!( + "Invalid enum source length: {} for ID: {}", + len, + id + )); + } + let count = len / element_size; + let data_entry = match id { + 0 => ListData::Scr(EnumScr::Scripts( + reader.read_struct_vec::(count, false, encoding)?, + )), + 1 => ListData::Scr(EnumScr::Names( + reader.read_struct_vec::(count, false, encoding)?, + )), + 2 => ListData::Scr(EnumScr::Vars( + reader.read_struct_vec::(count, false, encoding)?, + )), + 3 => ListData::Scr(EnumScr::Scenes( + reader.read_struct_vec::(count, false, encoding)?, + )), + 9999 => { + // Special case for unknown enum source ID + ListData::Unknown(unk.clone()) + } + _ => return Err(anyhow::anyhow!("Unknown enum source ID: {}", id)), + }; + ent.data = data_entry; + } + } + } + } + Ok(()) + } +} + +impl Script for EscudeBinList { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Custom + } + + fn is_output_supported(&self, output: OutputScriptType) -> bool { + matches!(output, OutputScriptType::Custom) + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn custom_output_extension(&self) -> &'static str { + "json" + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = serde_json::to_string_pretty(&self.entries) + .map_err(|e| anyhow::anyhow!("Failed to write Escude list to JSON: {}", e))?; + let mut writer = crate::utils::files::write_file(filename)?; + let s = encode_string(encoding, &s, false)?; + writer.write_all(&s)?; + writer.flush()?; + Ok(()) + } +} + +#[derive(Debug, Serialize, Deserialize, StructPack, StructUnpack)] +struct ScriptT { + #[fstring = 64] + /// File name + pub file: String, + pub source: u32, + #[fstring = 64] + pub title: String, +} + +#[derive(Debug, Serialize, Deserialize, StructPack, StructUnpack)] +struct NameT { + #[fstring = 64] + /// Name of the character + pub text: String, + /// Text color + pub color: u32, + #[fstring = 32] + /// Face image file name + pub face: String, +} + +#[derive(Debug, Serialize, Deserialize, StructPack, StructUnpack)] +struct VarT { + /// Variable name + #[fstring = 32] + pub name: String, + /// Variable value + pub value: u16, + /// Variable flag + pub flag: u16, +} + +#[derive(Debug, Serialize, Deserialize, StructPack, StructUnpack)] +struct SceneT { + /// The scene script ID + pub script: u32, + /// The scene name + #[fstring = 64] + pub name: String, + /// The scene thumbail image file name + #[fstring = 32] + pub thumbnail: String, + /// The scene order in the scene (Extra) + pub order: i32, +} + +#[derive(Debug, Serialize, Deserialize, StructPack)] +#[serde(tag = "type", content = "data")] +enum EnumScr { + Scripts(Vec), + Names(Vec), + Vars(Vec), + Scenes(Vec), +} + +#[derive(Debug, Serialize, Deserialize, StructPack)] +#[serde(tag = "type", content = "data")] +enum ListData { + Scr(EnumScr), + Unknown(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +struct ListEntry { + id: u32, + data: ListData, +} diff --git a/src/scripts/escude/mod.rs b/src/scripts/escude/mod.rs index 7820101..40dd4ea 100644 --- a/src/scripts/escude/mod.rs +++ b/src/scripts/escude/mod.rs @@ -1,4 +1,5 @@ pub mod archive; mod crypto; +pub mod list; mod lzw; pub mod script; diff --git a/src/scripts/escude/script.rs b/src/scripts/escude/script.rs index 91f02cc..ae8cbb1 100644 --- a/src/scripts/escude/script.rs +++ b/src/scripts/escude/script.rs @@ -24,6 +24,7 @@ impl ScriptBuilder for EscudeBinScriptBuilder { fn build_script( &self, data: Vec, + _filename: &str, encoding: Encoding, _archive_encoding: Encoding, config: &ExtraConfig, diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 37a7a4b..35209e5 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -18,6 +18,8 @@ lazy_static::lazy_static! { Box::new(escude::archive::EscudeBinArchiveBuilder::new()), #[cfg(feature = "escude")] Box::new(escude::script::EscudeBinScriptBuilder::new()), + #[cfg(feature = "escude")] + Box::new(escude::list::EscudeBinListBuilder::new()), ]; pub static ref ALL_EXTS: Vec = BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect(); diff --git a/src/types.rs b/src/types.rs index a9c587e..52e7970 100644 --- a/src/types.rs +++ b/src/types.rs @@ -60,6 +60,14 @@ pub enum OutputScriptType { M3t, /// JSON which can be used for GalTransl Json, + /// Custom output + Custom, +} + +impl OutputScriptType { + pub fn is_custom(&self) -> bool { + matches!(self, OutputScriptType::Custom) + } } impl AsRef for OutputScriptType { @@ -67,6 +75,7 @@ impl AsRef for OutputScriptType { match self { OutputScriptType::M3t => "m3t", OutputScriptType::Json => "json", + OutputScriptType::Custom => "", } } } @@ -195,6 +204,9 @@ pub enum ScriptType { #[cfg(feature = "escude")] /// Escude bin script Escude, + #[cfg(feature = "escude")] + /// Escude list script + EscudeList, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/utils/files.rs b/src/utils/files.rs index 311cfed..cf69b1d 100644 --- a/src/utils/files.rs +++ b/src/utils/files.rs @@ -65,3 +65,13 @@ pub fn write_file + ?Sized>(f: &F) -> io::Result> Box::new(fs::File::create(f)?) }) } + +pub fn make_sure_dir_exists + ?Sized>(f: &F) -> io::Result<()> { + let path = f.as_ref(); + if let Some(parent) = path.parent() { + if !parent.exists() { + fs::create_dir_all(parent)?; + } + } + Ok(()) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 3946e88..6a3297b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,3 +4,4 @@ pub mod encoding; mod encoding_win; pub mod files; pub mod name_replacement; +pub mod struct_pack; diff --git a/src/utils/struct_pack.rs b/src/utils/struct_pack.rs new file mode 100644 index 0000000..2718ed6 --- /dev/null +++ b/src/utils/struct_pack.rs @@ -0,0 +1,34 @@ +use crate::types::Encoding; +use anyhow::Result; +use msg_tool_macro::struct_unpack_impl_for_num; +use std::io::{Read, Seek, Write}; + +pub trait StructUnpack: Sized { + fn unpack(reader: R, big: bool, encoding: Encoding) -> Result; +} + +pub trait StructPack: Sized { + fn pack(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()>; +} + +impl StructPack for Vec { + fn pack(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()> { + for item in self { + item.pack(writer, big, encoding)?; + } + Ok(()) + } +} + +struct_unpack_impl_for_num!(u8); +struct_unpack_impl_for_num!(u16); +struct_unpack_impl_for_num!(u32); +struct_unpack_impl_for_num!(u64); +struct_unpack_impl_for_num!(u128); +struct_unpack_impl_for_num!(i8); +struct_unpack_impl_for_num!(i16); +struct_unpack_impl_for_num!(i32); +struct_unpack_impl_for_num!(i64); +struct_unpack_impl_for_num!(i128); +struct_unpack_impl_for_num!(f32); +struct_unpack_impl_for_num!(f64);