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#[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 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 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 pub exe_angou_element: Option<Vec<u8>>,
153 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 let mut out = tmp[..scn_data_list_ofs].to_vec();
289
290 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 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 } 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 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 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 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 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
522pub 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}