1use anyhow::{Context, Result, anyhow, bail};
2use std::fs;
3#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6
7use crate::runtime::globals::SaveSlotState;
8
9pub const SAVE_APPEND_DIR_MAX_LEN: usize = 256;
10pub const SAVE_APPEND_NAME_MAX_LEN: usize = 256;
11pub const SAVE_TITLE_MAX_LEN: usize = 256;
12pub const SAVE_MESSAGE_MAX_LEN: usize = 256;
13pub const SAVE_FULL_MESSAGE_MAX_LEN: usize = 256;
14pub const SAVE_COMMENT_MAX_LEN: usize = 256;
15pub const SAVE_COMMENT2_MAX_LEN: usize = 256;
16pub const SAVE_FLAG_MAX_CNT: usize = 256;
17
18const SAVE_FIXED_STRING_CNT: usize = 7;
19pub const SAVE_HEADER_SIZE: usize =
20 10 * 4 + SAVE_FIXED_STRING_CNT * 256 * 2 + SAVE_FLAG_MAX_CNT * 4 + 4;
21const LEGACY_SAVE_HEADER_SIZE: usize = SAVE_HEADER_SIZE - 3 * 256 * 2;
22const SPLIT_SAVE_HEADER_SIZE: usize = LEGACY_SAVE_HEADER_SIZE + 4;
23pub const GLOBAL_SAVE_HEADER_SIZE: usize = 12;
24pub const CONFIG_SAVE_HEADER_SIZE: usize = 12;
25pub const READ_SAVE_HEADER_SIZE: usize = 16;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SaveKind {
29 Normal,
30 Quick,
31 End,
32}
33
34#[derive(Debug, Clone, Copy)]
35enum LocalSaveLayout {
36 CurrentEnvelope,
37 LegacyEnvelope,
38 LegacySplit { local_ex_data_size: i32 },
39}
40
41impl LocalSaveLayout {
42 fn is_legacy(self) -> bool {
43 !matches!(self, Self::CurrentEnvelope)
44 }
45
46 fn header_size(self) -> usize {
47 match self {
48 Self::CurrentEnvelope => SAVE_HEADER_SIZE,
49 Self::LegacyEnvelope => LEGACY_SAVE_HEADER_SIZE,
50 Self::LegacySplit { .. } => SPLIT_SAVE_HEADER_SIZE,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
56pub struct OriginalSaveHeader {
57 layout: LocalSaveLayout,
58 pub major_version: i32,
59 pub minor_version: i32,
60 pub year: i32,
61 pub month: i32,
62 pub day: i32,
63 pub weekday: i32,
64 pub hour: i32,
65 pub minute: i32,
66 pub second: i32,
67 pub millisecond: i32,
68 pub append_dir: String,
69 pub append_name: String,
70 pub title: String,
71 pub message: String,
72 pub full_message: String,
73 pub comment: String,
74 pub comment2: String,
75 pub flag: [i32; SAVE_FLAG_MAX_CNT],
76 pub data_size: i32,
77}
78
79impl Default for OriginalSaveHeader {
80 fn default() -> Self {
81 Self {
82 layout: LocalSaveLayout::CurrentEnvelope,
83 major_version: 1,
84 minor_version: 0,
85 year: 0,
86 month: 0,
87 day: 0,
88 weekday: 0,
89 hour: 0,
90 minute: 0,
91 second: 0,
92 millisecond: 0,
93 append_dir: String::new(),
94 append_name: String::new(),
95 title: String::new(),
96 message: String::new(),
97 full_message: String::new(),
98 comment: String::new(),
99 comment2: String::new(),
100 flag: [0; SAVE_FLAG_MAX_CNT],
101 data_size: 0,
102 }
103 }
104}
105
106impl OriginalSaveHeader {
107 pub fn from_slot(slot: &SaveSlotState, packed_size: usize) -> Self {
108 let mut flag = [0i32; SAVE_FLAG_MAX_CNT];
109 for (idx, dst) in flag.iter_mut().enumerate() {
110 *dst = slot.values.get(&(idx as i32)).copied().unwrap_or(0) as i32;
111 }
112 Self {
113 layout: LocalSaveLayout::CurrentEnvelope,
114 major_version: 1,
115 minor_version: 0,
116 year: slot.year as i32,
117 month: slot.month as i32,
118 day: slot.day as i32,
119 weekday: slot.weekday as i32,
120 hour: slot.hour as i32,
121 minute: slot.minute as i32,
122 second: slot.second as i32,
123 millisecond: slot.millisecond as i32,
124 append_dir: slot.append_dir.clone(),
125 append_name: slot.append_name.clone(),
126 title: slot.title.clone(),
127 message: slot.message.clone(),
128 full_message: slot.full_message.clone(),
129 comment: slot.comment.clone(),
130 comment2: slot.comment2.clone(),
131 flag,
132 data_size: packed_size as i32,
133 }
134 }
135
136 pub fn to_slot(&self) -> SaveSlotState {
137 let mut slot = SaveSlotState {
138 header_cache_valid: true,
139 exist: self.major_version == 1 && self.minor_version == 0,
140 year: self.year as i64,
141 month: self.month as i64,
142 day: self.day as i64,
143 weekday: self.weekday as i64,
144 hour: self.hour as i64,
145 minute: self.minute as i64,
146 second: self.second as i64,
147 millisecond: self.millisecond as i64,
148 append_dir: self.append_dir.clone(),
149 append_name: self.append_name.clone(),
150 title: self.title.clone(),
151 message: self.message.clone(),
152 full_message: self.full_message.clone(),
153 comment: self.comment.clone(),
154 comment2: self.comment2.clone(),
155 packed_data_size: self.data_size.max(0) as usize,
156 ..Default::default()
157 };
158
159 for (idx, value) in self.flag.iter().enumerate() {
160 if *value != 0 {
161 slot.values.insert(idx as i32, *value as i64);
162 }
163 }
164 slot
165 }
166
167 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
168 let header_size = match bytes.len() {
169 LEGACY_SAVE_HEADER_SIZE => LEGACY_SAVE_HEADER_SIZE,
170 SPLIT_SAVE_HEADER_SIZE => SPLIT_SAVE_HEADER_SIZE,
171 _ => SAVE_HEADER_SIZE,
172 };
173 let legacy_header = header_size != SAVE_HEADER_SIZE;
174 if bytes.len() < header_size {
175 bail!("save header too short: {} < {}", bytes.len(), header_size);
176 }
177 let mut rd = Reader::new(bytes);
178 let major_version = rd.i32()?;
179 let minor_version = rd.i32()?;
180 let year = rd.i32()?;
181 let month = rd.i32()?;
182 let day = rd.i32()?;
183 let weekday = rd.i32()?;
184 let hour = rd.i32()?;
185 let minute = rd.i32()?;
186 let second = rd.i32()?;
187 let millisecond = rd.i32()?;
188 let append_dir = if legacy_header {
189 String::new()
190 } else {
191 rd.utf16_fixed(SAVE_APPEND_DIR_MAX_LEN)?
192 };
193 let append_name = if legacy_header {
194 String::new()
195 } else {
196 rd.utf16_fixed(SAVE_APPEND_NAME_MAX_LEN)?
197 };
198 let title = rd.utf16_fixed(SAVE_TITLE_MAX_LEN)?;
199 let message = rd.utf16_fixed(SAVE_MESSAGE_MAX_LEN)?;
200 let full_message = if legacy_header {
201 message.clone()
202 } else {
203 rd.utf16_fixed(SAVE_FULL_MESSAGE_MAX_LEN)?
204 };
205 let comment = rd.utf16_fixed(SAVE_COMMENT_MAX_LEN)?;
206 let comment2 = rd.utf16_fixed(SAVE_COMMENT2_MAX_LEN)?;
207 let mut flag = [0i32; SAVE_FLAG_MAX_CNT];
208 for dst in &mut flag {
209 *dst = rd.i32()?;
210 }
211 let data_size = rd.i32()?;
212 let layout = match header_size {
213 SPLIT_SAVE_HEADER_SIZE => LocalSaveLayout::LegacySplit {
214 local_ex_data_size: rd.i32()?,
215 },
216 LEGACY_SAVE_HEADER_SIZE => LocalSaveLayout::LegacyEnvelope,
217 _ => LocalSaveLayout::CurrentEnvelope,
218 };
219 Ok(Self {
220 layout,
221 major_version,
222 minor_version,
223 year,
224 month,
225 day,
226 weekday,
227 hour,
228 minute,
229 second,
230 millisecond,
231 append_dir,
232 append_name,
233 title,
234 message,
235 full_message,
236 comment,
237 comment2,
238 flag,
239 data_size,
240 })
241 }
242
243 pub fn to_bytes(&self) -> Vec<u8> {
244 let mut out = Vec::with_capacity(self.header_size());
245 push_i32(&mut out, self.major_version);
246 push_i32(&mut out, self.minor_version);
247 push_i32(&mut out, self.year);
248 push_i32(&mut out, self.month);
249 push_i32(&mut out, self.day);
250 push_i32(&mut out, self.weekday);
251 push_i32(&mut out, self.hour);
252 push_i32(&mut out, self.minute);
253 push_i32(&mut out, self.second);
254 push_i32(&mut out, self.millisecond);
255 if !self.layout.is_legacy() {
256 push_utf16_fixed(&mut out, &self.append_dir, SAVE_APPEND_DIR_MAX_LEN);
257 push_utf16_fixed(&mut out, &self.append_name, SAVE_APPEND_NAME_MAX_LEN);
258 }
259 push_utf16_fixed(&mut out, &self.title, SAVE_TITLE_MAX_LEN);
260 push_utf16_fixed(&mut out, &self.message, SAVE_MESSAGE_MAX_LEN);
261 if !self.layout.is_legacy() {
262 push_utf16_fixed(&mut out, &self.full_message, SAVE_FULL_MESSAGE_MAX_LEN);
263 }
264 push_utf16_fixed(&mut out, &self.comment, SAVE_COMMENT_MAX_LEN);
265 push_utf16_fixed(&mut out, &self.comment2, SAVE_COMMENT2_MAX_LEN);
266 for v in &self.flag {
267 push_i32(&mut out, *v);
268 }
269 push_i32(&mut out, self.data_size);
270 if let LocalSaveLayout::LegacySplit { local_ex_data_size } = self.layout {
271 push_i32(&mut out, local_ex_data_size);
272 }
273 debug_assert_eq!(out.len(), self.header_size());
274 out
275 }
276
277 fn header_size(&self) -> usize {
278 self.layout.header_size()
279 }
280
281 fn from_file_prefix(bytes: &[u8], file_len: usize) -> Result<Self> {
282 for size in [
285 SAVE_HEADER_SIZE,
286 LEGACY_SAVE_HEADER_SIZE,
287 SPLIT_SAVE_HEADER_SIZE,
288 ] {
289 if bytes.len() < size {
290 continue;
291 }
292 let header = Self::from_bytes(&bytes[..size])?;
293 let extra = match header.layout {
294 LocalSaveLayout::LegacySplit { local_ex_data_size } => local_ex_data_size,
295 _ => 0,
296 };
297 if header.data_size < 0 || extra < 0 {
298 continue;
299 }
300 let end = size
301 .checked_add(header.data_size as usize)
302 .and_then(|end| end.checked_add(extra as usize));
303 if end == Some(file_len) {
304 return Ok(header);
305 }
306 }
307 bail!("unrecognized or truncated local save layout: file size {file_len}")
308 }
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub struct OriginalGlobalSaveHeader {
313 pub major_version: i32,
314 pub minor_version: i32,
315 pub global_data_size: i32,
316}
317
318impl OriginalGlobalSaveHeader {
319 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
320 if bytes.len() < GLOBAL_SAVE_HEADER_SIZE {
321 bail!(
322 "global save header too short: {} < {}",
323 bytes.len(),
324 GLOBAL_SAVE_HEADER_SIZE
325 );
326 }
327 let mut rd = Reader::new(bytes);
328 Ok(Self {
329 major_version: rd.i32()?,
330 minor_version: rd.i32()?,
331 global_data_size: rd.i32()?,
332 })
333 }
334
335 pub fn to_bytes(&self) -> Vec<u8> {
336 let mut out = Vec::with_capacity(GLOBAL_SAVE_HEADER_SIZE);
337 push_i32(&mut out, self.major_version);
338 push_i32(&mut out, self.minor_version);
339 push_i32(&mut out, self.global_data_size);
340 out
341 }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub struct OriginalConfigSaveHeader {
346 pub major_version: i32,
347 pub minor_version: i32,
348 pub config_data_size: i32,
349}
350
351impl OriginalConfigSaveHeader {
352 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
353 if bytes.len() < CONFIG_SAVE_HEADER_SIZE {
354 bail!(
355 "config save header too short: {} < {}",
356 bytes.len(),
357 CONFIG_SAVE_HEADER_SIZE
358 );
359 }
360 let mut rd = Reader::new(bytes);
361 Ok(Self {
362 major_version: rd.i32()?,
363 minor_version: rd.i32()?,
364 config_data_size: rd.i32()?,
365 })
366 }
367
368 pub fn to_bytes(&self) -> Vec<u8> {
369 let mut out = Vec::with_capacity(CONFIG_SAVE_HEADER_SIZE);
370 push_i32(&mut out, self.major_version);
371 push_i32(&mut out, self.minor_version);
372 push_i32(&mut out, self.config_data_size);
373 out
374 }
375}
376
377#[derive(Debug, Clone)]
378pub struct OriginalLocalSaveEnvelope {
379 pub save_id: [u16; 7],
380 pub append_dir: String,
381 pub append_name: String,
382 pub title: String,
383 pub message: String,
384 pub full_message: String,
385 pub local_stream: Vec<u8>,
386 pub local_ex_stream: Vec<u8>,
387 pub sel_saves: Vec<OriginalLocalSaveEnvelope>,
388}
389
390impl OriginalLocalSaveEnvelope {
391 pub fn from_slot_with_streams(
392 slot: &SaveSlotState,
393 local_stream: Vec<u8>,
394 local_ex_stream: Vec<u8>,
395 ) -> Self {
396 Self {
397 save_id: save_id_from_slot(slot),
398 append_dir: slot.append_dir.clone(),
399 append_name: slot.append_name.clone(),
400 title: slot.title.clone(),
401 message: slot.message.clone(),
402 full_message: slot.full_message.clone(),
403 local_stream,
404 local_ex_stream,
405 sel_saves: Vec::new(),
406 }
407 }
408
409 pub fn empty_from_slot(slot: &SaveSlotState) -> Self {
410 Self::from_slot_with_streams(slot, Vec::new(), Vec::new())
411 }
412
413 pub fn to_bytes(&self) -> Vec<u8> {
414 let mut out = Vec::new();
415 write_envelope(&mut out, self, true);
416 out
417 }
418
419 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
420 let mut rd = Reader::new(bytes);
421 read_envelope(&mut rd, true, false)
422 }
423}
424
425fn save_id_from_slot(slot: &SaveSlotState) -> [u16; 7] {
426 fn w(v: i64) -> u16 {
427 v.clamp(0, u16::MAX as i64) as u16
428 }
429 [
430 w(slot.year),
431 w(slot.month),
432 w(slot.day),
433 w(slot.hour),
434 w(slot.minute),
435 w(slot.second),
436 w(slot.millisecond),
437 ]
438}
439
440fn write_envelope(out: &mut Vec<u8>, env: &OriginalLocalSaveEnvelope, include_sel_saves: bool) {
441 for v in env.save_id {
442 push_u16(out, v);
443 }
444 push_str_len(out, &env.append_dir);
445 push_str_len(out, &env.append_name);
446 push_str_len(out, &env.title);
447 push_str_len(out, &env.message);
448 push_str_len(out, &env.full_message);
449 push_i32(out, env.local_stream.len() as i32);
450 out.extend_from_slice(&env.local_stream);
451 push_i32(out, env.local_ex_stream.len() as i32);
452 out.extend_from_slice(&env.local_ex_stream);
453 if include_sel_saves {
454 push_i32(out, env.sel_saves.len() as i32);
455 for child in &env.sel_saves {
456 write_envelope(out, child, false);
457 }
458 }
459}
460
461fn read_envelope(
462 rd: &mut Reader<'_>,
463 include_sel_saves: bool,
464 legacy: bool,
465) -> Result<OriginalLocalSaveEnvelope> {
466 let mut save_id = [0u16; 7];
467 for dst in &mut save_id {
468 *dst = rd.u16()?;
469 }
470 let (append_dir, append_name, title, message, full_message) = if legacy {
471 Default::default()
472 } else {
473 (
474 rd.str_len()?,
475 rd.str_len()?,
476 rd.str_len()?,
477 rd.str_len()?,
478 rd.str_len()?,
479 )
480 };
481 let local_size = rd.i32()?.max(0) as usize;
482 let local_stream = rd.take(local_size)?.to_vec();
483 let local_ex_size = rd.i32()?.max(0) as usize;
484 let local_ex_stream = rd.take(local_ex_size)?.to_vec();
485 let mut sel_saves = Vec::new();
486 if include_sel_saves {
487 let sel_save_cnt = rd.i32()?.max(0) as usize;
488 for _ in 0..sel_save_cnt {
489 sel_saves.push(read_envelope(rd, false, legacy)?);
490 }
491 }
492 Ok(OriginalLocalSaveEnvelope {
493 save_id,
494 append_dir,
495 append_name,
496 title,
497 message,
498 full_message,
499 local_stream,
500 local_ex_stream,
501 sel_saves,
502 })
503}
504
505pub struct OriginalStreamWriter {
506 data: Vec<u8>,
507}
508
509impl Default for OriginalStreamWriter {
510 fn default() -> Self {
511 Self::new()
512 }
513}
514
515impl OriginalStreamWriter {
516 pub fn new() -> Self {
517 Self { data: Vec::new() }
518 }
519
520 pub fn into_inner(self) -> Vec<u8> {
521 self.data
522 }
523
524 pub fn push_i32(&mut self, v: i32) {
525 push_i32(&mut self.data, v);
526 }
527
528 pub fn push_i64(&mut self, v: i64) {
529 push_i64(&mut self.data, v);
530 }
531
532 pub fn push_u32(&mut self, v: u32) {
533 push_u32(&mut self.data, v);
534 }
535
536 pub fn push_u16(&mut self, v: u16) {
537 push_u16(&mut self.data, v);
538 }
539
540 pub fn push_raw(&mut self, bytes: &[u8]) {
541 self.data.extend_from_slice(bytes);
542 }
543
544 pub fn position(&self) -> usize {
545 self.data.len()
546 }
547
548 pub fn push_bool(&mut self, v: bool) {
549 self.data.push(if v { 1 } else { 0 });
550 }
551
552 pub fn push_padding(&mut self, n: usize) {
553 self.data.resize(self.data.len() + n, 0);
554 }
555
556 pub fn push_element(&mut self, codes: &[i32]) {
557 for idx in 0..31 {
558 push_i32(&mut self.data, codes.get(idx).copied().unwrap_or(0));
559 }
560 push_i32(&mut self.data, codes.len().min(31) as i32);
561 }
562
563 pub fn push_empty_element(&mut self) {
564 self.push_element(&[]);
565 }
566
567 pub fn push_empty_proc(&mut self) {
568 self.push_i32(0);
569 self.push_empty_element();
570 self.push_i32(0);
571 self.push_i32(0);
572 self.push_bool(false);
573 self.push_bool(false);
574 self.push_bool(false);
575 self.push_i32(0);
576 }
577
578 pub fn push_len_bytes(&mut self, bytes: &[u8]) {
579 push_i32(&mut self.data, bytes.len() as i32);
580 self.data.extend_from_slice(bytes);
581 }
582
583 pub fn push_str(&mut self, s: &str) {
584 push_str_len(&mut self.data, s);
585 }
586
587 pub fn push_fixed_i32_list(&mut self, values: &[i64], fixed_len: usize) {
588 let jump_pos = self.data.len();
589 push_i32(&mut self.data, 0);
590 push_i32(&mut self.data, fixed_len as i32);
591 for idx in 0..fixed_len {
592 push_i32(&mut self.data, values.get(idx).copied().unwrap_or(0) as i32);
593 }
594 let end = self.data.len() as i32;
595 patch_i32(&mut self.data, jump_pos, end);
596 }
597
598 pub fn push_fixed_str_list(&mut self, values: &[String], fixed_len: usize) {
599 let jump_pos = self.data.len();
600 push_i32(&mut self.data, 0);
601 push_i32(&mut self.data, fixed_len as i32);
602 for idx in 0..fixed_len {
603 push_str_len(
604 &mut self.data,
605 values.get(idx).map(String::as_str).unwrap_or(""),
606 );
607 }
608 let end = self.data.len() as i32;
609 patch_i32(&mut self.data, jump_pos, end);
610 }
611
612 pub fn push_extend_i32_list(&mut self, values: &[i64]) {
613 push_i32(&mut self.data, values.len() as i32);
614 for v in values {
615 push_i32(&mut self.data, *v as i32);
616 }
617 }
618
619 pub fn push_extend_str_list(&mut self, values: &[String]) {
620 push_i32(&mut self.data, values.len() as i32);
621 for v in values {
622 push_str_len(&mut self.data, v);
623 }
624 }
625
626 pub fn push_tid(&mut self, tid: &[u16; 7]) {
627 for v in tid {
628 push_u16(&mut self.data, *v);
629 }
630 }
631
632 pub fn push_empty_fixed_array(&mut self) {
633 let jump_pos = self.data.len();
634 push_i32(&mut self.data, 0);
635 push_i32(&mut self.data, 0);
636 let end = self.data.len() as i32;
637 patch_i32(&mut self.data, jump_pos, end);
638 }
639
640 pub fn push_fixed_items<T, F>(&mut self, values: &[T], mut write_one: F)
641 where
642 F: FnMut(&mut OriginalStreamWriter, &T),
643 {
644 let jump_pos = self.data.len();
645 push_i32(&mut self.data, 0);
646 push_i32(&mut self.data, values.len() as i32);
647 for value in values {
648 write_one(self, value);
649 }
650 let end = self.data.len() as i32;
651 patch_i32(&mut self.data, jump_pos, end);
652 }
653
654 pub fn push_extend_items<T, F>(&mut self, values: &[T], mut write_one: F)
655 where
656 F: FnMut(&mut OriginalStreamWriter, &T),
657 {
658 push_i32(&mut self.data, values.len() as i32);
659 for value in values {
660 write_one(self, value);
661 }
662 }
663
664 pub fn push_tid_zero(&mut self) {
665 self.push_padding(14);
666 }
667}
668
669#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
672pub(crate) enum NativeLocalLayout {
673 #[default]
674 Current,
675 Legacy,
676 Early,
677 Indexed,
678 ShortElements,
679}
680
681impl NativeLocalLayout {
682 pub(crate) fn is_indexed(self) -> bool {
683 matches!(self, Self::Indexed | Self::ShortElements)
684 }
685
686 pub(crate) fn uses_early_records(self) -> bool {
687 matches!(self, Self::Early | Self::Indexed | Self::ShortElements)
688 }
689
690 pub(crate) fn proc_trailer_bytes(self) -> usize {
691 match self {
692 Self::ShortElements => 6,
693 _ => 7,
694 }
695 }
696
697 pub(crate) fn indexed_settings_bytes(self) -> usize {
698 match self {
699 Self::ShortElements => 283,
700 Self::Indexed => 303,
701 _ => unreachable!("indexed settings require a split-file layout"),
702 }
703 }
704
705 pub(crate) fn element_capacity(self) -> usize {
706 match self {
707 Self::ShortElements => 15,
708 _ => 31,
709 }
710 }
711}
712
713#[derive(Clone)]
714pub struct OriginalStreamReader<'a> {
715 rd: Reader<'a>,
716 pub(crate) layout: NativeLocalLayout,
717}
718
719impl<'a> OriginalStreamReader<'a> {
720 pub fn new(data: &'a [u8]) -> Self {
721 Self {
722 rd: Reader::new(data),
723 layout: NativeLocalLayout::Current,
724 }
725 }
726
727 pub fn i32(&mut self) -> Result<i32> {
728 self.rd.i32()
729 }
730
731 pub fn i64(&mut self) -> Result<i64> {
732 self.rd.i64()
733 }
734
735 pub fn u16(&mut self) -> Result<u16> {
736 self.rd.u16()
737 }
738
739 pub fn bool(&mut self) -> Result<bool> {
740 Ok(self.rd.u8()? != 0)
741 }
742
743 pub fn skip(&mut self, n: usize) -> Result<()> {
744 self.rd.take(n).map(|_| ())
745 }
746
747 pub fn element(&mut self) -> Result<Vec<i32>> {
748 let mut codes = [0i32; 31];
749 let capacity = self.layout.element_capacity();
750 for dst in &mut codes[..capacity] {
751 *dst = self.rd.i32()?;
752 }
753 let cnt = self.rd.i32()?.clamp(0, capacity as i32) as usize;
754 Ok(codes[..cnt].to_vec())
755 }
756
757 pub fn skip_element(&mut self) -> Result<()> {
758 self.skip((self.layout.element_capacity() + 1) * 4)
759 }
760
761 pub fn skip_empty_proc(&mut self) -> Result<()> {
762 self.skip(4)?;
763 self.skip_element()?;
764 self.skip(8)?;
765 self.skip(self.layout.proc_trailer_bytes())
766 }
767
768 pub fn tid(&mut self) -> Result<[u16; 7]> {
769 let mut out = [0u16; 7];
770 for v in &mut out {
771 *v = self.rd.u16()?;
772 }
773 Ok(out)
774 }
775
776 pub fn take_raw(&mut self, n: usize) -> Result<&'a [u8]> {
777 self.rd.take(n)
778 }
779
780 pub fn len_bytes(&mut self) -> Result<Vec<u8>> {
781 let n = self.rd.i32()?;
782 if n < 0 {
783 bail!("negative byte length {}", n);
784 }
785 Ok(self.rd.take(n as usize)?.to_vec())
786 }
787
788 pub fn remaining(&self) -> &'a [u8] {
789 &self.rd.data[self.rd.pos..]
790 }
791
792 pub(crate) fn detect_local_layout(&mut self) -> Result<()> {
796 if self.layout.uses_early_records() {
797 return Ok(());
798 }
799 for legacy in [false, true] {
800 if self
801 .check_local_layout(legacy, if legacy { 344 } else { 356 })
802 .is_ok()
803 {
804 self.layout = if legacy {
805 NativeLocalLayout::Legacy
806 } else {
807 NativeLocalLayout::Current
808 };
809 return Ok(());
810 }
811 }
812 bail!(
813 "unsupported or corrupt local save layout at byte {}",
814 self.rd.pos
815 )
816 }
817
818 pub(crate) fn detect_early_local_layout(&mut self, button_count: usize) {
821 let mut probe = self.clone();
822 if probe.skip(button_count).is_ok() && probe.check_local_layout(true, 332).is_ok() {
823 self.layout = NativeLocalLayout::Early;
824 }
825 }
826
827 pub(crate) fn check_local_layout(&self, legacy: bool, pod_size: usize) -> Result<()> {
828 let mut probe = self.clone();
829 if !legacy {
830 probe.string()?;
831 }
832 probe.skip(pod_size)?;
833 for string_stack in [false, true, false] {
836 let count = probe.i32()?;
837 if count < 0 || count as usize > probe.remaining().len() / 4 {
838 bail!("invalid local stack count {count}");
839 }
840 if string_stack {
841 for _ in 0..count {
842 probe.string()?;
843 }
844 } else {
845 probe.skip(count as usize * 4)?;
846 }
847 }
848 probe.skip(12 + 76)?; if probe.layout != NativeLocalLayout::ShortElements {
850 probe.string()?; probe.skip(44 + 8)?; }
853 for _ in 0..if probe.layout == NativeLocalLayout::ShortElements {
854 6
855 } else {
856 7
857 } {
858 let jump = probe.i32()?;
859 let count = probe.i32()?;
860 if count < 0 || count as usize > probe.remaining().len() / 4 {
861 bail!("invalid local flag count {count}");
862 }
863 probe.skip(count as usize * 4)?;
864 if jump < 0 || jump as usize != probe.rd.pos {
865 bail!("invalid local flag boundary {jump}");
866 }
867 }
868 Ok(())
869 }
870
871 pub(crate) fn count(&mut self, min_record_bytes: usize) -> Result<usize> {
872 let pos = self.rd.pos;
873 let count = self.i32()?;
874 anyhow::ensure!(
875 count >= 0 && count as usize <= self.remaining().len() / min_record_bytes,
876 "invalid record count {count} at byte {pos}"
877 );
878 Ok(count as usize)
879 }
880
881 pub fn string(&mut self) -> Result<String> {
882 let pos = self.rd.pos;
883 self.rd
884 .str_len()
885 .with_context(|| format!("string at byte {pos}"))
886 }
887
888 fn finish_fixed_array(&mut self, jump: i32) -> Result<()> {
889 if jump < 0 {
890 bail!("negative fixed-array jump {}", jump);
891 }
892 let jump = jump as usize;
893 if jump > self.rd.data.len() {
894 bail!(
895 "fixed-array jump out of bounds: jump {}, stream {}",
896 jump,
897 self.rd.data.len()
898 );
899 }
900 if self.rd.pos > jump {
901 bail!(
902 "fixed-array reader overran jump: pos {}, jump {}",
903 self.rd.pos,
904 jump
905 );
906 }
907 self.rd.pos = jump;
908 Ok(())
909 }
910
911 pub fn fixed_i32_list(&mut self) -> Result<Vec<i64>> {
912 let jump = self.rd.i32()?;
913 let cnt = self.count(1)?;
914 let mut out = Vec::with_capacity(cnt);
915 for _ in 0..cnt {
916 out.push(self.rd.i32()? as i64);
917 }
918 self.finish_fixed_array(jump)?;
919 Ok(out)
920 }
921
922 pub fn fixed_str_list(&mut self) -> Result<Vec<String>> {
923 let jump = self.rd.i32()?;
924 let cnt = self.count(1)?;
925 let mut out = Vec::with_capacity(cnt);
926 for _ in 0..cnt {
927 out.push(self.rd.str_len()?);
928 }
929 self.finish_fixed_array(jump)?;
930 Ok(out)
931 }
932
933 pub fn extend_i32_list(&mut self) -> Result<Vec<i64>> {
934 let cnt = self.count(1)?;
935 let mut out = Vec::with_capacity(cnt);
936 for _ in 0..cnt {
937 out.push(self.rd.i32()? as i64);
938 }
939 Ok(out)
940 }
941
942 pub fn fixed_items<T, F>(&mut self, mut read_one: F) -> Result<Vec<T>>
943 where
944 F: FnMut(&mut OriginalStreamReader<'a>) -> Result<T>,
945 {
946 let jump = self.rd.i32()?;
947 let cnt = self.count(1)?;
948 let mut out = Vec::with_capacity(cnt);
949 for _ in 0..cnt {
950 out.push(read_one(self)?);
951 }
952 self.finish_fixed_array(jump)?;
953 Ok(out)
954 }
955
956 pub fn extend_items<T, F>(&mut self, mut read_one: F) -> Result<Vec<T>>
957 where
958 F: FnMut(&mut OriginalStreamReader<'a>) -> Result<T>,
959 {
960 let cnt = self.count(1)?;
961 let mut out = Vec::with_capacity(cnt);
962 for _ in 0..cnt {
963 out.push(read_one(self)?);
964 }
965 Ok(out)
966 }
967
968 pub fn skip_fixed_items<F>(&mut self, mut skip_one: F) -> Result<()>
969 where
970 F: FnMut(&mut OriginalStreamReader<'a>) -> Result<()>,
971 {
972 let jump = self.rd.i32()?;
973 let cnt = self.count(1)?;
974 for _ in 0..cnt {
975 skip_one(self)?;
976 }
977 self.finish_fixed_array(jump)
978 }
979}
980
981pub fn save_dir(project_dir: &Path) -> PathBuf {
982 project_dir.join("savedata")
983}
984
985pub fn original_save_no(
986 save_cnt: usize,
987 quick_save_cnt: usize,
988 kind: SaveKind,
989 idx: usize,
990) -> usize {
991 match kind {
992 SaveKind::Normal => idx,
993 SaveKind::Quick => save_cnt + idx,
994 SaveKind::End => save_cnt + quick_save_cnt + idx,
995 }
996}
997
998pub fn save_file_path_for_no(project_dir: &Path, save_no: usize) -> PathBuf {
999 save_dir(project_dir).join(format!("{save_no:04}.sav"))
1000}
1001
1002pub fn save_file_path_with_counts(
1003 project_dir: &Path,
1004 save_cnt: usize,
1005 quick_save_cnt: usize,
1006 kind: SaveKind,
1007 idx: usize,
1008) -> PathBuf {
1009 save_file_path_for_no(
1010 project_dir,
1011 original_save_no(save_cnt, quick_save_cnt, kind, idx),
1012 )
1013}
1014
1015pub fn slot_save_no(project_dir: &Path, kind: SaveKind, idx: usize) -> usize {
1016 let save_cnt = configured_count(project_dir, false);
1017 let quick_save_cnt = configured_count(project_dir, true);
1018 original_save_no(save_cnt, quick_save_cnt, kind, idx)
1019}
1020
1021pub fn save_file_path(project_dir: &Path, kind: SaveKind, idx: usize) -> PathBuf {
1022 save_file_path_for_no(project_dir, slot_save_no(project_dir, kind, idx))
1023}
1024
1025pub fn thumb_candidate_paths_for_no(project_dir: &Path, save_no: usize) -> [PathBuf; 2] {
1026 let stem = format!("{save_no:04}");
1027 let dir = save_dir(project_dir);
1028 [
1029 dir.join(format!("{stem}.png")),
1030 dir.join(format!("{stem}.bmp")),
1031 ]
1032}
1033
1034pub fn thumb_candidate_paths_with_counts(
1035 project_dir: &Path,
1036 save_cnt: usize,
1037 quick_save_cnt: usize,
1038 kind: SaveKind,
1039 idx: usize,
1040) -> [PathBuf; 2] {
1041 thumb_candidate_paths_for_no(
1042 project_dir,
1043 original_save_no(save_cnt, quick_save_cnt, kind, idx),
1044 )
1045}
1046
1047pub fn read_header_from_path(path: &Path) -> Result<OriginalSaveHeader> {
1048 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1052 {
1053 let mut file = crate::resource::open_game_file(path)
1054 .with_context(|| format!("open save header {}", path.display()))?;
1055 let file_len = file.metadata()?.len() as usize;
1056 let mut data = vec![0u8; file_len.min(SAVE_HEADER_SIZE)];
1057 file.read_exact(&mut data)
1058 .with_context(|| format!("read save header {}", path.display()))?;
1059 OriginalSaveHeader::from_file_prefix(&data, file_len)
1060 }
1061
1062 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1065 {
1066 let data = crate::resource::read_file_bytes(path)
1067 .with_context(|| format!("read save header {}", path.display()))?;
1068 OriginalSaveHeader::from_file_prefix(&data[..data.len().min(SAVE_HEADER_SIZE)], data.len())
1069 }
1070}
1071
1072pub fn read_header(project_dir: &Path, kind: SaveKind, idx: usize) -> Option<OriginalSaveHeader> {
1073 let path = save_file_path(project_dir, kind, idx);
1074 read_header_from_path(&path).ok()
1075}
1076
1077pub fn read_slot(project_dir: &Path, kind: SaveKind, idx: usize) -> Option<SaveSlotState> {
1078 read_header(project_dir, kind, idx).map(|h| h.to_slot())
1079}
1080
1081pub fn read_slot_from_path(path: &Path) -> Option<SaveSlotState> {
1082 read_header_from_path(path).ok().map(|h| h.to_slot())
1083}
1084
1085pub fn write_header_in_place(path: &Path, header: &OriginalSaveHeader) -> Result<()> {
1086 let existing = read_header_from_path(path)?;
1089 let mut header = header.clone();
1090 header.layout = existing.layout;
1091 header.data_size = existing.data_size;
1092 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1095 {
1096 let mut file = fs::OpenOptions::new()
1097 .write(true)
1098 .open(path)
1099 .with_context(|| format!("open save header for update {}", path.display()))?;
1100 file.write_all(&header.to_bytes())
1101 .with_context(|| format!("write save header {}", path.display()))?;
1102 Ok(())
1103 }
1104
1105 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1106 {
1107 let mut data = crate::resource::read_file_bytes(path)
1108 .with_context(|| format!("read save file {}", path.display()))?;
1109 if data.len() < header.header_size() {
1110 bail!("save file too short for header update: {}", path.display());
1111 }
1112 data[..header.header_size()].copy_from_slice(&header.to_bytes());
1113 fs::write(path, data).with_context(|| format!("write save file {}", path.display()))?;
1114 crate::resource::invalidate_game_path_cache(path);
1115 Ok(())
1116 }
1117}
1118
1119pub fn write_local_save_file(
1120 path: &Path,
1121 slot: &SaveSlotState,
1122 env: &OriginalLocalSaveEnvelope,
1123) -> Result<()> {
1124 let payload = env.to_bytes();
1125 let packed = pack_buffer(&payload);
1126 let header = OriginalSaveHeader::from_slot(slot, packed.len());
1127 let mut out = header.to_bytes();
1128 out.extend_from_slice(&packed);
1129 if let Some(parent) = path.parent() {
1130 fs::create_dir_all(parent)
1131 .with_context(|| format!("create save dir {}", parent.display()))?;
1132 }
1133 fs::write(path, out).with_context(|| format!("write save file {}", path.display()))?;
1134 crate::resource::invalidate_game_path_cache(path);
1135 Ok(())
1136}
1137
1138pub fn write_slot_file(path: &Path, slot: &SaveSlotState) -> Result<()> {
1139 let env = OriginalLocalSaveEnvelope::empty_from_slot(slot);
1140 write_local_save_file(path, slot, &env)
1141}
1142
1143pub fn read_local_save_file(
1144 path: &Path,
1145) -> Result<(OriginalSaveHeader, OriginalLocalSaveEnvelope)> {
1146 let data = crate::resource::read_file_bytes(path)
1147 .with_context(|| format!("read save file {}", path.display()))?;
1148 if data.len() < LEGACY_SAVE_HEADER_SIZE {
1149 bail!("save file too short: {}", path.display());
1150 }
1151 let header = OriginalSaveHeader::from_file_prefix(&data, data.len())?;
1152 let env = match header.layout {
1153 LocalSaveLayout::LegacySplit { local_ex_data_size } => {
1154 read_split_local_save(&data, &header, local_ex_data_size)
1155 }
1156 LocalSaveLayout::CurrentEnvelope | LocalSaveLayout::LegacyEnvelope => {
1157 read_enveloped_local_save(&data, &header)
1158 }
1159 }
1160 .with_context(|| format!("decode local save {} ({:?})", path.display(), header.layout))?;
1161 Ok((header, env))
1162}
1163
1164fn unpack_local_save_part(data: &[u8], start: usize, size: i32) -> Result<(Vec<u8>, usize)> {
1165 anyhow::ensure!(size >= 0, "negative local save payload size: {size}");
1166 let end = start
1167 .checked_add(size as usize)
1168 .ok_or_else(|| anyhow!("save data size overflow"))?;
1169 if end > data.len() {
1170 bail!("save payload truncated: need {}, have {}", end, data.len());
1171 }
1172 Ok((unpack_buffer(&data[start..end])?, end))
1173}
1174
1175fn read_enveloped_local_save(
1176 data: &[u8],
1177 header: &OriginalSaveHeader,
1178) -> Result<OriginalLocalSaveEnvelope> {
1179 let (payload, _) = unpack_local_save_part(data, header.header_size(), header.data_size)?;
1180 let mut env = read_envelope(&mut Reader::new(&payload), true, header.layout.is_legacy())?;
1181 if header.layout.is_legacy() {
1182 env.title = header.title.clone();
1183 env.message = header.message.clone();
1184 env.full_message = header.full_message.clone();
1185 }
1186 Ok(env)
1187}
1188
1189fn read_split_local_save(
1190 data: &[u8],
1191 header: &OriginalSaveHeader,
1192 local_ex_data_size: i32,
1193) -> Result<OriginalLocalSaveEnvelope> {
1194 let (local_stream, end) = unpack_local_save_part(data, header.header_size(), header.data_size)
1197 .context("unpack local stream")?;
1198 let (local_ex_stream, _) = unpack_local_save_part(data, end, local_ex_data_size)
1199 .context("unpack local extra stream")?;
1200 Ok(OriginalLocalSaveEnvelope::from_slot_with_streams(
1201 &header.to_slot(),
1202 local_stream,
1203 local_ex_stream,
1204 ))
1205}
1206
1207pub fn write_global_save_file(project_dir: &Path, global_stream: &[u8]) -> Result<()> {
1208 let packed = pack_buffer(global_stream);
1209 let header = OriginalGlobalSaveHeader {
1210 major_version: 2,
1211 minor_version: 0,
1212 global_data_size: packed.len() as i32,
1213 };
1214 let path = save_dir(project_dir).join("global.sav");
1215 if let Some(parent) = path.parent() {
1216 fs::create_dir_all(parent)
1217 .with_context(|| format!("create save dir {}", parent.display()))?;
1218 }
1219 let mut out = header.to_bytes();
1220 out.extend_from_slice(&packed);
1221 fs::write(&path, out).with_context(|| format!("write global save file {}", path.display()))?;
1222 crate::resource::invalidate_game_path_cache(&path);
1223 Ok(())
1224}
1225
1226pub fn read_global_save_file(project_dir: &Path) -> Result<(OriginalGlobalSaveHeader, Vec<u8>)> {
1227 let path = save_dir(project_dir).join("global.sav");
1228 let data = crate::resource::read_file_bytes(&path)
1229 .with_context(|| format!("read global save file {}", path.display()))?;
1230 if data.len() < GLOBAL_SAVE_HEADER_SIZE {
1231 bail!("global save file too short: {}", path.display());
1232 }
1233 let header = OriginalGlobalSaveHeader::from_bytes(&data[..GLOBAL_SAVE_HEADER_SIZE])?;
1234 let size = header.global_data_size.max(0) as usize;
1235 let end = GLOBAL_SAVE_HEADER_SIZE
1236 .checked_add(size)
1237 .ok_or_else(|| anyhow!("global save data size overflow"))?;
1238 if end > data.len() {
1239 bail!(
1240 "global save payload truncated: need {}, have {}",
1241 end,
1242 data.len()
1243 );
1244 }
1245 let payload = unpack_buffer(&data[GLOBAL_SAVE_HEADER_SIZE..end])?;
1246 Ok((header, payload))
1247}
1248
1249pub fn write_read_save_file(project_dir: &Path, scene_rows: &[(String, Vec<u8>)]) -> Result<()> {
1255 let mut stream = OriginalStreamWriter::new();
1256 for (scene_name, flags) in scene_rows {
1257 stream.push_str(scene_name);
1258 stream.push_i32(flags.len().min(i32::MAX as usize) as i32);
1259 stream.push_raw(flags);
1260 }
1261 let packed = pack_buffer(&stream.into_inner());
1262 let path = save_dir(project_dir).join("read.sav");
1263 if let Some(parent) = path.parent() {
1264 fs::create_dir_all(parent)
1265 .with_context(|| format!("create save dir {}", parent.display()))?;
1266 }
1267 let mut out = Vec::with_capacity(16 + packed.len());
1268 push_i32(&mut out, 1);
1269 push_i32(&mut out, 0);
1270 push_i32(&mut out, packed.len().min(i32::MAX as usize) as i32);
1271 push_i32(&mut out, scene_rows.len().min(i32::MAX as usize) as i32);
1272 out.extend_from_slice(&packed);
1273 fs::write(&path, out).with_context(|| format!("write read save file {}", path.display()))?;
1274 crate::resource::invalidate_game_path_cache(&path);
1275 Ok(())
1276}
1277
1278pub fn read_read_save_file(project_dir: &Path) -> Result<Vec<(String, Vec<u8>)>> {
1281 let path = save_dir(project_dir).join("read.sav");
1282 let data = crate::resource::read_file_bytes(&path)
1283 .with_context(|| format!("read read save file {}", path.display()))?;
1284 if data.len() < 16 {
1285 bail!("read save header too short: {}", data.len());
1286 }
1287 let mut header = Reader::new(&data[..16]);
1288 let major = header.i32()?;
1289 let minor = header.i32()?;
1290 let packed_size = header.i32()?;
1291 let scene_count = header.i32()?;
1292 if major != 1 || minor != 0 {
1293 bail!("unsupported read save version {}.{}", major, minor);
1294 }
1295 if packed_size < 0 || scene_count < 0 {
1296 bail!(
1297 "invalid read save header: packed_size={} scene_count={}",
1298 packed_size,
1299 scene_count
1300 );
1301 }
1302 let end = 16usize
1303 .checked_add(packed_size as usize)
1304 .ok_or_else(|| anyhow!("read save size overflow"))?;
1305 if end > data.len() {
1306 bail!(
1307 "read save payload truncated: need {}, have {}",
1308 end,
1309 data.len()
1310 );
1311 }
1312 let payload = unpack_buffer(&data[16..end])?;
1313 let mut rd = OriginalStreamReader::new(&payload);
1314 let mut rows = Vec::with_capacity(scene_count as usize);
1315 for _ in 0..scene_count as usize {
1316 let scene_name = rd.string()?;
1317 let count = rd.i32()?;
1318 if count < 0 {
1319 bail!(
1320 "negative read flag count {} for scene {}",
1321 count,
1322 scene_name
1323 );
1324 }
1325 let flags = rd.take_raw(count as usize)?.to_vec();
1326 rows.push((scene_name, flags));
1327 }
1328 Ok(rows)
1329}
1330
1331pub fn write_config_save_file(project_dir: &Path, config_stream: &[u8]) -> Result<()> {
1332 let packed = pack_buffer(config_stream);
1333 let header = OriginalConfigSaveHeader {
1334 major_version: 1,
1335 minor_version: 4,
1336 config_data_size: packed.len() as i32,
1337 };
1338 let path = save_dir(project_dir).join("config.sav");
1339 if let Some(parent) = path.parent() {
1340 fs::create_dir_all(parent)
1341 .with_context(|| format!("create save dir {}", parent.display()))?;
1342 }
1343 let mut out = header.to_bytes();
1344 out.extend_from_slice(&packed);
1345 fs::write(&path, out).with_context(|| format!("write config save file {}", path.display()))?;
1346 crate::resource::invalidate_game_path_cache(&path);
1347 Ok(())
1348}
1349
1350pub fn read_config_save_file(project_dir: &Path) -> Result<(OriginalConfigSaveHeader, Vec<u8>)> {
1351 let path = save_dir(project_dir).join("config.sav");
1352 let data = crate::resource::read_file_bytes(&path)
1353 .with_context(|| format!("read config save file {}", path.display()))?;
1354 if data.len() < CONFIG_SAVE_HEADER_SIZE {
1355 bail!("config save file too short: {}", path.display());
1356 }
1357 let header = OriginalConfigSaveHeader::from_bytes(&data[..CONFIG_SAVE_HEADER_SIZE])?;
1358 if header.major_version != 1 || header.minor_version < 0 {
1359 bail!(
1360 "unsupported config save version {}.{}",
1361 header.major_version,
1362 header.minor_version
1363 );
1364 }
1365 let size = header.config_data_size.max(0) as usize;
1366 let end = CONFIG_SAVE_HEADER_SIZE
1367 .checked_add(size)
1368 .ok_or_else(|| anyhow!("config save data size overflow"))?;
1369 if end > data.len() {
1370 bail!(
1371 "config save payload truncated: need {}, have {}",
1372 end,
1373 data.len()
1374 );
1375 }
1376 let payload = unpack_buffer(&data[CONFIG_SAVE_HEADER_SIZE..end])?;
1377 Ok((header, payload))
1378}
1379
1380pub fn pack_buffer(src: &[u8]) -> Vec<u8> {
1381 let mut out = Vec::with_capacity(8 + src.len() + src.len().div_ceil(8));
1382 push_u32(&mut out, 0);
1383 push_u32(&mut out, src.len() as u32);
1384 for chunk in src.chunks(8) {
1385 let mut flag = 0u8;
1386 for i in 0..chunk.len() {
1387 flag |= 1u8 << i;
1388 }
1389 out.push(flag);
1390 out.extend_from_slice(chunk);
1391 }
1392 let arc_size = out.len() as u32;
1393 out[0..4].copy_from_slice(&arc_size.to_le_bytes());
1394 tpc_xor(&mut out);
1395 out
1396}
1397
1398pub fn unpack_buffer(src: &[u8]) -> Result<Vec<u8>> {
1399 let mut data = src.to_vec();
1400 tpc_xor(&mut data);
1401 if data.len() < 8 {
1402 bail!("lzss payload too short");
1403 }
1404 let arc_size = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
1405 let org_size = u32::from_le_bytes(data[4..8].try_into().unwrap()) as usize;
1406 if arc_size > data.len() {
1407 bail!("lzss arc_size out of bounds: {} > {}", arc_size, data.len());
1408 }
1409 let mut out = Vec::with_capacity(org_size);
1410 let mut p = 8usize;
1411 while out.len() < org_size {
1412 if p >= arc_size {
1413 bail!("lzss payload ended before output was complete");
1414 }
1415 let mut flags = data[p];
1416 p += 1;
1417 for _ in 0..8 {
1418 if out.len() >= org_size {
1419 break;
1420 }
1421 if flags & 1 != 0 {
1422 if p >= arc_size {
1423 bail!("lzss literal out of bounds");
1424 }
1425 out.push(data[p]);
1426 p += 1;
1427 } else {
1428 if p + 2 > arc_size {
1429 bail!("lzss backref out of bounds");
1430 }
1431 let token = u16::from_le_bytes([data[p], data[p + 1]]);
1432 p += 2;
1433 let offset = (token >> 4) as usize;
1434 let len = ((token & 0x0f) as usize) + 2;
1435 if offset == 0 || offset > out.len() {
1436 bail!(
1437 "lzss invalid backref offset {} at out {}",
1438 offset,
1439 out.len()
1440 );
1441 }
1442 let base = out.len() - offset;
1443 for i in 0..len {
1444 let b = out[base + i];
1445 out.push(b);
1446 if out.len() >= org_size {
1447 break;
1448 }
1449 }
1450 }
1451 flags >>= 1;
1452 }
1453 }
1454 Ok(out)
1455}
1456
1457pub fn configured_count(project_dir: &Path, quick: bool) -> usize {
1458 let keys: [&str; 2] = if quick {
1459 ["#QUICK_SAVE.CNT", "QUICK_SAVE.CNT"]
1460 } else {
1461 ["#SAVE.CNT", "SAVE.CNT"]
1462 };
1463 configured_usize_any(project_dir, &keys, if quick { 3 } else { 10 }).min(10000)
1464}
1465
1466pub fn configured_flag_count(project_dir: &Path) -> usize {
1467 configured_usize_any(project_dir, &["#FLAG.CNT", "FLAG.CNT"], 1000).min(10000)
1468}
1469
1470pub fn configured_mwnd_waku_btn_count(project_dir: &Path) -> usize {
1471 configured_usize_any(project_dir, &["#WAKU.BTN.CNT", "WAKU.BTN.CNT"], 8).min(256)
1472}
1473
1474fn configured_usize_any(_project_dir: &Path, _keys: &[&str], default_value: usize) -> usize {
1475 default_value
1476}
1477
1478fn patch_i32(out: &mut [u8], off: usize, v: i32) {
1479 out[off..off + 4].copy_from_slice(&v.to_le_bytes());
1480}
1481
1482fn push_i32(out: &mut Vec<u8>, v: i32) {
1483 out.extend_from_slice(&v.to_le_bytes());
1484}
1485
1486fn push_i64(out: &mut Vec<u8>, v: i64) {
1487 out.extend_from_slice(&v.to_le_bytes());
1488}
1489
1490fn push_u16(out: &mut Vec<u8>, v: u16) {
1491 out.extend_from_slice(&v.to_le_bytes());
1492}
1493
1494fn push_u32(out: &mut Vec<u8>, v: u32) {
1495 out.extend_from_slice(&v.to_le_bytes());
1496}
1497
1498fn push_str_len(out: &mut Vec<u8>, s: &str) {
1499 let utf16: Vec<u16> = s.encode_utf16().collect();
1500 push_i32(out, utf16.len().min(i32::MAX as usize) as i32);
1501 for ch in utf16 {
1502 out.extend_from_slice(&ch.to_le_bytes());
1503 }
1504}
1505
1506fn push_utf16_fixed(out: &mut Vec<u8>, s: &str, units: usize) {
1507 let mut written = 0usize;
1508 for ch in s.encode_utf16().take(units.saturating_sub(1)) {
1509 out.extend_from_slice(&ch.to_le_bytes());
1510 written += 1;
1511 }
1512 while written < units {
1513 out.extend_from_slice(&0u16.to_le_bytes());
1514 written += 1;
1515 }
1516}
1517
1518#[derive(Clone)]
1519struct Reader<'a> {
1520 data: &'a [u8],
1521 pos: usize,
1522}
1523
1524impl<'a> Reader<'a> {
1525 fn new(data: &'a [u8]) -> Self {
1526 Self { data, pos: 0 }
1527 }
1528
1529 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
1530 let end = self
1531 .pos
1532 .checked_add(n)
1533 .ok_or_else(|| anyhow!("reader overflow"))?;
1534 if end > self.data.len() {
1535 bail!(
1536 "reader out of bounds: need {}, have {}",
1537 end,
1538 self.data.len()
1539 );
1540 }
1541 let out = &self.data[self.pos..end];
1542 self.pos = end;
1543 Ok(out)
1544 }
1545
1546 fn i32(&mut self) -> Result<i32> {
1547 let b = self.take(4)?;
1548 Ok(i32::from_le_bytes(b.try_into().unwrap()))
1549 }
1550
1551 fn u8(&mut self) -> Result<u8> {
1552 let b = self.take(1)?;
1553 Ok(b[0])
1554 }
1555
1556 fn u16(&mut self) -> Result<u16> {
1557 let b = self.take(2)?;
1558 Ok(u16::from_le_bytes(b.try_into().unwrap()))
1559 }
1560
1561 fn i64(&mut self) -> Result<i64> {
1562 let b = self.take(8)?;
1563 Ok(i64::from_le_bytes(b.try_into().unwrap()))
1564 }
1565
1566 fn utf16_fixed(&mut self, units: usize) -> Result<String> {
1567 let bytes = self.take(units * 2)?;
1568 let mut u16s = Vec::new();
1569 for i in 0..units {
1570 let p = i * 2;
1571 let w = u16::from_le_bytes([bytes[p], bytes[p + 1]]);
1572 if w == 0 {
1573 break;
1574 }
1575 u16s.push(w);
1576 }
1577 Ok(String::from_utf16_lossy(&u16s))
1578 }
1579
1580 fn str_len(&mut self) -> Result<String> {
1581 let offset = self.pos;
1582 let len = self.i32()?;
1583 if len < 0 {
1584 bail!("negative string length {} at byte offset {}", len, offset);
1585 }
1586 let len = len as usize;
1587 let bytes = self.take(len * 2)?;
1588 let mut u16s = Vec::with_capacity(len);
1589 for i in 0..len {
1590 let p = i * 2;
1591 u16s.push(u16::from_le_bytes([bytes[p], bytes[p + 1]]));
1592 }
1593 Ok(String::from_utf16_lossy(&u16s))
1594 }
1595}
1596
1597fn tpc_xor(data: &mut [u8]) {
1598 for (i, b) in data.iter_mut().enumerate() {
1599 *b ^= TPC_ANGOU_TABLE[i & 0xff];
1600 }
1601}
1602
1603const TPC_ANGOU_TABLE: [u8; 256] = [
1604 0x8b, 0xe5, 0x5d, 0xc3, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x85, 0xc0, 0x74, 0x09, 0x5f, 0x5e, 0x33,
1605 0xc0, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x45, 0x0c, 0x85, 0xc0, 0x75, 0x14, 0x8b, 0x55, 0xec,
1606 0x83, 0xc2, 0x20, 0x52, 0x6a, 0x00, 0xe8, 0xf5, 0x28, 0x01, 0x00, 0x83, 0xc4, 0x08, 0x89, 0x45,
1607 0x0c, 0x8b, 0x45, 0xe4, 0x6a, 0x00, 0x6a, 0x00, 0x50, 0x53, 0xff, 0x15, 0x34, 0xb1, 0x43, 0x00,
1608 0x8b, 0x45, 0x10, 0x85, 0xc0, 0x74, 0x05, 0x8b, 0x4d, 0xec, 0x89, 0x08, 0x8a, 0x45, 0xf0, 0x84,
1609 0xc0, 0x75, 0x78, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x8b, 0x7d, 0xe8, 0x8b, 0x75, 0x0c, 0x85, 0xc0,
1610 0x75, 0x44, 0x8b, 0x1d, 0xd0, 0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x37, 0x81, 0xff, 0x00, 0x00,
1611 0x04, 0x00, 0x6a, 0x00, 0x76, 0x43, 0x8b, 0x45, 0xf8, 0x8d, 0x55, 0xfc, 0x52, 0x68, 0x00, 0x00,
1612 0x04, 0x00, 0x56, 0x50, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0x6a, 0x05, 0xff, 0xd3, 0xa1, 0xe0,
1613 0x30, 0x44, 0x00, 0x81, 0xef, 0x00, 0x00, 0x04, 0x00, 0x81, 0xc6, 0x00, 0x00, 0x04, 0x00, 0x85,
1614 0xc0, 0x74, 0xc5, 0x8b, 0x5d, 0xf8, 0x53, 0xe8, 0xf4, 0xfb, 0xff, 0xff, 0x8b, 0x45, 0x0c, 0x83,
1615 0xc4, 0x04, 0x5f, 0x5e, 0x5b, 0x8b, 0xe5, 0x5d, 0xc3, 0x8b, 0x55, 0xf8, 0x8d, 0x4d, 0xfc, 0x51,
1616 0x57, 0x56, 0x52, 0xff, 0x15, 0x2c, 0xb1, 0x43, 0x00, 0xeb, 0xd8, 0x8b, 0x45, 0xe8, 0x83, 0xc0,
1617 0x20, 0x50, 0x6a, 0x00, 0xe8, 0x47, 0x28, 0x01, 0x00, 0x8b, 0x7d, 0xe8, 0x89, 0x45, 0xf4, 0x8b,
1618 0xf0, 0xa1, 0xe0, 0x30, 0x44, 0x00, 0x83, 0xc4, 0x08, 0x85, 0xc0, 0x75, 0x56, 0x8b, 0x1d, 0xd0,
1619 0xb0, 0x43, 0x00, 0x85, 0xff, 0x76, 0x49, 0x81, 0xff, 0x00, 0x00, 0x04, 0x00, 0x6a, 0x00, 0x76,
1620];
1621
1622#[cfg(test)]
1623mod local_layout_tests {
1624 use super::*;
1625
1626 fn local_prefix(legacy: bool, voice: i32, font: &str) -> (Vec<u8>, usize) {
1627 let mut w = OriginalStreamWriter::new();
1628 w.push_padding(19);
1630 let start = w.position();
1631 if !legacy {
1632 w.push_str(font);
1633 }
1634 w.push_i32(voice);
1635 w.push_padding(if legacy { 340 } else { 352 });
1636 w.push_i32(2);
1637 w.push_i32(-1);
1638 w.push_i32(123);
1639 w.push_i32(1);
1640 w.push_str("stack value");
1641 w.push_i32(1);
1642 w.push_i32(0);
1643 w.push_padding(12 + 76);
1644 w.push_str("fog");
1645 w.push_padding(44 + 8);
1646 for _ in 0..7 {
1647 w.push_fixed_i32_list(&[1, -1, 3], 3);
1648 }
1649 (w.into_inner(), start)
1650 }
1651
1652 #[test]
1653 fn early_local_layout_skips_buttons_and_has_no_message_or_font_string() {
1654 for button_count in [0, 19, 32] {
1655 let mut w = OriginalStreamWriter::new();
1656 w.push_padding(23);
1657 let start = w.position();
1658 w.push_padding(button_count);
1659 w.push_i32(-1); w.push_padding(328);
1661 for _ in 0..3 {
1662 w.push_i32(0);
1663 }
1664 w.push_padding(12 + 76);
1665 w.push_str("");
1666 w.push_padding(44 + 8);
1667 for _ in 0..7 {
1668 w.push_fixed_i32_list(&[3, 5], 2);
1669 }
1670 let mut bytes = w.into_inner();
1671 let mut rd = OriginalStreamReader::new(&bytes);
1672 rd.skip(start).unwrap();
1673 rd.detect_early_local_layout(button_count);
1674 assert!(rd.layout.uses_early_records() && (rd.layout != NativeLocalLayout::Current));
1675 assert_eq!(rd.remaining().len(), bytes.len() - start);
1676 rd.skip(button_count).unwrap();
1677 rd.detect_local_layout().unwrap();
1678 assert_eq!(rd.i32().unwrap(), -1);
1679
1680 bytes.pop();
1681 let mut rd = OriginalStreamReader::new(&bytes);
1682 rd.skip(start).unwrap();
1683 rd.detect_early_local_layout(button_count);
1684 assert!(!rd.layout.uses_early_records());
1685 }
1686 }
1687
1688 #[test]
1689 fn split_header_loads_both_streams_and_preserves_them_on_metadata_update() {
1690 let local = vec![17; 2048];
1691 let extra = vec![3, 7, 0, 9];
1692 let packed_local = pack_buffer(&local);
1693 let packed_extra = pack_buffer(&extra);
1694 let mut header = OriginalSaveHeader {
1695 layout: LocalSaveLayout::LegacySplit {
1696 local_ex_data_size: packed_extra.len() as i32,
1697 },
1698 data_size: packed_local.len() as i32,
1699 title: "saved scene".into(),
1700 message: "saved message".into(),
1701 year: 2013,
1702 month: 7,
1703 day: 26,
1704 ..Default::default()
1705 };
1706
1707 let mut bytes = header.to_bytes();
1708 assert_eq!(bytes.len(), 3120);
1709 bytes.extend_from_slice(&packed_local);
1710 bytes.extend_from_slice(&packed_extra);
1711 let dir =
1712 std::env::temp_dir().join(format!("siglus-split-save-test-{}", std::process::id()));
1713 fs::create_dir_all(&dir).unwrap();
1714 let path = dir.join("0001.sav");
1715 fs::write(&path, &bytes).unwrap();
1716 let (restored_header, env) = read_local_save_file(&path).unwrap();
1717 assert_eq!(restored_header.header_size(), 3120);
1718 assert_eq!(env.title, header.title);
1719 assert_eq!(env.full_message, header.message);
1720 assert_eq!(env.save_id[..3], [2013, 7, 26]);
1721 assert_eq!(env.local_stream, local);
1722 assert_eq!(env.local_ex_stream, extra);
1723 let mut updated = OriginalSaveHeader {
1726 comment: "edited".into(),
1727 ..Default::default()
1728 };
1729
1730 write_header_in_place(&path, &updated).unwrap();
1731 let after = fs::read(&path).unwrap();
1732 assert_eq!(after.len(), bytes.len());
1733 assert_eq!(&after[3112..], &bytes[3112..]);
1734 let (restored_header, env) = read_local_save_file(&path).unwrap();
1735 assert_eq!(restored_header.comment, "edited");
1736 assert_eq!(env.local_stream, local);
1737 assert_eq!(env.local_ex_stream, extra);
1738 fs::write(&path, &bytes[..bytes.len() - 1]).unwrap();
1741 assert!(
1742 read_local_save_file(&path)
1743 .unwrap_err()
1744 .to_string()
1745 .contains("truncated local save layout")
1746 );
1747 fs::remove_dir_all(dir).unwrap();
1748 }
1749
1750 #[test]
1751 fn legacy_header_and_envelope_load_and_metadata_update_preserves_payload() {
1752 let mut payload = Vec::new();
1755 for index in 0..2u16 {
1756 for value in [2022, 6, 6, 18, 24, index, 740] {
1757 push_u16(&mut payload, value);
1758 }
1759 push_i32(&mut payload, 3);
1760 payload.extend_from_slice(&[index as u8, 7, 9]);
1761 push_i32(&mut payload, 1024);
1762 payload.extend_from_slice(&[4, 5]);
1763 payload.resize(payload.len() + 1022, 0);
1764 if index == 0 {
1765 push_i32(&mut payload, 1);
1766 }
1767 }
1768 let packed = pack_buffer(&payload);
1769 let mut header = Vec::new();
1770 for value in [1, 0, 2022, 6, 6, 1, 18, 24, 0, 740] {
1771 push_i32(&mut header, value);
1772 }
1773 for value in ["saved scene", "saved message", "comment", "comment2"] {
1774 push_utf16_fixed(&mut header, value, 256);
1775 }
1776 for value in 0..256 {
1777 push_i32(&mut header, value);
1778 }
1779 push_i32(&mut header, packed.len() as i32);
1780 assert_eq!(header.len(), 3116);
1781 let mut bytes = header;
1782 bytes.extend_from_slice(&packed);
1783 assert!(bytes.len() > SAVE_HEADER_SIZE);
1784 let dir =
1785 std::env::temp_dir().join(format!("siglus-early-save-test-{}", std::process::id()));
1786 fs::create_dir_all(&dir).unwrap();
1787 let path = dir.join("0001.sav");
1788 fs::write(&path, &bytes).unwrap();
1789 let (header, env) = read_local_save_file(&path).unwrap();
1790 assert!(header.layout.is_legacy());
1791 assert_eq!(header.flag[255], 255);
1792 assert_eq!(env.title, "saved scene");
1793 assert_eq!(env.full_message, "saved message");
1794 assert_eq!(env.local_stream, [0, 7, 9]);
1795 assert_eq!(env.local_ex_stream.len(), 1024);
1796 assert_eq!(&env.local_ex_stream[..2], &[4, 5]);
1797 assert_eq!(env.sel_saves.len(), 1);
1798 assert_eq!(env.sel_saves[0].local_stream, [1, 7, 9]);
1799 assert_eq!(env.sel_saves[0].save_id[5], 1);
1800 let mut updated = OriginalSaveHeader {
1802 comment: "edited".to_owned(),
1803 ..Default::default()
1804 };
1805
1806 write_header_in_place(&path, &updated).unwrap();
1807 let after = fs::read(&path).unwrap();
1808 assert_eq!(after.len(), bytes.len());
1809 assert_eq!(&after[3116..], &bytes[3116..]);
1810 let (header, env) = read_local_save_file(&path).unwrap();
1811 assert_eq!(header.comment, "edited");
1812 assert_eq!(env.local_stream, [0, 7, 9]);
1813 let modern_path = dir.join("0002.sav");
1814 let slot = SaveSlotState::default();
1815 let mut modern =
1816 OriginalLocalSaveEnvelope::from_slot_with_streams(&slot, vec![1, 2], vec![3]);
1817 modern.full_message = "complete message".to_owned();
1818 write_local_save_file(&modern_path, &slot, &modern).unwrap();
1819 let (header, restored) = read_local_save_file(&modern_path).unwrap();
1820 assert!(!header.layout.is_legacy());
1821 assert_eq!(restored.full_message, modern.full_message);
1822 assert_eq!(restored.local_stream, modern.local_stream);
1823 fs::remove_dir_all(dir).unwrap();
1824 }
1825
1826 #[test]
1827 fn local_layout_uses_boundaries_not_voice_number_as_string_length() {
1828 for legacy in [false, true] {
1829 for voice in [-1, 0, 1, 240] {
1830 for font in ["", "Test Font"] {
1831 let (bytes, start) = local_prefix(legacy, voice, font);
1832 let mut rd = OriginalStreamReader::new(&bytes);
1833 rd.skip(start).unwrap();
1834 let remaining = rd.remaining().len();
1835 rd.detect_local_layout().unwrap();
1836 assert_eq!((rd.layout != NativeLocalLayout::Current), legacy);
1837 assert_eq!(rd.remaining().len(), remaining);
1838 }
1839 }
1840 }
1841 }
1842
1843 #[test]
1844 fn local_layout_rejects_truncated_or_invalid_flag_boundaries() {
1845 for legacy in [false, true] {
1846 let (mut bytes, start) = local_prefix(legacy, -1, "");
1847 let boundary = bytes.len() - 20;
1848 bytes[boundary..boundary + 4].copy_from_slice(&0i32.to_le_bytes());
1849 let mut rd = OriginalStreamReader::new(&bytes);
1850 rd.skip(start).unwrap();
1851 assert!(rd.detect_local_layout().is_err());
1852 bytes.truncate(boundary);
1853 let mut rd = OriginalStreamReader::new(&bytes);
1854 rd.skip(start).unwrap();
1855 assert!(rd.detect_local_layout().is_err());
1856 }
1857 }
1858}