mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-06 21:08:48 +08:00
197 lines
6.3 KiB
Rust
197 lines
6.3 KiB
Rust
//! Buriko General Interpreter/Ethornell BP Script (._bp)
|
|
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)]
|
|
/// Builder for BGI BP scripts.
|
|
pub struct BGIBpScriptBuilder {}
|
|
|
|
impl BGIBpScriptBuilder {
|
|
/// Creates a new instance of `BGIBpScriptBuilder`.
|
|
pub fn new() -> Self {
|
|
BGIBpScriptBuilder {}
|
|
}
|
|
}
|
|
|
|
impl ScriptBuilder for BGIBpScriptBuilder {
|
|
fn default_encoding(&self) -> Encoding {
|
|
Encoding::Cp932
|
|
}
|
|
|
|
fn build_script(
|
|
&self,
|
|
buf: Vec<u8>,
|
|
_filename: &str,
|
|
encoding: Encoding,
|
|
_archive_encoding: Encoding,
|
|
config: &ExtraConfig,
|
|
_archive: Option<&Box<dyn Script>>,
|
|
) -> Result<Box<dyn Script>> {
|
|
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)]
|
|
/// BGI BP script.
|
|
pub struct BGIBpScript {
|
|
data: MemReader,
|
|
header_size: u32,
|
|
strings: Vec<BpString>,
|
|
encoding: Encoding,
|
|
}
|
|
|
|
impl BGIBpScript {
|
|
/// Creates a new instance of `BGIBpScript` from a buffer.
|
|
///
|
|
/// * `buf` - The buffer containing the script data.
|
|
/// * `encoding` - The encoding of the script.
|
|
/// * `config` - Extra configuration options.
|
|
pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
|
|
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))?;
|
|
let max_instr_len = reader.data.len();
|
|
while reader.pos < max_instr_len {
|
|
let instr = reader.peek_u8()?;
|
|
if instr == 0x17 {
|
|
let need_zero_bytes = 3 - reader.pos % 4;
|
|
let mut all_zero = true;
|
|
for i in 0..need_zero_bytes {
|
|
let b = reader.peek_u8_at(reader.pos as u64 + 1 + i as u64)?;
|
|
if b != 0 {
|
|
all_zero = false;
|
|
break;
|
|
}
|
|
}
|
|
if all_zero {
|
|
last_instr_pos = reader.pos;
|
|
reader.pos += 1 + need_zero_bytes;
|
|
continue;
|
|
}
|
|
}
|
|
reader.pos += 1;
|
|
}
|
|
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<Vec<Message>> {
|
|
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 as u64)?;
|
|
let str = decode_to_string(self.encoding, str.as_bytes(), true)?;
|
|
messages.push(Message {
|
|
name: None,
|
|
message: str,
|
|
});
|
|
}
|
|
Ok(messages)
|
|
}
|
|
|
|
fn import_messages<'a>(
|
|
&'a self,
|
|
messages: Vec<Message>,
|
|
mut file: Box<dyn WriteSeek + 'a>,
|
|
_filename: &str,
|
|
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 u64)?
|
|
.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 as u64, new_text_offset)?;
|
|
new_pos += new_str_len;
|
|
} else {
|
|
file.write_all_at(text_address as u64, &str)?;
|
|
}
|
|
}
|
|
let new_instr_size = (new_pos - self.header_size as usize) as u32;
|
|
file.write_u32_at(4, new_instr_size)?;
|
|
Ok(())
|
|
}
|
|
}
|