Skip to main content

siglus_assets/
scene_pck.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use anyhow::{anyhow, bail, Context, Result};
6
7use crate::lzss::lzss_unpack_lenient;
8
9#[derive(Debug, Clone, Copy)]
10pub struct CIndex {
11    pub offset: i32,
12    pub size: i32,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct PackIncProp {
17    pub form: i32,
18    pub size: i32,
19}
20
21impl PackIncProp {
22    pub fn read(buf: &[u8], off: usize) -> Result<Self> {
23        if off + 8 > buf.len() {
24            bail!("scene_pck: PackIncProp out of bounds");
25        }
26        let form = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
27        let size = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
28        Ok(Self { form, size })
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct PackIncCmd {
34    pub scn_no: i32,
35    pub offset: i32,
36}
37
38impl PackIncCmd {
39    pub fn read(buf: &[u8], off: usize) -> Result<Self> {
40        if off + 8 > buf.len() {
41            bail!("scene_pck: PackIncCmd out of bounds");
42        }
43        let scn_no = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
44        let offset = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
45        Ok(Self { scn_no, offset })
46    }
47}
48
49impl CIndex {
50    pub fn read(buf: &[u8], off: usize) -> Result<Self> {
51        if off + 8 > buf.len() {
52            bail!("scene_pck: CIndex out of bounds");
53        }
54        let offset = i32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
55        let size = i32::from_le_bytes(buf[off + 4..off + 8].try_into().unwrap());
56        Ok(Self { offset, size })
57    }
58}
59
60/// All fields are little-endian i32.
61#[derive(Debug, Clone, Copy)]
62pub struct PackScnHeader {
63    pub header_size: i32,
64    pub inc_prop_list_ofs: i32,
65    pub inc_prop_cnt: i32,
66    pub inc_prop_name_index_list_ofs: i32,
67    pub inc_prop_name_index_cnt: i32,
68    pub inc_prop_name_list_ofs: i32,
69    pub inc_prop_name_cnt: i32,
70    pub inc_cmd_list_ofs: i32,
71    pub inc_cmd_cnt: i32,
72    pub inc_cmd_name_index_list_ofs: i32,
73    pub inc_cmd_name_index_cnt: i32,
74    pub inc_cmd_name_list_ofs: i32,
75    pub inc_cmd_name_cnt: i32,
76    pub scn_name_index_list_ofs: i32,
77    pub scn_name_index_cnt: i32,
78    pub scn_name_list_ofs: i32,
79    pub scn_name_cnt: i32,
80    pub scn_data_index_list_ofs: i32,
81    pub scn_data_index_cnt: i32,
82    pub scn_data_list_ofs: i32,
83    pub scn_data_cnt: i32,
84    pub scn_data_exe_angou_mod: i32,
85    pub original_source_header_size: i32,
86}
87
88impl PackScnHeader {
89    pub fn read(buf: &[u8], off: usize, has_signature: bool) -> Result<Self> {
90        // header size is stored in the first i32 (no signature in older builds).
91        let min_need = if has_signature { 8 + 4 } else { 4 };
92        if off + min_need > buf.len() {
93            bail!("scene_pck: header out of bounds");
94        }
95        let mut p = off;
96        if has_signature {
97            if &buf[off..off + 8] != b"pack_scn" {
98                bail!("scene_pck: bad signature (expected pack_scn)");
99            }
100            p += 8;
101        }
102        let mut rd = || {
103            let v = i32::from_le_bytes(buf[p..p + 4].try_into().unwrap());
104            p += 4;
105            v
106        };
107        let header_size = rd();
108        let mut out = Self {
109            header_size,
110            inc_prop_list_ofs: rd(),
111            inc_prop_cnt: rd(),
112            inc_prop_name_index_list_ofs: rd(),
113            inc_prop_name_index_cnt: rd(),
114            inc_prop_name_list_ofs: rd(),
115            inc_prop_name_cnt: rd(),
116            inc_cmd_list_ofs: rd(),
117            inc_cmd_cnt: rd(),
118            inc_cmd_name_index_list_ofs: rd(),
119            inc_cmd_name_index_cnt: rd(),
120            inc_cmd_name_list_ofs: rd(),
121            inc_cmd_name_cnt: rd(),
122            scn_name_index_list_ofs: rd(),
123            scn_name_index_cnt: rd(),
124            scn_name_list_ofs: rd(),
125            scn_name_cnt: rd(),
126            scn_data_index_list_ofs: rd(),
127            scn_data_index_cnt: rd(),
128            scn_data_list_ofs: rd(),
129            scn_data_cnt: rd(),
130            scn_data_exe_angou_mod: rd(),
131            original_source_header_size: rd(),
132        };
133
134        // Optional extra fields in newer headers (ignored for now).
135        let header_bytes = header_size.max(0) as usize;
136        let base_fields_bytes = 23 * 4;
137        let extra_bytes = header_bytes.saturating_sub(base_fields_bytes);
138        let extra_fields = extra_bytes / 4;
139        if extra_fields > 0 {
140            for _ in 0..extra_fields {
141                let _ = rd();
142            }
143        }
144
145        Ok(out)
146    }
147}
148
149#[derive(Debug, Clone)]
150pub struct ScenePckDecodeOptions {
151    /// Optional 16-byte exe angou element table (`TNM_EXE_ANGOU_ELEMENT_CNT`).
152    pub exe_angou_element: Option<Vec<u8>>,
153    /// Optional easy angou code table (`TNM_EASY_ANGOU_CODE_SIZE`, typically 256).
154    pub easy_angou_code: Option<Vec<u8>>,
155}
156
157impl Default for ScenePckDecodeOptions {
158    fn default() -> Self {
159        Self {
160            exe_angou_element: None,
161            easy_angou_code: None,
162        }
163    }
164}
165
166impl ScenePckDecodeOptions {
167    pub fn from_project_dir(project_dir: &Path) -> Result<Self> {
168        let exe = crate::key_toml::load_key16_from_project_dir(project_dir)?.map(|v| v.to_vec());
169        Ok(Self {
170            exe_angou_element: exe,
171            easy_angou_code: Some(crate::keys::SCENE_KEY.to_vec()),
172        })
173    }
174}
175
176#[derive(Debug, Clone)]
177pub struct ScenePck {
178    pub buf: Vec<u8>,
179    pub header: PackScnHeader,
180    pub scn_name_map: HashMap<String, usize>,
181    pub inc_prop_name_map: HashMap<u32, String>,
182    pub inc_cmd_name_map: HashMap<u32, String>,
183    pub inc_props: Vec<PackIncProp>,
184    pub inc_cmds: Vec<PackIncCmd>,
185}
186
187fn read_pack_inc_props(buf: &[u8], list_ofs: usize, count: usize) -> Result<Vec<PackIncProp>> {
188    let mut out = Vec::new();
189    if count == 0 {
190        return Ok(out);
191    }
192    let byte_len = count
193        .checked_mul(8)
194        .ok_or_else(|| anyhow!("scene_pck: inc_prop_list size overflow"))?;
195    let end = list_ofs
196        .checked_add(byte_len)
197        .ok_or_else(|| anyhow!("scene_pck: inc_prop_list offset overflow"))?;
198    if end > buf.len() {
199        bail!("scene_pck: inc_prop_list out of bounds");
200    }
201    out.reserve(count);
202    for i in 0..count {
203        out.push(PackIncProp::read(buf, list_ofs + i * 8)?);
204    }
205    Ok(out)
206}
207
208fn read_pack_inc_cmds(buf: &[u8], list_ofs: usize, count: usize) -> Result<Vec<PackIncCmd>> {
209    let mut out = Vec::new();
210    if count == 0 {
211        return Ok(out);
212    }
213    let byte_len = count
214        .checked_mul(8)
215        .ok_or_else(|| anyhow!("scene_pck: inc_cmd_list size overflow"))?;
216    let end = list_ofs
217        .checked_add(byte_len)
218        .ok_or_else(|| anyhow!("scene_pck: inc_cmd_list offset overflow"))?;
219    if end > buf.len() {
220        bail!("scene_pck: inc_cmd_list out of bounds");
221    }
222    out.reserve(count);
223    for i in 0..count {
224        out.push(PackIncCmd::read(buf, list_ofs + i * 8)?);
225    }
226    Ok(out)
227}
228
229fn read_indexed_utf16_name_map(
230    buf: &[u8],
231    index_list_ofs: usize,
232    count: usize,
233    list_ofs: usize,
234) -> Result<HashMap<u32, String>> {
235    let mut out = HashMap::new();
236    if index_list_ofs + count * 8 > buf.len() || list_ofs > buf.len() {
237        return Ok(out);
238    }
239    for i in 0..count {
240        let idx = CIndex::read(buf, index_list_ofs + i * 8)?;
241        let o = idx.offset.max(0) as usize;
242        let n = idx.size.max(0) as usize;
243        let byte_off = list_ofs
244            .checked_add(o * 2)
245            .ok_or_else(|| anyhow!("scene_pck: name offset overflow"))?;
246        let byte_end = byte_off
247            .checked_add(n * 2)
248            .ok_or_else(|| anyhow!("scene_pck: name size overflow"))?;
249        if byte_end > buf.len() {
250            continue;
251        }
252        let mut u16s = Vec::with_capacity(n);
253        for j in 0..n {
254            let p = byte_off + j * 2;
255            let w = u16::from_le_bytes([buf[p], buf[p + 1]]);
256            if w == 0 {
257                break;
258            }
259            u16s.push(w);
260        }
261        let s = String::from_utf16_lossy(&u16s);
262        if !s.is_empty() {
263            out.insert(i as u32, s);
264        }
265    }
266    Ok(out)
267}
268
269impl ScenePck {
270    pub fn load_and_rebuild(path: &Path, opt: &ScenePckDecodeOptions) -> Result<Self> {
271        let tmp = fs::read(path).with_context(|| format!("read {}", path.display()))?;
272        Self::load_and_rebuild_from_bytes(tmp, opt)
273    }
274
275    pub fn load_and_rebuild_from_bytes(mut tmp: Vec<u8>, opt: &ScenePckDecodeOptions) -> Result<Self> {
276        if tmp.len() < 4 {
277            bail!("scene_pck: file too short");
278        }
279        let has_signature = tmp.len() >= 8 && &tmp[0..8] == b"pack_scn";
280        let header = PackScnHeader::read(&tmp, 0, has_signature)?;
281        let scn_data_list_ofs = header.scn_data_list_ofs as usize;
282        if scn_data_list_ofs > tmp.len() {
283            bail!("scene_pck: scn_data_list_ofs out of bounds");
284        }
285
286        // Rebuild m_scn_data exactly like the original implementation: keep everything before scn_data_list_ofs,
287        // then append decrypted/decompressed scene chunks contiguously.
288        let mut out = tmp[..scn_data_list_ofs].to_vec();
289
290        // Load original index list from the input.
291        let idx_ofs = header.scn_data_index_list_ofs as usize;
292        let scn_cnt = if header.scn_data_cnt > 0 {
293            header.scn_data_cnt as usize
294        } else {
295            header.scn_data_index_cnt.max(0) as usize
296        };
297        if idx_ofs + scn_cnt * 8 > tmp.len() {
298            bail!("scene_pck: scn_data_index_list out of bounds");
299        }
300        let mut idx_list: Vec<CIndex> = Vec::with_capacity(scn_cnt);
301        for i in 0..scn_cnt {
302            idx_list.push(CIndex::read(&tmp, idx_ofs + i * 8)?);
303        }
304
305        let mut offset = idx_list
306            .get(0)
307            .map(|x| x.offset.max(0) as usize)
308            .unwrap_or(0);
309        if out.len() < scn_data_list_ofs + offset {
310            out.resize(scn_data_list_ofs + offset, 0);
311        }
312
313        for scn_no in 0..scn_cnt {
314            let entry = idx_list[scn_no];
315            let mut new_size = 0usize;
316
317            if entry.size > 0 {
318                let sp_off = scn_data_list_ofs
319                    .checked_add(entry.offset.max(0) as usize)
320                    .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
321                let sp_end = sp_off
322                    .checked_add(entry.size as usize)
323                    .ok_or_else(|| anyhow!("scene_pck: size overflow"))?;
324                if sp_end > tmp.len() {
325                    bail!(
326                        "scene_pck: scn chunk out of bounds (scn_no={}, end={}, len={})",
327                        scn_no,
328                        sp_end,
329                        tmp.len()
330                    );
331                }
332
333                let chunk = &mut tmp[sp_off..sp_end];
334
335                let out_chunk: Vec<u8>;
336                if header.original_source_header_size > 0 {
337                    // exe angou element XOR (optional)
338                    if header.scn_data_exe_angou_mod != 0 {
339                        if let Some(exe_el) = opt.exe_angou_element.as_deref() {
340                            if exe_el.is_empty() {
341                                // nothing
342                            } else {
343                                let mut eac = 0usize;
344                                for b in chunk.iter_mut() {
345                                    *b ^= exe_el[eac];
346                                    eac += 1;
347                                    if eac >= exe_el.len() {
348                                        eac = 0;
349                                    }
350                                }
351                            }
352                        }
353                    }
354
355                    // easy angou XOR (optional)
356                    if let Some(easy) = opt.easy_angou_code.as_deref() {
357                        if !easy.is_empty() {
358                            let mut eac = 0usize;
359                            for b in chunk.iter_mut() {
360                                *b ^= easy[eac];
361                                eac += 1;
362                                if eac >= easy.len() {
363                                    eac = 0;
364                                }
365                            }
366                        }
367                    }
368
369                    out_chunk = lzss_unpack_lenient(chunk)
370                        .with_context(|| format!("scene_pck: lzss unpack scn_no={}", scn_no))?;
371                } else {
372                    // Easy-link mode: keep the chunk bytes as-is.
373                    out_chunk = chunk.to_vec();
374                }
375
376                new_size = out_chunk.len();
377                let dst_off = scn_data_list_ofs + offset;
378                let need_len = dst_off
379                    .checked_add(new_size)
380                    .ok_or_else(|| anyhow!("scene_pck: out size overflow"))?;
381                if out.len() < need_len {
382                    out.resize(need_len, 0);
383                }
384                out[dst_off..dst_off + new_size].copy_from_slice(&out_chunk);
385            }
386
387            // Patch the index list inside the output buffer.
388            let out_idx_ofs = header.scn_data_index_list_ofs as usize;
389            let out_entry_ofs = out_idx_ofs + scn_no * 8;
390            if out_entry_ofs + 8 > out.len() {
391                bail!("scene_pck: output index list out of bounds");
392            }
393            out[out_entry_ofs..out_entry_ofs + 4].copy_from_slice(&(offset as i32).to_le_bytes());
394            out[out_entry_ofs + 4..out_entry_ofs + 8]
395                .copy_from_slice(&(new_size as i32).to_le_bytes());
396
397            offset = offset
398                .checked_add(new_size)
399                .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
400        }
401
402        // Build name map.
403        let mut scn_name_map = HashMap::new();
404        let name_idx_ofs = header.scn_name_index_list_ofs as usize;
405        let name_cnt = header.scn_name_cnt.max(0) as usize;
406        let name_list_ofs = header.scn_name_list_ofs as usize;
407        if name_idx_ofs + name_cnt * 8 <= out.len() && name_list_ofs <= out.len() {
408            for i in 0..name_cnt {
409                let idx = CIndex::read(&out, name_idx_ofs + i * 8)?;
410                let o = idx.offset.max(0) as usize;
411                let n = idx.size.max(0) as usize;
412                let byte_off = name_list_ofs
413                    .checked_add(o * 2)
414                    .ok_or_else(|| anyhow!("scene_pck: name offset overflow"))?;
415                let byte_end = byte_off
416                    .checked_add(n * 2)
417                    .ok_or_else(|| anyhow!("scene_pck: name size overflow"))?;
418                if byte_end > out.len() {
419                    continue;
420                }
421                let mut u16s = Vec::with_capacity(n);
422                for j in 0..n {
423                    let p = byte_off + j * 2;
424                    let w = u16::from_le_bytes([out[p], out[p + 1]]);
425                    if w == 0 {
426                        break;
427                    }
428                    u16s.push(w);
429                }
430                let s = String::from_utf16_lossy(&u16s);
431                if !s.is_empty() {
432                    scn_name_map.insert(s, i);
433                }
434            }
435        }
436
437        let inc_prop_name_map = read_indexed_utf16_name_map(
438            &out,
439            header.inc_prop_name_index_list_ofs.max(0) as usize,
440            header.inc_prop_name_cnt.max(0) as usize,
441            header.inc_prop_name_list_ofs.max(0) as usize,
442        )?;
443        let inc_cmd_name_map = read_indexed_utf16_name_map(
444            &out,
445            header.inc_cmd_name_index_list_ofs.max(0) as usize,
446            header.inc_cmd_name_cnt.max(0) as usize,
447            header.inc_cmd_name_list_ofs.max(0) as usize,
448        )?;
449        let inc_props = read_pack_inc_props(
450            &out,
451            header.inc_prop_list_ofs.max(0) as usize,
452            header.inc_prop_cnt.max(0) as usize,
453        )?;
454        let inc_cmds = read_pack_inc_cmds(
455            &out,
456            header.inc_cmd_list_ofs.max(0) as usize,
457            header.inc_cmd_cnt.max(0) as usize,
458        )?;
459
460        Ok(Self {
461            buf: out,
462            header,
463            scn_name_map,
464            inc_prop_name_map,
465            inc_cmd_name_map,
466            inc_props,
467            inc_cmds,
468        })
469    }
470
471    pub fn scn_data_slice(&self, scn_no: usize) -> Result<&[u8]> {
472        let scn_cnt = self.header.scn_data_cnt.max(0) as usize;
473        if scn_no >= scn_cnt {
474            bail!("scene_pck: scn_no out of range");
475        }
476        let idx_ofs = self.header.scn_data_index_list_ofs as usize;
477        let entry = CIndex::read(&self.buf, idx_ofs + scn_no * 8)?;
478        if entry.size <= 0 {
479            return Ok(&[]);
480        }
481        let base = self.header.scn_data_list_ofs as usize;
482        let off = base
483            .checked_add(entry.offset.max(0) as usize)
484            .ok_or_else(|| anyhow!("scene_pck: offset overflow"))?;
485        let end = off
486            .checked_add(entry.size as usize)
487            .ok_or_else(|| anyhow!("scene_pck: size overflow"))?;
488        if end > self.buf.len() {
489            bail!("scene_pck: scn slice out of bounds");
490        }
491        Ok(&self.buf[off..end])
492    }
493
494    pub fn find_scene_no(&self, name_or_index: &str) -> Option<usize> {
495        if let Ok(i) = name_or_index.parse::<usize>() {
496            return Some(i);
497        }
498        self.scn_name_map.get(name_or_index).copied()
499    }
500
501    pub fn find_scene_name(&self, scn_no: usize) -> Option<&str> {
502        self.scn_name_map.iter().find_map(|(name, no)| {
503            if *no == scn_no {
504                Some(name.as_str())
505            } else {
506                None
507            }
508        })
509    }
510
511    pub fn find_inc_cmd_no(&self, cmd_name: &str) -> Option<usize> {
512        self.inc_cmd_name_map.iter().find_map(|(no, name)| {
513            if name.eq_ignore_ascii_case(cmd_name) {
514                Some(*no as usize)
515            } else {
516                None
517            }
518        })
519    }
520}
521
522/// Helper for typical game directory layout.
523pub fn find_scene_pck_in_project(project_dir: &Path) -> Result<std::path::PathBuf> {
524    let candidates = [
525        project_dir.join("Scene.pck"),
526        project_dir.join("scene.pck"),
527        project_dir.join("Data").join("Scene.pck"),
528        project_dir.join("data").join("Scene.pck"),
529    ];
530    for p in candidates {
531        if p.is_file() {
532            return Ok(p);
533        }
534    }
535    bail!(
536        "scene_pck: Scene.pck not found under {}",
537        project_dir.display()
538    );
539}