Skip to main content

siglus_scene_vm/
original_save.rs

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        // These layouts all use version 1.0. Validate the complete payload
283        // boundary, including both compressed streams in the 3120-byte layout.
284        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/// Mutually exclusive native local-stream generations. File header sizes alone
670/// do not identify these layouts; the VM probes record and array boundaries.
671#[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    /// Called immediately after the window-button disable bytes. Save headers
793    /// do not distinguish these layouts. Validate the stack and absolute flag
794    /// array boundaries: a voice number can also look like a string length.
795    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    /// Probe before the optional full-message string and window buttons.
819    /// Early saves omit that string as well as the later font fields.
820    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        // Integer stack, string stack, and element-point stack. Bound counts
834        // before iterating; this probe must not allocate from untrusted counts.
835        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)?; // Local clocks and system menu POD.
849        if probe.layout != NativeLocalLayout::ShortElements {
850            probe.string()?; // Fog texture name.
851            probe.skip(44 + 8)?; // Fog event and near/far planes.
852        }
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    // Native C_tnm_save_cache::load_cache() reads only
1049    // sizeof(S_tnm_save_header). Do not pull the packed local-save payload
1050    // into memory just to answer LOAD_SCENE metadata queries.
1051    #[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    // The browser VFS currently exposes whole-file reads only. Preserve that
1063    // backend while keeping the native path faithful to C_file::read(header).
1064    #[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    // Callers rebuild metadata from SaveSlotState, which does not carry the
1087    // on-disk layout. Retain the existing boundary and payload length.
1088    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    // C_tnm_save_cache::save_cache() opens the existing file as rb+ and
1093    // overwrites only S_tnm_save_header. Keep the packed payload untouched.
1094    #[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    // This generation stores the raw VM and extra streams in separate LZSS
1195    // blocks, without an envelope or selection snapshots. Metadata is in the header.
1196    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
1249/// Write the original per-scene read-flag file (`savedata/read.sav`).
1250///
1251/// C++ stores every scene row by scene name, followed by the exact byte count
1252/// and one byte per read flag.  Keeping the scene name is important because
1253/// scene indices can move when scripts are rebuilt.
1254pub 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
1278/// Read `savedata/read.sav` without assigning rows to current scene indices.
1279/// The caller performs the original name lookup and exact flag-count check.
1280pub 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        // Exercise absolute array jumps with a nonzero header length.
1629        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); // Current voice, not a string length.
1660            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        // Header caches reconstruct the latest header type. Preserve the
1724        // original layout and both lengths when applying that metadata.
1725        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        // A missing byte in either stream must not trigger a fallback to a
1739        // different header and report a misleading LZSS error.
1740        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        // The early native envelope stores only IDs and streams, including its
1753        // selection snapshots. Text metadata exists only in the 3116-byte header.
1754        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        // Match the cache path: it reconstructs a current-format header.
1801        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}