From a6e230eb7dd8eab2b2ffd32e22c2c503b557bb1e Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 8 Jun 2025 09:25:54 +0800 Subject: [PATCH] Add support for BGI ._bp script --- src/scripts/bgi/bp.rs | 170 +++++++++++++++++++++++++++++++++++++++++ src/scripts/bgi/mod.rs | 1 + src/scripts/mod.rs | 2 + src/types.rs | 4 + 4 files changed, 177 insertions(+) create mode 100644 src/scripts/bgi/bp.rs diff --git a/src/scripts/bgi/bp.rs b/src/scripts/bgi/bp.rs new file mode 100644 index 0000000..8f8e79e --- /dev/null +++ b/src/scripts/bgi/bp.rs @@ -0,0 +1,170 @@ +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::io::{Seek, SeekFrom}; + +#[derive(Debug)] +pub struct BGIBpScriptBuilder {} + +impl BGIBpScriptBuilder { + pub fn new() -> Self { + BGIBpScriptBuilder {} + } +} + +impl ScriptBuilder for BGIBpScriptBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + Ok(Box::new(BGIBpScript::new(buf, encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["_bp"] + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::BGIBp + } +} + +#[derive(Debug)] +struct BpString { + offset_pos: usize, + text_offset: u16, +} + +#[derive(Debug)] +pub struct BGIBpScript { + data: MemReader, + header_size: u32, + strings: Vec, + encoding: Encoding, +} + +impl BGIBpScript { + pub fn new(buf: Vec, encoding: Encoding, _config: &ExtraConfig) -> Result { + let mut reader = MemReader::new(buf); + let header_size = reader.read_u32()?; + let instr_size = reader.read_u32()?; + if header_size as usize + instr_size as usize != reader.data.len() { + return Err(anyhow::anyhow!("Invalid bp script file size")); + } + let mut last_instr_pos = 0; + reader.seek(SeekFrom::Start(header_size as u64))?; + for _ in 0..instr_size / 4 { + let instr = reader.read_u32()?; + if instr == 0x17 { + last_instr_pos = reader.pos; + } + } + if last_instr_pos == 0 { + return Err(anyhow::anyhow!("No end instruction found in bp script")); + } + reader.seek(SeekFrom::Start(header_size as u64))?; + let mut strings = Vec::new(); + while reader.pos < last_instr_pos { + let ins = reader.read_u8()?; + if ins == 5 { + let text_offset = reader.peek_u16()?; + let text_address = reader.pos + text_offset as usize - 1; + if text_address >= last_instr_pos + && text_address < reader.data.len() + && (text_address == last_instr_pos || reader.data[text_address - 1] == 0) + { + strings.push(BpString { + offset_pos: reader.pos, + text_offset, + }); + reader.pos += 2; + } + } + } + return Ok(BGIBpScript { + data: reader, + header_size, + strings, + encoding, + }); + } +} + +impl Script for BGIBpScript { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Json + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn extract_messages(&self) -> Result> { + let mut messages = Vec::new(); + for i in self.strings.iter() { + let text_address = i.offset_pos + i.text_offset as usize - 1; + // println!("offset: {}, text address: {}, text_offset: {}", i.offset_pos, text_address, i.text_offset); + let str = self.data.cpeek_cstring_at(text_address)?; + let str = decode_to_string(self.encoding, str.as_bytes())?; + messages.push(Message { + name: None, + message: str, + }); + } + Ok(messages) + } + + fn import_messages<'a>( + &'a self, + messages: Vec, + mut file: Box, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + if messages.len() != self.strings.len() { + return Err(anyhow::anyhow!( + "Number of messages does not match the number of strings in the script" + )); + } + file.write_all(&self.data.data)?; + let mut new_pos = self.data.data.len(); + for (i, mes) in self.strings.iter().zip(messages) { + let text_address = i.offset_pos + i.text_offset as usize - 1; + let old_str_len = self + .data + .cpeek_cstring_at(text_address)? + .as_bytes_with_nul() + .len(); + let mut str = mes.message; + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + str = str.replace(key, value); + } + } + let mut str = encode_string(encoding, &str, false)?; + str.push(0); // Null terminator + let new_str_len = str.len(); + if new_str_len > old_str_len { + file.write_all(&str)?; + let new_text_offset = (new_pos - i.offset_pos + 1) as u16; + file.write_u16_at(i.offset_pos, new_text_offset)?; + new_pos += new_str_len; + } else { + file.write_all_at(text_address, &str)?; + } + } + let new_instr_size = (new_pos - self.header_size as usize) as u32; + file.write_u32_at(4, new_instr_size)?; + Ok(()) + } +} diff --git a/src/scripts/bgi/mod.rs b/src/scripts/bgi/mod.rs index 6a7cc61..b494910 100644 --- a/src/scripts/bgi/mod.rs +++ b/src/scripts/bgi/mod.rs @@ -1,3 +1,4 @@ +pub mod bp; pub mod bsi; mod parser; pub mod script; diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 7ca682f..12b1f9e 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -16,6 +16,8 @@ lazy_static::lazy_static! { Box::new(bgi::script::BGIScriptBuilder::new()), #[cfg(feature = "bgi")] Box::new(bgi::bsi::BGIBsiScriptBuilder::new()), + #[cfg(feature = "bgi")] + Box::new(bgi::bp::BGIBpScriptBuilder::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 178b869..1053f3b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -213,6 +213,10 @@ pub enum ScriptType { #[value(alias("ethornell-bsi"))] /// Buriko General Interpreter/Ethornell bsi script (._bsi) BGIBsi, + #[cfg(feature = "bgi")] + #[value(alias("ethornell-bp"))] + /// Buriko General Interpreter/Ethornell bp script (._bp) + BGIBp, #[cfg(feature = "escude-arc")] /// Escude bin archive EscudeArc,