1use crate::{Result, error::Error};
4use std::collections::BTreeMap;
5use std::path::Path;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct SaveMetadata {
10 pub version: [u8; 4],
11 pub unknown_04: u32,
12 pub unknown_08: u32,
13 pub description: String,
15 pub world_id: u32,
16 pub bitmap_filename: String,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct SaveSection {
23 pub name: [u8; 4],
24 pub data: Vec<u8>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct SaveGame {
30 pub metadata: SaveMetadata,
31 pub sections: Vec<SaveSection>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct SaveSlot {
38 pub game: SaveGame,
39 pub maps: BTreeMap<String, SaveGame>,
41 pub logbook: Option<Vec<u8>>,
43}
44
45pub fn parse_savegame_file(mut bytes: &[u8]) -> Result<SaveGame> {
48 let invalid = |message: &str| Error::Parse(format!("save game: {message}"));
49 let mut metadata = None;
50 let mut sections = Vec::new();
51 loop {
52 if bytes.starts_with(b"END ") {
53 if bytes.len() != 4 {
54 return Err(invalid("trailing bytes after END"));
55 }
56 return Ok(SaveGame {
57 metadata: metadata.ok_or_else(|| invalid("missing SVGM metadata"))?,
58 sections,
59 });
60 }
61 let header = bytes
62 .get(..8)
63 .ok_or_else(|| invalid("truncated chunk header or missing END"))?;
64 let name: [u8; 4] = header[..4].try_into().unwrap();
65 let size = usize::try_from(u32::from_be_bytes(header[4..8].try_into().unwrap()))
66 .map_err(|_| invalid("chunk length exceeds address space"))?;
67 bytes = &bytes[8..];
68 let data = bytes
69 .get(..size)
70 .ok_or_else(|| invalid("chunk payload exceeds remaining file bytes"))?;
71 if sections.is_empty() && name != *b"SVGM" {
72 return Err(invalid("first chunk must be SVGM"));
73 }
74 if name == *b"SVGM" {
75 if metadata.is_some() {
76 return Err(invalid("duplicate SVGM metadata"));
77 }
78 metadata = Some(parse_metadata(data)?);
79 }
80 sections.push(SaveSection {
81 name,
82 data: data.to_vec(),
83 });
84 bytes = &bytes[size..];
85 }
86}
87
88pub fn load_save_slot(path: impl AsRef<Path>) -> Result<SaveSlot> {
92 let path = path.as_ref();
93 let file_error = |path: &Path, message: String| Error::File {
94 path: path.to_path_buf(),
95 message,
96 };
97 let entries = std::fs::read_dir(path).map_err(|e| file_error(path, e.to_string()))?;
98 let mut resources = BTreeMap::new();
99 for entry in entries {
100 let entry = entry.map_err(|e| file_error(path, e.to_string()))?;
101 let Some(name) = entry.file_name().to_str().map(str::to_ascii_uppercase) else {
102 continue;
103 };
104 if name != "SAVEGAME.SAV" && name != "LOGBOOK.TXT" && !name.ends_with(".TSG") {
105 continue;
106 }
107 let resource_path = entry.path();
108 let kind = entry
109 .file_type()
110 .map_err(|e| file_error(&resource_path, e.to_string()))?;
111 if !kind.is_file() {
112 return Err(file_error(&resource_path, "expected a regular file".into()));
113 }
114 if resources.insert(name, resource_path.clone()).is_some() {
115 return Err(file_error(
116 &resource_path,
117 "duplicate resource filename".into(),
118 ));
119 }
120 }
121 let read = |path: &Path| std::fs::read(path).map_err(|e| file_error(path, e.to_string()));
122 let parse = |path: &Path| {
123 parse_savegame_file(&read(path)?).map_err(|e| file_error(path, e.to_string()))
124 };
125 let game_path = resources
126 .remove("SAVEGAME.SAV")
127 .ok_or_else(|| file_error(path, "missing SAVEGAME.SAV".into()))?;
128 let game = parse(&game_path)?;
129 let logbook = resources
130 .remove("LOGBOOK.TXT")
131 .map(|path| read(&path))
132 .transpose()?;
133 let maps = resources
134 .into_iter()
135 .map(|(name, path)| Ok((name, parse(&path)?)))
136 .collect::<Result<_>>()?;
137 Ok(SaveSlot {
138 game,
139 maps,
140 logbook,
141 })
142}
143
144pub fn encode_savegame_file(sections: &[SaveSection]) -> Result<Vec<u8>> {
149 let invalid = |message: &str| Error::Parse(format!("save game: {message}"));
150 let first = sections
151 .first()
152 .ok_or_else(|| invalid("missing SVGM metadata"))?;
153 if first.name != *b"SVGM" {
154 return Err(invalid("first chunk must be SVGM"));
155 }
156 parse_metadata(&first.data)?;
157 let mut length = 4usize;
158 for (index, section) in sections.iter().enumerate() {
159 if section.name == *b"END " {
160 return Err(invalid("END is a terminator, not a payload section"));
161 }
162 if index != 0 && section.name == *b"SVGM" {
163 return Err(invalid("duplicate SVGM metadata"));
164 }
165 u32::try_from(section.data.len()).map_err(|_| invalid("chunk length exceeds u32"))?;
166 length = length
167 .checked_add(8)
168 .and_then(|n| n.checked_add(section.data.len()))
169 .ok_or_else(|| invalid("container length exceeds address space"))?;
170 }
171 let mut bytes = Vec::new();
172 bytes
173 .try_reserve_exact(length)
174 .map_err(|e| Error::Conversion(format!("save game allocation: {e}")))?;
175 for section in sections {
176 bytes.extend_from_slice(§ion.name);
177 bytes.extend_from_slice(&(section.data.len() as u32).to_be_bytes());
178 bytes.extend_from_slice(§ion.data);
179 }
180 bytes.extend_from_slice(b"END ");
181 Ok(bytes)
182}
183
184fn parse_metadata(data: &[u8]) -> Result<SaveMetadata> {
185 let invalid = |message: &str| Error::Parse(format!("save game: {message}"));
186 if data.len() != 272 {
187 return Err(invalid("SVGM payload must be 272 bytes"));
188 }
189 if &data[..4] != b"1.31" {
190 return Err(invalid("unsupported save version; expected 1.31"));
191 }
192 let text = |raw: &[u8]| {
193 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
194 String::from_utf8_lossy(&raw[..end]).into_owned()
195 };
196 let number = |offset| u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
197 Ok(SaveMetadata {
198 version: data[..4].try_into().unwrap(),
199 unknown_04: number(4),
200 unknown_08: number(8),
201 description: text(&data[12..140]),
202 world_id: number(140),
203 bitmap_filename: text(&data[144..272]),
204 })
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
209pub struct SaveFlags {
210 pub values: Vec<i32>,
211 pub states: Vec<u8>,
213}
214
215pub fn parse_save_flags(data: &[u8]) -> Result<SaveFlags> {
217 let invalid = || Error::Parse("save game SVGF: invalid count or payload length".into());
218 let count = u32::from_le_bytes(data.get(..4).ok_or_else(invalid)?.try_into().unwrap());
219 let count = usize::try_from(count).map_err(|_| invalid())?;
220 let expected = count
221 .checked_mul(5)
222 .and_then(|n| n.checked_add(4))
223 .ok_or_else(invalid)?;
224 if data.len() != expected {
225 return Err(invalid());
226 }
227 let end = 4 + count * 4;
228 Ok(SaveFlags {
229 values: data[4..end]
230 .as_chunks::<4>()
231 .0
232 .iter()
233 .map(|value| i32::from_le_bytes(*value))
234 .collect(),
235 states: data[end..].to_vec(),
236 })
237}
238
239pub fn encode_save_flags(flags: &SaveFlags) -> Result<Vec<u8>> {
241 if flags.values.len() != flags.states.len() {
242 return Err(Error::Parse(
243 "save game SVGF: flag and state counts differ".into(),
244 ));
245 }
246 let count = u32::try_from(flags.values.len())
247 .map_err(|_| Error::Parse("save game SVGF: count exceeds u32".into()))?;
248 let length = flags
249 .values
250 .len()
251 .checked_mul(5)
252 .and_then(|n| n.checked_add(4))
253 .ok_or_else(|| Error::Parse("save game SVGF: payload exceeds address space".into()))?;
254 let mut data = Vec::new();
255 data.try_reserve_exact(length)
256 .map_err(|e| Error::Conversion(format!("save game SVGF allocation: {e}")))?;
257 data.extend_from_slice(&count.to_le_bytes());
258 for value in &flags.values {
259 data.extend_from_slice(&value.to_le_bytes());
260 }
261 data.extend_from_slice(&flags.states);
262 Ok(data)
263}