Skip to main content

hcb2lua_decompiler/
parser.rs

1use serde::{Deserialize, Serialize};
2use std::collections::{BTreeMap, HashMap};
3use std::fs::File;
4use std::io::Read;
5use std::mem::size_of;
6use std::path::Path;
7use std::rc::Rc;
8use std::str::FromStr;
9
10use anyhow::{anyhow, Result};
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
13pub enum Nls {
14    #[default]
15    ShiftJIS = 0,
16    GBK = 1,
17    UTF8 = 2,
18}
19
20impl FromStr for Nls {
21    type Err = anyhow::Error;
22
23    fn from_str(s: &str) -> Result<Self> {
24        let lower = s.to_ascii_lowercase();
25        match lower.as_str() {
26            "sjis" => Ok(Nls::ShiftJIS),
27            "gbk" => Ok(Nls::GBK),
28            "utf8" => Ok(Nls::UTF8),
29            _ => Err(anyhow!("unknown NLS")),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35pub struct Syscall {
36    /// How many arguments the syscall takes from the stack.
37    pub args: u8,
38    /// Name of the syscall.
39    pub name: String,
40}
41
42#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43struct YamlSyscall {
44    pub args: u8,
45    pub name: String,
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49struct YamlExport {
50    pub nls: Nls,
51    pub custom_syscall_count: u16,
52    pub game_mode: u8,
53    pub game_mode_reserved: u8,
54    pub game_title: String,
55    pub syscall_count: u16,
56    pub syscalls: BTreeMap<usize, YamlSyscall>,
57}
58
59#[derive(Debug, Clone, Default, Serialize, Deserialize)]
60pub struct Parser {
61    #[serde(skip)]
62    pub buffer: Rc<Vec<u8>>,
63    pub nls: Nls,
64    pub sys_desc_offset: u32,
65    /// Entry point (offset) of the script.
66    pub entry_point: u32,
67    pub non_volatile_global_count: u16,
68    pub volatile_global_count: u16,
69    /// Register a script function as syscall (usually unused).
70    pub custom_syscall_count: u16,
71    /// Game resolution id.
72    pub game_mode: u8,
73    pub game_mode_reserved: u8,
74    pub game_title: String,
75    pub syscall_count: u16,
76    pub syscalls: HashMap<usize, Syscall>,
77}
78
79impl Parser {
80    pub fn new(path: impl AsRef<Path>, nls: Nls) -> Result<Self> {
81        let mut rdr = File::open(path)?;
82        let mut buffer = Vec::new();
83        rdr.read_to_end(&mut buffer)?;
84
85        let mut parser = Parser {
86            buffer: Rc::new(buffer),
87            nls,
88            sys_desc_offset: 0,
89            entry_point: 0,
90            non_volatile_global_count: 0,
91            volatile_global_count: 0,
92            custom_syscall_count: 0,
93            game_mode: 0,
94            game_mode_reserved: 0,
95            game_title: String::new(),
96            syscall_count: 0,
97            syscalls: HashMap::new(),
98        };
99
100        parser.parse_header()?;
101        Ok(parser)
102    }
103
104    pub fn len(&self) -> usize {
105        self.buffer.len()
106    }
107
108    pub fn read_u8(&self, offset: usize) -> Result<u8> {
109        self.buffer
110            .get(offset)
111            .copied()
112            .ok_or_else(|| anyhow!("offset out of bounds"))
113    }
114
115    pub fn read_i8(&self, offset: usize) -> Result<i8> {
116        Ok(self.read_u8(offset)? as i8)
117    }
118
119    pub fn read_u16(&self, offset: usize) -> Result<u16> {
120        if offset + 1 >= self.buffer.len() {
121            return Err(anyhow!("offset out of bounds"));
122        }
123        Ok(u16::from_le_bytes([
124            self.buffer[offset],
125            self.buffer[offset + 1],
126        ]))
127    }
128
129    pub fn read_i16(&self, offset: usize) -> Result<i16> {
130        Ok(self.read_u16(offset)? as i16)
131    }
132
133    pub fn read_u32(&self, offset: usize) -> Result<u32> {
134        if offset + 3 >= self.buffer.len() {
135            return Err(anyhow!("offset out of bounds"));
136        }
137        Ok(u32::from_le_bytes([
138            self.buffer[offset],
139            self.buffer[offset + 1],
140            self.buffer[offset + 2],
141            self.buffer[offset + 3],
142        ]))
143    }
144
145    pub fn read_i32(&self, offset: usize) -> Result<i32> {
146        Ok(self.read_u32(offset)? as i32)
147    }
148
149    pub fn read_f32(&self, offset: usize) -> Result<f32> {
150        if offset + 3 >= self.buffer.len() {
151            return Err(anyhow!("offset out of bounds"));
152        }
153        Ok(f32::from_le_bytes([
154            self.buffer[offset],
155            self.buffer[offset + 1],
156            self.buffer[offset + 2],
157            self.buffer[offset + 3],
158        ]))
159    }
160
161    /// Read a C-style string with a maximum length `len` (may contain an early NUL).
162    /// Then decode it into UTF-8 according to the configured NLS.
163    pub fn read_cstring(&self, offset: usize, len: usize) -> Result<String> {
164        if offset + len > self.buffer.len() {
165            return Err(anyhow!("offset out of bounds"));
166        }
167        let mut raw = Vec::new();
168        for i in 0..len {
169            let b = self.buffer[offset + i];
170            if b == 0 {
171                break;
172            }
173            raw.push(b);
174        }
175
176        let decoded = match self.nls {
177            Nls::ShiftJIS => {
178                let (s, _, had_err) = encoding_rs::SHIFT_JIS.decode(&raw);
179                if had_err {
180                    log::warn!("ShiftJIS decode error");
181                }
182                s
183            }
184            Nls::GBK => {
185                let (s, _, had_err) = encoding_rs::GBK.decode(&raw);
186                if had_err {
187                    log::warn!("GBK decode error");
188                }
189                s
190            }
191            Nls::UTF8 => {
192                let (s, _, had_err) = encoding_rs::UTF_8.decode(&raw);
193                if had_err {
194                    log::warn!("UTF-8 decode error");
195                }
196                s
197            }
198        };
199
200        Ok(decoded.to_string())
201    }
202
203    fn parse_header(&mut self) -> Result<()> {
204        let mut off: usize = 0;
205        self.sys_desc_offset = self.read_u32(off)?;
206
207        off = self.sys_desc_offset as usize;
208        self.entry_point = self.read_u32(off)?;
209        off += size_of::<u32>();
210
211        self.non_volatile_global_count = self.read_u16(off)?;
212        off += size_of::<u16>();
213
214        self.volatile_global_count = self.read_u16(off)?;
215        off += size_of::<u16>();
216
217        self.game_mode = self.read_u8(off)? as u8;
218        off += size_of::<u8>();
219
220        self.game_mode_reserved = self.read_u8(off)? as u8;
221        off += size_of::<u8>();
222
223        let title_len = self.read_u8(off)? as usize;
224        off += size_of::<u8>();
225
226        self.game_title = self.read_cstring(off, title_len)?;
227        off += title_len;
228
229        self.syscall_count = self.read_u16(off)?;
230        off += size_of::<u16>();
231
232        for i in 0..self.syscall_count {
233            let args = self.read_u8(off)?;
234            off += size_of::<u8>();
235
236            let name_len = self.read_u8(off)? as usize;
237            off += size_of::<u8>();
238
239            let name = self.read_cstring(off, name_len)?;
240            off += name_len;
241
242            self.syscalls.insert(i as usize, Syscall { args, name });
243        }
244
245        self.custom_syscall_count = self.read_u16(off)?;
246        if self.custom_syscall_count > 0 {
247            log::warn!("custom syscall count: {}", self.custom_syscall_count);
248        }
249
250        Ok(())
251    }
252
253    pub fn is_code_area(&self, addr: u32) -> bool {
254        addr >= 4 && addr < self.sys_desc_offset
255    }
256
257    pub fn get_syscall(&self, id: u16) -> Option<&Syscall> {
258        self.syscalls.get(&(id as usize))
259    }
260
261    pub fn get_all_syscalls(&self) -> &HashMap<usize, Syscall> {
262        &self.syscalls
263    }
264
265    pub fn export_yaml(&self, path: impl AsRef<Path>) -> Result<()> {
266        let mut syscalls = BTreeMap::new();
267        for (id, sc) in &self.syscalls {
268            syscalls.insert(
269                *id,
270                YamlSyscall {
271                    args: sc.args,
272                    name: sc.name.clone(),
273                },
274            );
275        }
276
277        let export = YamlExport {
278            nls: self.nls.clone(),
279            custom_syscall_count: self.custom_syscall_count,
280            game_mode: self.game_mode,
281            game_mode_reserved: self.game_mode_reserved,
282            game_title: self.game_title.clone(),
283            syscall_count: self.syscall_count,
284            syscalls,
285        };
286
287        let s = serde_yml::to_string(&export)?;
288        std::fs::write(path, s)?;
289        Ok(())
290    }
291}
292
293impl Parser {
294    pub fn get_title(&self) -> &str {
295        &self.game_title
296    }
297
298    pub fn get_non_volatile_global_count(&self) -> u16 {
299        self.non_volatile_global_count
300    }
301
302    pub fn get_volatile_global_count(&self) -> u16 {
303        self.volatile_global_count
304    }
305
306    pub fn get_screen_size(&self) -> (u32, u32) {
307        match self.game_mode {
308            0 => (640, 480),
309            1 => (800, 600),
310            2 => (1024, 768),
311            3 => (1280, 960),
312            4 => (1600, 1200),
313            5 => (640, 480),
314            6 => (1024, 576),
315            7 => (1024, 640),
316            8 => (1280, 720),
317            9 => (1280, 800),
318            10 => (1440, 810),
319            11 => (1440, 900),
320            12 => (1680, 945),
321            13 => (1680, 1050),
322            14 => (1920, 1080),
323            15 => (1920, 1200),
324            _ => {
325                log::warn!(
326                    "unknown resolution: {}, defaulting to 640x480",
327                    self.game_mode
328                );
329                (640, 480)
330            }
331        }
332    }
333
334    pub fn get_game_mode(&self) -> u8 {
335        self.game_mode
336    }
337
338    pub fn get_game_mode_reserved(&self) -> u8 {
339        self.game_mode_reserved
340    }
341
342    pub fn get_entry_point(&self) -> u32 {
343        self.entry_point
344    }
345
346    pub fn get_custom_syscall_count(&self) -> u16 {
347        self.custom_syscall_count
348    }
349
350    pub fn get_sys_desc_offset(&self) -> u32 {
351        self.sys_desc_offset
352    }
353}