msg_tool\scripts\bgi/
bp.rs

1//! Buriko General Interpreter/Ethornell BP Script (._bp)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::{decode_to_string, encode_string};
6use anyhow::Result;
7use std::io::{Seek, SeekFrom};
8
9#[derive(Debug)]
10/// Builder for BGI BP scripts.
11pub struct BGIBpScriptBuilder {}
12
13impl BGIBpScriptBuilder {
14    /// Creates a new instance of `BGIBpScriptBuilder`.
15    pub fn new() -> Self {
16        BGIBpScriptBuilder {}
17    }
18}
19
20impl ScriptBuilder for BGIBpScriptBuilder {
21    fn default_encoding(&self) -> Encoding {
22        Encoding::Cp932
23    }
24
25    fn build_script(
26        &self,
27        buf: Vec<u8>,
28        _filename: &str,
29        encoding: Encoding,
30        _archive_encoding: Encoding,
31        config: &ExtraConfig,
32        _archive: Option<&Box<dyn Script>>,
33    ) -> Result<Box<dyn Script>> {
34        Ok(Box::new(BGIBpScript::new(buf, encoding, config)?))
35    }
36
37    fn extensions(&self) -> &'static [&'static str] {
38        &["_bp"]
39    }
40
41    fn script_type(&self) -> &'static ScriptType {
42        &ScriptType::BGIBp
43    }
44}
45
46#[derive(Debug)]
47struct BpString {
48    offset_pos: usize,
49    text_offset: u16,
50}
51
52#[derive(Debug)]
53/// BGI BP script.
54pub struct BGIBpScript {
55    data: MemReader,
56    header_size: u32,
57    strings: Vec<BpString>,
58    encoding: Encoding,
59}
60
61impl BGIBpScript {
62    /// Creates a new instance of `BGIBpScript` from a buffer.
63    ///
64    /// * `buf` - The buffer containing the script data.
65    /// * `encoding` - The encoding of the script.
66    /// * `config` - Extra configuration options.
67    pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
68        let mut reader = MemReader::new(buf);
69        let header_size = reader.read_u32()?;
70        let instr_size = reader.read_u32()?;
71        if header_size as usize + instr_size as usize != reader.data.len() {
72            return Err(anyhow::anyhow!("Invalid bp script file size"));
73        }
74        let mut last_instr_pos = 0;
75        reader.seek(SeekFrom::Start(header_size as u64))?;
76        let max_instr_len = reader.data.len() - 4;
77        while reader.pos < max_instr_len {
78            let instr = reader.cpeek_u32()?;
79            if instr == 0x17 {
80                last_instr_pos = reader.pos;
81                reader.pos += 4;
82            } else {
83                reader.pos += 1;
84            }
85        }
86        if last_instr_pos == 0 {
87            // return Err(anyhow::anyhow!("No end instruction found in bp script"));
88            last_instr_pos = reader.data.len();
89        }
90        reader.seek(SeekFrom::Start(header_size as u64))?;
91        let mut strings = Vec::new();
92        while reader.pos < last_instr_pos {
93            let ins = reader.read_u8()?;
94            if ins == 5 {
95                let text_offset = reader.peek_u16()?;
96                let text_address = reader.pos + text_offset as usize - 1;
97                if text_address >= last_instr_pos
98                    && text_address < reader.data.len()
99                    && (text_address == last_instr_pos || reader.data[text_address - 1] == 0)
100                {
101                    strings.push(BpString {
102                        offset_pos: reader.pos,
103                        text_offset,
104                    });
105                    reader.pos += 2;
106                }
107            }
108        }
109        return Ok(BGIBpScript {
110            data: reader,
111            header_size,
112            strings,
113            encoding,
114        });
115    }
116}
117
118impl Script for BGIBpScript {
119    fn default_output_script_type(&self) -> OutputScriptType {
120        OutputScriptType::Json
121    }
122
123    fn default_format_type(&self) -> FormatOptions {
124        FormatOptions::None
125    }
126
127    fn extract_messages(&self) -> Result<Vec<Message>> {
128        let mut messages = Vec::new();
129        for i in self.strings.iter() {
130            let text_address = i.offset_pos + i.text_offset as usize - 1;
131            // println!("offset: {}, text address: {}, text_offset: {}", i.offset_pos, text_address, i.text_offset);
132            let str = self.data.cpeek_cstring_at(text_address as u64)?;
133            let str = decode_to_string(self.encoding, str.as_bytes(), true)?;
134            messages.push(Message {
135                name: None,
136                message: str,
137            });
138        }
139        Ok(messages)
140    }
141
142    fn import_messages<'a>(
143        &'a self,
144        messages: Vec<Message>,
145        mut file: Box<dyn WriteSeek + 'a>,
146        _filename: &str,
147        encoding: Encoding,
148        replacement: Option<&'a ReplacementTable>,
149    ) -> Result<()> {
150        if messages.len() != self.strings.len() {
151            return Err(anyhow::anyhow!(
152                "Number of messages does not match the number of strings in the script"
153            ));
154        }
155        file.write_all(&self.data.data)?;
156        let mut new_pos = self.data.data.len();
157        for (i, mes) in self.strings.iter().zip(messages) {
158            let text_address = i.offset_pos + i.text_offset as usize - 1;
159            let old_str_len = self
160                .data
161                .cpeek_cstring_at(text_address as u64)?
162                .as_bytes_with_nul()
163                .len();
164            let mut str = mes.message;
165            if let Some(replacement) = replacement {
166                for (key, value) in replacement.map.iter() {
167                    str = str.replace(key, value);
168                }
169            }
170            let mut str = encode_string(encoding, &str, false)?;
171            str.push(0); // Null terminator
172            let new_str_len = str.len();
173            if new_str_len > old_str_len {
174                file.write_all(&str)?;
175                let new_text_offset = (new_pos - i.offset_pos + 1) as u16;
176                file.write_u16_at(i.offset_pos as u64, new_text_offset)?;
177                new_pos += new_str_len;
178            } else {
179                file.write_all_at(text_address as u64, &str)?;
180            }
181        }
182        let new_instr_size = (new_pos - self.header_size as usize) as u32;
183        file.write_u32_at(4, new_instr_size)?;
184        Ok(())
185    }
186}