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::{Result, anyhow, bail};
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    fn get_group_tree_pointer(
277        &self,
278        gc0: i32,
279        gc1: i32,
280        gc2: i32,
281        gc3: i32,
282        gc4: i32,
283    ) -> Option<&CgGroupTree> {
284        if self.group_tree_root.tree.is_empty() {
285            return None;
286        }
287        let code = [gc0, gc1, gc2, gc3, gc4];
288        // SiglusEngine starts from `&cg_table_group_tree.tree[0]`.
289        Self::get_group_tree_pointer_rec(self.group_tree_root.tree.first()?, &code, 0)
290    }
291
292    fn get_group_tree_pointer_rec<'a>(
293        group: &'a CgGroupTree,
294        // NOTE: do NOT tie the lifetime of `code` to `'a`. `code` is often a
295        // local temporary array in the caller; if it shares `'a` with `group`,
296        // Rust will infer the returned reference is bounded by the shorter
297        // temporary lifetime, causing "lifetime may not live long enough".
298        code: &[i32; CG_TABLE_DATA_CODE_MAX],
299        code_index: usize,
300    ) -> Option<&'a CgGroupTree> {
301        let gc = code[code_index];
302        if gc == -1 {
303            return Some(group);
304        }
305        if gc < 0 {
306            return None;
307        }
308        let gc_u = gc as usize;
309        if gc_u >= group.tree.len() {
310            return None;
311        }
312        if code_index + 1 >= CG_TABLE_DATA_CODE_MAX {
313            return group.tree.get(gc_u);
314        }
315        if code[code_index + 1] == -1 {
316            return group.tree.get(gc_u);
317        }
318        let child = group.tree.get(gc_u)?;
319        Self::get_group_tree_pointer_rec(child, code, code_index + 1)
320    }
321
322    fn get_flag_list_rec(&self, group: &CgGroupTree, out: &mut Vec<i32>) {
323        if group.tree.len() <= 1 {
324            out.push(self.entries[group.sub_index].flag_no);
325            return;
326        }
327        for child in &group.tree {
328            self.get_flag_list_rec(child, out);
329        }
330    }
331}
332
333#[derive(Debug, Clone, Copy)]
334struct AvgCgTableHeader {
335    head: [u8; 16],
336    cnt: i32,
337    auto_flag: i32,
338    rev0: i32,
339    rev1: i32,
340}
341
342fn expand_cgm_in_place(buf: &mut [u8]) -> Result<Vec<CgTableEntry>> {
343    if buf.len() < 16 + 4 * 4 {
344        bail!("CGM: input too short");
345    }
346
347    let mut off = 0usize;
348    let mut head = [0u8; 16];
349    head.copy_from_slice(&buf[0..16]);
350    off += 16;
351
352    let cnt = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
353    off += 4;
354    let auto_flag = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
355    off += 4;
356    let rev0 = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
357    off += 4;
358    let rev1 = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
359    off += 4;
360
361    let header = AvgCgTableHeader {
362        head,
363        cnt,
364        auto_flag,
365        rev0,
366        rev1,
367    };
368
369    if header.cnt <= 0 {
370        bail!("CGM: invalid cnt={}", header.cnt);
371    }
372
373    let ident = c_string_prefix(&header.head);
374
375    let wp = &mut buf[off..];
376    tpc_angou_in_place(wp);
377
378    let expand_data = lzss::lzss_unpack_lenient(wp)?;
379
380    match ident.as_str() {
381        "CGTABLE2" => parse_table2(&expand_data, header.cnt as usize),
382        "CGTABLE" => parse_table1(&expand_data, header.cnt as usize),
383        _ => bail!("CGM: unsupported identifier: {ident}"),
384    }
385}
386
387fn parse_table2(expand_data: &[u8], cnt: usize) -> Result<Vec<CgTableEntry>> {
388    let entry_size = AVG_CG_TABLE_NAME_LEN + 4 + (CG_TABLE_DATA_CODE_MAX * 4) + 4;
389    let need = cnt
390        .checked_mul(entry_size)
391        .ok_or_else(|| anyhow!("CGM: size overflow"))?;
392    if expand_data.len() < need {
393        bail!(
394            "CGM: expanded data too short (need={}, got={})",
395            need,
396            expand_data.len()
397        );
398    }
399
400    let mut out = Vec::with_capacity(cnt);
401    let mut off = 0usize;
402    for i in 0..cnt {
403        let name_raw = &expand_data[off..off + AVG_CG_TABLE_NAME_LEN];
404        off += AVG_CG_TABLE_NAME_LEN;
405
406        let flag_no = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
407        off += 4;
408
409        let mut code = [0i32; CG_TABLE_DATA_CODE_MAX];
410        for j in 0..CG_TABLE_DATA_CODE_MAX {
411            code[j] = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
412            off += 4;
413        }
414
415        let code_exist_cnt = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
416        off += 4;
417
418        let mut name = decode_name(name_raw);
419        name = name.to_ascii_uppercase();
420
421        out.push(CgTableEntry {
422            name,
423            flag_no,
424            code_exist_cnt,
425            code,
426            list_no: i as i32,
427            group: [-1; CG_TABLE_DATA_CODE_MAX],
428        });
429    }
430    Ok(out)
431}
432
433fn parse_table1(expand_data: &[u8], cnt: usize) -> Result<Vec<CgTableEntry>> {
434    let entry_size = AVG_CG_TABLE_NAME_LEN + 4;
435    let need = cnt
436        .checked_mul(entry_size)
437        .ok_or_else(|| anyhow!("CGM: size overflow"))?;
438    if expand_data.len() < need {
439        bail!(
440            "CGM: expanded data too short (need={}, got={})",
441            need,
442            expand_data.len()
443        );
444    }
445
446    let mut out = Vec::with_capacity(cnt);
447    let mut off = 0usize;
448    for i in 0..cnt {
449        let name_raw = &expand_data[off..off + AVG_CG_TABLE_NAME_LEN];
450        off += AVG_CG_TABLE_NAME_LEN;
451
452        let flag_no = i32::from_le_bytes(expand_data[off..off + 4].try_into().unwrap());
453        off += 4;
454
455        let mut name = decode_name(name_raw);
456        name = name.to_ascii_uppercase();
457
458        out.push(CgTableEntry {
459            name,
460            flag_no,
461            code_exist_cnt: 0,
462            code: [0; CG_TABLE_DATA_CODE_MAX],
463            list_no: i as i32,
464            group: [-1; CG_TABLE_DATA_CODE_MAX],
465        });
466    }
467    Ok(out)
468}
469
470fn decode_name(name_raw: &[u8]) -> String {
471    let end = name_raw
472        .iter()
473        .position(|&b| b == 0)
474        .unwrap_or(name_raw.len());
475    let (cow, _, _) = SHIFT_JIS.decode(&name_raw[..end]);
476    cow.into_owned()
477}
478
479fn c_string_prefix(buf: &[u8]) -> String {
480    let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
481    let (cow, _, _) = SHIFT_JIS.decode(&buf[..end]);
482    cow.into_owned()
483}
484
485/// `tpc_angou` is XOR with a 256-byte repeating table.
486fn tpc_angou_in_place(src: &mut [u8]) {
487    for (i, b) in src.iter_mut().enumerate() {
488        *b ^= TPC_ANGOU_TABLE[i & 0xFF];
489    }
490}
491
492// Angou XOR table (256 bytes, repeats).
493const TPC_ANGOU_TABLE: [u8; 256] = [
494    0x8b, 0xe5, 0x5d, 0xc3, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x85, 0xc0, 0x74, 0x09, 0x5f, 0x5e, 0x33,
495    0xc0, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x45, 0x0c, 0x85, 0xc0, 0x75, 0x14, 0x8b, 0x55, 0xec,
496    0x83, 0xc2, 0x20, 0x52, 0x6a, 0x00, 0xe8, 0xf5, 0x28, 0x01, 0x00, 0x83, 0xc4, 0x08, 0x89, 0x45,
497    0x0c, 0x8b, 0x45, 0xe4, 0x6a, 0x00, 0x6a, 0x00, 0x50, 0x53, 0xff, 0x15, 0x34, 0xb1, 0x43, 0x00,
498    0x8b, 0x45, 0x10, 0x85, 0xc0, 0x74, 0x05, 0x8b, 0x4d, 0xec, 0x89, 0x08, 0x8a, 0x45, 0xf0, 0x84,
499    0xc0, 0x75, 0x78, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x8b, 0x7d, 0xe8, 0x8b, 0x75, 0x0c, 0x85, 0xc0,
500    0x75, 0x44, 0x8b, 0x1d, 0xd0, 0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x37, 0x81, 0xff, 0x00, 0x00,
501    0x04, 0x00, 0x6a, 0x00, 0x76, 0x43, 0x8b, 0x45, 0xf8, 0x8d, 0x55, 0xfc, 0x52, 0x68, 0x00, 0x00,
502    0x04, 0x00, 0x56, 0x50, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0x6a, 0x05, 0xff, 0xd3, 0xa1, 0xe0,
503    0x30, 0x44, 0x00, 0x81, 0xef, 0x00, 0x00, 0x04, 0x00, 0x81, 0xc6, 0x00, 0x00, 0x04, 0x00, 0x85,
504    0xc0, 0x74, 0xc5, 0x8b, 0x5d, 0xf8, 0x53, 0xe8, 0xf4, 0xfb, 0xff, 0xff, 0x8b, 0x45, 0x0c, 0x83,
505    0xc4, 0x04, 0x5f, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x55, 0xf8, 0x8d, 0x4d, 0xfc, 0x51,
506    0x57, 0x56, 0x52, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0xeb, 0xd8, 0x8b, 0x45, 0xe8, 0x83, 0xc0,
507    0x20, 0x50, 0x6a, 0x00, 0xe8, 0x47, 0x28, 0x01, 0x00, 0x8b, 0x7d, 0xe8, 0x89, 0x45, 0xf4, 0x8b,
508    0xf0, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x83, 0xc4, 0x08, 0x85, 0xc0, 0x75, 0x56, 0x8b, 0x1d, 0xd0,
509    0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x49, 0x81, 0xff, 0x00, 0x00, 0x04, 0x00, 0x6a, 0x00, 0x76,
510];