diff --git a/src/args.rs b/src/args.rs index 96019e4..28781b7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -160,6 +160,13 @@ pub enum Command { /// Output directory output: Option, }, + /// Create a new script file + Create { + /// Input script + input: String, + /// Output script file + output: Option, + }, } pub fn parse_args() -> Arg { diff --git a/src/main.rs b/src/main.rs index f29a5ab..8eeff02 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1043,6 +1043,47 @@ pub fn unpack_archive( Ok(types::ScriptResult::Ok) } +pub fn create_file(input: &str, output: Option<&str>, arg: &args::Arg) -> anyhow::Result<()> { + let typ = match &arg.script_type { + Some(t) => t, + None => { + return Err(anyhow::anyhow!("No script type specified")); + } + }; + let builder = scripts::BUILDER + .iter() + .find(|b| b.script_type() == typ) + .ok_or_else(|| anyhow::anyhow!("Unsupported script type"))?; + + if !builder.can_create_file() { + return Err(anyhow::anyhow!( + "Script type {:?} does not support file creation", + typ + )); + } + + let output = match output { + Some(output) => output.to_string(), + None => { + let mut pb = std::path::PathBuf::from(input); + let ext = builder.extensions().first().unwrap_or(&"unk"); + pb.set_extension(ext); + if pb.to_string_lossy() == input { + pb.set_extension(format!("{}.{}", ext, ext)); + } + pb.to_string_lossy().into_owned() + } + }; + + builder.create_file_filename( + input, + &output, + get_encoding(arg, builder), + get_output_encoding(arg), + )?; + Ok(()) +} + lazy_static::lazy_static! { static ref COUNTER: utils::counter::Counter = utils::counter::Counter::new(); } @@ -1195,6 +1236,16 @@ fn main() { } } } + args::Command::Create { input, output } => { + let re = create_file(input, output.as_ref().map(|s| s.as_str()), &arg); + if let Err(e) = re { + COUNTER.inc_error(); + eprintln!("Error creating file: {}", e); + if arg.backtrace { + eprintln!("Backtrace: {}", e.backtrace()); + } + } + } } eprintln!("{}", std::ops::Deref::deref(&COUNTER)); } diff --git a/src/scripts/base.rs b/src/scripts/base.rs index fba6d16..21ab892 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -79,6 +79,34 @@ pub trait ScriptBuilder: std::fmt::Debug { "This script type does not support creating an archive." )) } + + fn can_create_file(&self) -> bool { + false + } + + fn create_file<'a>( + &'a self, + _filename: &'a str, + _writer: Box, + _encoding: Encoding, + _file_encoding: Encoding, + ) -> Result<()> { + Err(anyhow::anyhow!( + "This script type does not support creating directly." + )) + } + + fn create_file_filename( + &self, + filename: &str, + output_filename: &str, + encoding: Encoding, + file_encoding: Encoding, + ) -> Result<()> { + let f = std::fs::File::create(output_filename)?; + let f = std::io::BufWriter::new(f); + self.create_file(filename, Box::new(f), encoding, file_encoding) + } } pub trait ArchiveContent { diff --git a/src/scripts/bgi/bsi.rs b/src/scripts/bgi/bsi.rs new file mode 100644 index 0000000..e06acfd --- /dev/null +++ b/src/scripts/bgi/bsi.rs @@ -0,0 +1,156 @@ +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::{decode_to_string, encode_string}; +use anyhow::Result; +use std::collections::BTreeMap; +use std::ffi::CString; + +#[derive(Debug)] +pub struct BGIBsiScriptBuilder {} + +impl BGIBsiScriptBuilder { + pub fn new() -> Self { + BGIBsiScriptBuilder {} + } +} + +impl ScriptBuilder for BGIBsiScriptBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Utf8 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(BGIBsiScript::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["_bsi"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::BGIBsi + } + + fn can_create_file(&self) -> bool { + true + } + + fn create_file<'a>( + &'a self, + filename: &'a str, + writer: Box, + encoding: Encoding, + file_encoding: Encoding, + ) -> Result<()> { + create_file(filename, writer, encoding, file_encoding) + } +} + +#[derive(Debug)] +pub struct BGIBsiScript { + pub data: BTreeMap>, +} + +impl BGIBsiScript { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut data = BTreeMap::new(); + let mut reader = MemReader::new(buf); + let section_count = reader.read_u32()?; + for _ in 0..section_count { + let section_name = reader.read_cstring()?; + let section_name = decode_to_string(encoding, section_name.as_bytes())?; + let mut section_data = BTreeMap::new(); + let entry_count = reader.read_u32()?; + for _ in 0..entry_count { + let key = reader.read_cstring()?; + let key = decode_to_string(encoding, key.as_bytes())?; + let value = reader.read_cstring()?; + let value = decode_to_string(encoding, value.as_bytes())?; + section_data.insert(key, value); + } + data.insert(section_name, section_data); + } + if !reader.is_eof() { + eprintln!( + "Warning: BGIBsiScript data not fully read, remaining bytes: {}", + reader.data.len() - reader.pos + ); + crate::COUNTER.inc_warning(); + } + Ok(BGIBsiScript { data }) + } +} + +impl Script for BGIBsiScript { + 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.data) + .map_err(|e| anyhow::anyhow!("Failed to write BSI Map data 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(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + create_file(custom_filename, file, encoding, output_encoding) + } +} + +fn create_file<'a>( + custom_filename: &'a str, + mut writer: Box, + encoding: Encoding, + output_encoding: Encoding, +) -> Result<()> { + let input = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &input)?; + let data: BTreeMap> = serde_json::from_str(&s) + .map_err(|e| anyhow::anyhow!("Failed to read BSI Map data from JSON: {}", e))?; + writer.write_u32(data.len() as u32)?; + for (section_name, section_data) in data { + let section_name_bytes = encode_string(encoding, §ion_name, false)?; + let section_name = CString::new(section_name_bytes)?; + writer.write_cstring(§ion_name)?; + writer.write_u32(section_data.len() as u32)?; + for (key, value) in section_data { + let key_bytes = encode_string(encoding, &key, false)?; + let key = CString::new(key_bytes)?; + writer.write_cstring(&key)?; + let value_bytes = encode_string(encoding, &value, false)?; + let value = CString::new(value_bytes)?; + writer.write_cstring(&value)?; + } + } + Ok(()) +} diff --git a/src/scripts/bgi/mod.rs b/src/scripts/bgi/mod.rs index 5b504d4..6a7cc61 100644 --- a/src/scripts/bgi/mod.rs +++ b/src/scripts/bgi/mod.rs @@ -1,2 +1,3 @@ +pub mod bsi; mod parser; pub mod script; diff --git a/src/scripts/escude/list.rs b/src/scripts/escude/list.rs index 9f31e01..7e18490 100644 --- a/src/scripts/escude/list.rs +++ b/src/scripts/escude/list.rs @@ -49,6 +49,20 @@ impl ScriptBuilder for EscudeBinListBuilder { } None } + + fn can_create_file(&self) -> bool { + true + } + + fn create_file<'a>( + &'a self, + filename: &'a str, + writer: Box, + encoding: Encoding, + file_encoding: Encoding, + ) -> Result<()> { + create_file(filename, writer, encoding, file_encoding) + } } #[derive(Debug)] @@ -257,6 +271,37 @@ impl EscudeBinList { } } +fn create_file<'a>( + custom_filename: &'a str, + mut writer: Box, + encoding: Encoding, + output_encoding: Encoding, +) -> Result<()> { + let input = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &input)?; + let entries: Vec = serde_json::from_str(&s) + .map_err(|e| anyhow::anyhow!("Failed to read Escude list from JSON: {}", e))?; + writer.write_all(b"LIST")?; + writer.write_u32(0)?; // Placeholder for size + let mut total_size = 0; + for entry in entries { + let cur_pos = writer.stream_position()?; + writer.write_u32(entry.id)?; + writer.write_u32(0)?; // Placeholder for size + entry.data.pack(&mut writer, false, encoding)?; + let end_pos = writer.stream_position()?; + let size = (end_pos - cur_pos - 8) as u32; // 8 bytes for id and size + writer.seek(std::io::SeekFrom::Start(cur_pos + 4))?; // Seek to size position + writer.write_u32(size)?; + writer.seek(std::io::SeekFrom::Start(end_pos))?; // Seek to end + total_size += size + 8; + } + writer.seek(std::io::SeekFrom::Start(4))?; // Seek back to size position + writer.write_u32(total_size)?; + writer.flush()?; + Ok(()) +} + impl Script for EscudeBinList { fn default_output_script_type(&self) -> OutputScriptType { OutputScriptType::Custom @@ -287,33 +332,11 @@ impl Script for EscudeBinList { fn custom_import<'a>( &'a self, custom_filename: &'a str, - mut writer: Box, + writer: Box, encoding: Encoding, output_encoding: Encoding, ) -> Result<()> { - let input = crate::utils::files::read_file(custom_filename)?; - let s = decode_to_string(output_encoding, &input)?; - let entries: Vec = serde_json::from_str(&s) - .map_err(|e| anyhow::anyhow!("Failed to read Escude list from JSON: {}", e))?; - writer.write_all(b"LIST")?; - writer.write_u32(0)?; // Placeholder for size - let mut total_size = 0; - for entry in entries { - let cur_pos = writer.stream_position()?; - writer.write_u32(entry.id)?; - writer.write_u32(0)?; // Placeholder for size - entry.data.pack(&mut writer, false, encoding)?; - let end_pos = writer.stream_position()?; - let size = (end_pos - cur_pos - 8) as u32; // 8 bytes for id and size - writer.seek(std::io::SeekFrom::Start(cur_pos + 4))?; // Seek to size position - writer.write_u32(size)?; - writer.seek(std::io::SeekFrom::Start(end_pos))?; // Seek to end - total_size += size + 8; - } - writer.seek(std::io::SeekFrom::Start(4))?; // Seek back to size position - writer.write_u32(total_size)?; - writer.flush()?; - Ok(()) + create_file(custom_filename, writer, encoding, output_encoding) } } diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 7671a31..7ca682f 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -14,6 +14,8 @@ lazy_static::lazy_static! { Box::new(circus::script::CircusMesScriptBuilder::new()), #[cfg(feature = "bgi")] Box::new(bgi::script::BGIScriptBuilder::new()), + #[cfg(feature = "bgi")] + Box::new(bgi::bsi::BGIBsiScriptBuilder::new()), #[cfg(feature = "escude-arc")] Box::new(escude::archive::EscudeBinArchiveBuilder::new()), #[cfg(feature = "escude")] diff --git a/src/types.rs b/src/types.rs index 17c22a2..178b869 100644 --- a/src/types.rs +++ b/src/types.rs @@ -209,6 +209,10 @@ pub enum ScriptType { #[value(alias("ethornell"))] /// Buriko General Interpreter/Ethornell Script BGI, + #[cfg(feature = "bgi")] + #[value(alias("ethornell-bsi"))] + /// Buriko General Interpreter/Ethornell bsi script (._bsi) + BGIBsi, #[cfg(feature = "escude-arc")] /// Escude bin archive EscudeArc,