Skip to main content

siglus_assets/
cgm.rs

1//! CGM (CG table) loader.
2//!
3//! Parses the `.cgm` CG table format.
4//!
5//! The `.cgm` payload is LZSS-compressed and then obfuscated by a 256-byte XOR table.
6//! Supported identifiers:
7//! - `CGTABLE` : legacy table entries (name + flag_no)
8//! - `CGTABLE2` : extended table entries (name + flag_no + code[5] + code_exist_cnt)
9
10use crate::lzss;
11use anyhow::{anyhow, bail, Result};
12use encoding_rs::SHIFT_JIS;
13use std::collections::HashMap;
14use std::fs;
15use std::path::Path;
16
17pub const CG_TABLE_DATA_CODE_MAX: usize = 5;
18const AVG_CG_TABLE_NAME_LEN: usize = 32;
19
20#[derive(Debug, Clone)]
21pub struct CgTableEntry {
22    pub name: String,
23    pub flag_no: i32,
24    pub code_exist_cnt: i32,
25    pub code: [i32; CG_TABLE_DATA_CODE_MAX],
26    pub list_no: i32,
27    pub group: [i32; CG_TABLE_DATA_CODE_MAX],
28}
29
30#[derive(Debug, Clone)]
31pub struct CgGroupTree {
32    /// Index into `CgTableData.entries`.
33    pub sub_index: usize,
34    pub tree: Vec<CgGroupTree>,
35}
36
37impl CgGroupTree {
38    fn new(sub_index: usize) -> Self {
39        Self {
40            sub_index,
41            tree: Vec::new(),
42        }
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct CgTableData {
48    pub entries: Vec<CgTableEntry>,
49
50    name_find: HashMap<String, usize>,
51    flag_find: HashMap<i32, usize>,
52    sort_list: Vec<usize>,
53
54    /// Root group tree node.
55    group_tree_root: CgGroupTree,
56}
57
58impl CgTableData {
59    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
60        let buf = fs::read(path)?;
61        Self::from_bytes(&buf)
62    }
63
64    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
65        let mut buf = bytes.to_vec();
66        let entries = expand_cgm_in_place(&mut buf)?;
67        let mut out = Self {
68            entries,
69            name_find: HashMap::new(),
70            flag_find: HashMap::new(),
71            sort_list: Vec::new(),
72            group_tree_root: CgGroupTree::new(0),
73        };
74        out.create_name_find_map();
75        out.create_flag_find_map();
76        out.create_sort_list();
77        out.create_group_tree();
78        Ok(out)
79    }
80
81    pub fn get_cg_cnt(&self) -> usize {
82        self.entries.len()
83    }
84
85    pub fn get_sub_from_name(&self, name: &str) -> Option<&CgTableEntry> {
86        let upper = name.to_ascii_uppercase();
87        let idx = self.name_find.get(&upper).copied()?;
88        self.entries.get(idx)
89    }
90
91    pub fn get_sub_from_list_no(&self, list_no: i32) -> Option<&CgTableEntry> {
92        if list_no < 0 {
93            return None;
94        }
95        self.entries.get(list_no as usize)
96    }
97
98    pub fn get_sub_from_flag_no(&self, flag_no: i32) -> Option<&CgTableEntry> {
99        let idx = self.flag_find.get(&flag_no).copied()?;
100        self.entries.get(idx)
101    }
102
103    /// Lookup by group code indices (gc0..gc4), matching `get_sub_pointer_from_group_code_func`.
104    pub fn get_sub_from_group_code(
105        &self,
106        gc0: i32,
107        gc1: i32,
108        gc2: i32,
109        gc3: i32,
110        gc4: i32,
111    ) -> Option<&CgTableEntry> {
112        let g = self.get_group_tree_pointer(gc0, gc1, gc2, gc3, gc4)?;
113        self.entries.get(g.sub_index)
114    }
115
116    /// Collect flag numbers under a group subtree (depth-first), matching `get_flag_list_func`.
117    pub fn get_flag_list(&self, gc0: i32, gc1: i32, gc2: i32, gc3: i32, gc4: i32) -> Vec<i32> {
118        let mut out = Vec::new();
119        let Some(g) = self.get_group_tree_pointer(gc0, gc1, gc2, gc3, gc4) else {
120            return out;
121        };
122        if g.tree.is_empty() {
123            return out;
124        }
125        self.get_flag_list_rec(g, &mut out);
126        out
127    }
128
129    fn create_name_find_map(&mut self) {
130        self.name_find.clear();
131        for (i, e) in self.entries.iter().enumerate() {
132            // The engine uses std::map::insert which keeps the first on duplicates.
133            self.name_find.entry(e.name.clone()).or_insert(i);
134        }
135    }
136
137    fn create_flag_find_map(&mut self) {
138        self.flag_find.clear();
139        for (i, e) in self.entries.iter().enumerate() {
140            self.flag_find.entry(e.flag_no).or_insert(i);
141        }
142    }
143
144    fn create_sort_list(&mut self) {
145        self.sort_list.clear();
146        self.sort_list.extend(0..self.entries.len());
147        self.sort_list.sort_by(|&a, &b| {
148            let lhs = &self.entries[a];
149            let rhs = &self.entries[b];
150            for i in 0..CG_TABLE_DATA_CODE_MAX {
151                if lhs.code[i] < rhs.code[i] {
152                    return std::cmp::Ordering::Less;
153                }
154                if lhs.code[i] > rhs.code[i] {
155                    return std::cmp::Ordering::Greater;
156                }
157            }
158            lhs.list_no.cmp(&rhs.list_no)
159        });
160    }
161
162    fn create_group_tree(&mut self) {
163        if self.sort_list.is_empty() {
164            self.group_tree_root = CgGroupTree::new(0);
165            return;
166        }
167
168        // IMPORTANT: build into a local root first.
169        //
170        // The naive translation from the original implementation tends to call something like:
171        //   self.create_group_tree_rec(&mut self.group_tree_root, ...)
172        // which triggers E0499 (multiple mutable borrows of `self`).
173        //
174        // Using a local `root` allows us to take disjoint borrows of `entries` and
175        // `sort_list` without ever holding `&mut self` across the recursive call.
176        let mut root = CgGroupTree::new(self.sort_list[0]);
177        let mut code = [-1i32; CG_TABLE_DATA_CODE_MAX];
178        let entries: &mut [CgTableEntry] = &mut self.entries;
179        let sort_list: &[usize] = &self.sort_list;
180        Self::create_group_tree_rec(entries, sort_list, &mut root, 0, &mut code, 0);
181        self.group_tree_root = root;
182    }
183
184    fn create_group_tree_rec(
185        entries: &mut [CgTableEntry],
186        sort_list: &[usize],
187        node: &mut CgGroupTree,
188        sort_list_index: usize,
189        code: &mut [i32; CG_TABLE_DATA_CODE_MAX],
190        code_index: usize,
191    ) {
192        let sort_list_index_backup = sort_list_index;
193
194        // Determine the span in sort_list for the current prefix code[0..code_index).
195        let mut end = sort_list_index;
196        while end < sort_list.len() {
197            let sub = &entries[sort_list[end]];
198            let mut loop_out = false;
199            for i in 0..code_index {
200                if sub.code[i] != code[i] {
201                    loop_out = true;
202                    break;
203                }
204            }
205            if loop_out {
206                break;
207            }
208            end += 1;
209        }
210
211        if sort_list_index_backup >= end {
212            return;
213        }
214
215        // Count groups at this level.
216        let mut group_cnt = 0usize;
217        let mut now_code = -1i32;
218        for idx in sort_list_index_backup..end {
219            let sub = &entries[sort_list[idx]];
220            if sub.code[code_index] != now_code || code_index == (CG_TABLE_DATA_CODE_MAX - 1) {
221                now_code = sub.code[code_index];
222                group_cnt += 1;
223            }
224        }
225        if group_cnt == 0 {
226            return;
227        }
228
229        node.tree.clear();
230        node.tree.reserve(group_cnt);
231
232        // Match engine: node.sub points to the first element in this span.
233        node.sub_index = sort_list[sort_list_index_backup];
234
235        // Second pass: build children and assign group indices to entries.
236        let mut now_code = -1i32;
237        let mut groupe_no = 0i32;
238        let mut tree_index = 0usize;
239        let mut idx = sort_list_index_backup;
240        while idx < end {
241            let sub_idx = sort_list[idx];
242            let sub_code = entries[sub_idx].code[code_index];
243            if sub_code != now_code || code_index == (CG_TABLE_DATA_CODE_MAX - 1) {
244                if now_code != -1 {
245                    groupe_no += 1;
246                }
247                now_code = sub_code;
248
249                // Ensure child exists.
250                node.tree.push(CgGroupTree::new(sub_idx));
251
252                if code_index + 1 < CG_TABLE_DATA_CODE_MAX {
253                    code[code_index] = now_code;
254                    let child = node.tree.get_mut(tree_index).unwrap();
255                    Self::create_group_tree_rec(
256                        entries,
257                        sort_list,
258                        child,
259                        idx,
260                        code,
261                        code_index + 1,
262                    );
263                }
264
265                // Match engine: child.sub points to the first element of its span.
266                node.tree[tree_index].sub_index = sub_idx;
267                tree_index += 1;
268            }
269
270            entries[sub_idx].group[code_index] = groupe_no;
271            idx += 1;
272        }
273    }
274
275    /// Match `get_groupe_tree_pointer_func` / `get_groupe_tree_pointer_funcfunc`.
276
277    fn get_group_tree_pointer(
278        &self,
279        gc0: i32,
280        gc1: i32,
281        gc2: i32,
282        gc3: i32,
283        gc4: i32,
284    ) -> Option<&CgGroupTree> {
285        if self.group_tree_root.tree.is_empty() {
286            return None;
287        }
288        let code = [gc0, gc1, gc2, gc3, gc4];
289        // SiglusEngine starts from `&cg_table_group_tree.tree[0]`.
290        Self::get_group_tree_pointer_rec(self.group_tree_root.tree.get(0)?, &code, 0)
291    }
292
293    fn get_group_tree_pointer_rec<'a>(
294        group: &'a CgGroupTree,
295        // NOTE: do NOT tie the lifetime of `code` to `'a`. `code` is often a
296        // local temporary array in the caller; if it shares `'a` with `group`,
297        // Rust will infer the returned reference is bounded by the shorter
298        // temporary lifetime, causing "lifetime may not live long enough".
299        code: &[i32; CG_TABLE_DATA_CODE_MAX],
300        code_index: usize,
301    ) -> Option<&'a CgGroupTree> {
302        let gc = code[code_index];
303        if gc == -1 {
304            return Some(group);
305        }
306        if gc < 0 {
307            return None;
308        }
309        let gc_u = gc as usize;
310        if gc_u >= group.tree.len() {
311            return None;
312        }
313        if code_index + 1 >= CG_TABLE_DATA_CODE_MAX {
314            return group.tree.get(gc_u);
315        }
316        if code[code_index + 1] == -1 {
317            return group.tree.get(gc_u);
318        }
319        let child = group.tree.get(gc_u)?;
320        Self::get_group_tree_pointer_rec(child, code, code_index + 1)
321    }
322
323    fn get_flag_list_rec(&self, group: &CgGroupTree, out: &mut Vec<i32>) {
324        if group.tree.len() <= 1 {
325            out.push(self.entries[group.sub_index].flag_no);
326            return;
327        }
328        for child in &group.tree {
329            self.get_flag_list_rec(child, out);
330        }
331    }
332}
333
334#[derive(Debug, Clone, Copy)]
335struct AvgCgTableHeader {
336    head: [u8; 16],
337    cnt: i32,
338    auto_flag: i32,
339    rev0: i32,
340    rev1: i32,
341}
342
343fn expand_cgm_in_place(buf: &mut [u8]) -> Result<Vec<CgTableEntry>> {
344    if buf.len() < 16 + 4 * 4 {
345        bail!("CGM: input too short");
346    }
347
348    let mut off = 0usize;
349    let mut head = [0u8; 16];
350    head.copy_from_slice(&buf[0..16]);
351    off += 16;
352
353    let cnt = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
354    off += 4;
355    let auto_flag = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
356    off += 4;
357    let rev0 = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
358    off += 4;
359    let rev1 = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
360    off += 4;
361
362    let header = AvgCgTableHeader {
363        head,
364        cnt,
365        auto_flag,
366        rev0,
367        rev1,
368    };
369
370    if header.cnt <= 0 {
371        bail!("CGM: invalid cnt={}", header.cnt);
372    }
373
374    let ident = c_string_prefix(&header.head);
375
376    let wp = &mut buf[off..];
377    tpc_angou_in_place(wp);
378
379    let expand_data = lzss::lzss_unpack_lenient(wp)?;
380
381    match ident.as_str() {
382        "CGTABLE2" => parse_table2(&expand_data, header.cnt as usize),
383        "CGTABLE" => parse_table1(&expand_data, header.cnt as usize),
384        _ => bail!("CGM: unsupported identifier: {ident}"),
385    }
386}
387
388fn parse_table2(expand_data: &[u8], cnt: usize) -> Result<Vec<CgTableEntry>> {
389    let entry_size = AVG_CG_TABLE_NAME_LEN + 4 + (CG_TABLE_DATA_CODE_MAX * 4) + 4;
390    let need = cnt
391        .checked_mul(entry_size)
392        .ok_or_else(|| anyhow!("CGM: size overflow"))?;
393    if expand_data.len() < need {
394        bail!(
395            "CGM: expanded data too short (need={}, got={})",
396            need,
397            expand_data.len()
398        );
399    }
400
401    let mut out = Vec::with_capacity(cnt);
402    let mut off = 0usize;
403    for i in 0..cnt {
404        let name_raw = &expand_data[off..off + AVG_CG_TABLE_NAME_LEN];
405        off += AVG_CG_TABLE_NAME_LEN;
406
407        let flag_no = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
408        off += 4;
409
410        let mut code = [0i32; CG_TABLE_DATA_CODE_MAX];
411        for j in 0..CG_TABLE_DATA_CODE_MAX {
412            code[j] = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
413            off += 4;
414        }
415
416        let code_exist_cnt = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
417        off += 4;
418
419        let mut name = decode_name(name_raw);
420        name = name.to_ascii_uppercase();
421
422        out.push(CgTableEntry {
423            name,
424            flag_no,
425            code_exist_cnt,
426            code,
427            list_no: i as i32,
428            group: [-1; CG_TABLE_DATA_CODE_MAX],
429        });
430    }
431    Ok(out)
432}
433
434fn parse_table1(expand_data: &[u8], cnt: usize) -> Result<Vec<CgTableEntry>> {
435    let entry_size = AVG_CG_TABLE_NAME_LEN + 4;
436    let need = cnt
437        .checked_mul(entry_size)
438        .ok_or_else(|| anyhow!("CGM: size overflow"))?;
439    if expand_data.len() < need {
440        bail!(
441            "CGM: expanded data too short (need={}, got={})",
442            need,
443            expand_data.len()
444        );
445    }
446
447    let mut out = Vec::with_capacity(cnt);
448    let mut off = 0usize;
449    for i in 0..cnt {
450        let name_raw = &expand_data[off..off + AVG_CG_TABLE_NAME_LEN];
451        off += AVG_CG_TABLE_NAME_LEN;
452
453        let flag_no = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
454        off += 4;
455
456        let mut name = decode_name(name_raw);
457        name = name.to_ascii_uppercase();
458
459        out.push(CgTableEntry {
460            name,
461            flag_no,
462            code_exist_cnt: 0,
463            code: [0; CG_TABLE_DATA_CODE_MAX],
464            list_no: i as i32,
465            group: [-1; CG_TABLE_DATA_CODE_MAX],
466        });
467    }
468    Ok(out)
469}
470
471fn decode_name(name_raw: &[u8]) -> String {
472    let end = name_raw
473        .iter()
474        .position(|&b| b == 0)
475        .unwrap_or(name_raw.len());
476    let (cow, _, _) = SHIFT_JIS.decode(&name_raw[..end]);
477    cow.into_owned()
478}
479
480fn c_string_prefix(buf: &[u8]) -> String {
481    let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
482    let (cow, _, _) = SHIFT_JIS.decode(&buf[..end]);
483    cow.into_owned()
484}
485
486/// `tpc_angou` is XOR with a 256-byte repeating table.
487fn tpc_angou_in_place(src: &mut [u8]) {
488    for (i, b) in src.iter_mut().enumerate() {
489        *b ^= TPC_ANGOU_TABLE[i & 0xFF];
490    }
491}
492
493// Angou XOR table (256 bytes, repeats).
494const TPC_ANGOU_TABLE: [u8; 256] = [
495    0x8b, 0xe5, 0x5d, 0xc3, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x85, 0xc0, 0x74, 0x09, 0x5f, 0x5e, 0x33,
496    0xc0, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x45, 0x0c, 0x85, 0xc0, 0x75, 0x14, 0x8b, 0x55, 0xec,
497    0x83, 0xc2, 0x20, 0x52, 0x6a, 0x00, 0xe8, 0xf5, 0x28, 0x01, 0x00, 0x83, 0xc4, 0x08, 0x89, 0x45,
498    0x0c, 0x8b, 0x45, 0xe4, 0x6a, 0x00, 0x6a, 0x00, 0x50, 0x53, 0xff, 0x15, 0x34, 0xb1, 0x43, 0x00,
499    0x8b, 0x45, 0x10, 0x85, 0xc0, 0x74, 0x05, 0x8b, 0x4d, 0xec, 0x89, 0x08, 0x8a, 0x45, 0xf0, 0x84,
500    0xc0, 0x75, 0x78, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x8b, 0x7d, 0xe8, 0x8b, 0x75, 0x0c, 0x85, 0xc0,
501    0x75, 0x44, 0x8b, 0x1d, 0xd0, 0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x37, 0x81, 0xff, 0x00, 0x00,
502    0x04, 0x00, 0x6a, 0x00, 0x76, 0x43, 0x8b, 0x45, 0xf8, 0x8d, 0x55, 0xfc, 0x52, 0x68, 0x00, 0x00,
503    0x04, 0x00, 0x56, 0x50, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0x6a, 0x05, 0xff, 0xd3, 0xa1, 0xe0,
504    0x30, 0x44, 0x00, 0x81, 0xef, 0x00, 0x00, 0x04, 0x00, 0x81, 0xc6, 0x00, 0x00, 0x04, 0x00, 0x85,
505    0xc0, 0x74, 0xc5, 0x8b, 0x5d, 0xf8, 0x53, 0xe8, 0xf4, 0xfb, 0xff, 0xff, 0x8b, 0x45, 0x0c, 0x83,
506    0xc4, 0x04, 0x5f, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x55, 0xf8, 0x8d, 0x4d, 0xfc, 0x51,
507    0x57, 0x56, 0x52, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0xeb, 0xd8, 0x8b, 0x45, 0xe8, 0x83, 0xc0,
508    0x20, 0x50, 0x6a, 0x00, 0xe8, 0x47, 0x28, 0x01, 0x00, 0x8b, 0x7d, 0xe8, 0x89, 0x45, 0xf4, 0x8b,
509    0xf0, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x83, 0xc4, 0x08, 0x85, 0xc0, 0x75, 0x56, 0x8b, 0x1d, 0xd0,
510    0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x49, 0x81, 0xff, 0x00, 0x00, 0x04, 0x00, 0x6a, 0x00, 0x76,
511];