1use crate::zlib_reader::ChunkedZLibReader;
4use crate::SessionVisiblity::{SvFriendsOnly, SvInvalid, SvPrivate};
5use anyhow::{Error, Result};
6use byteorder::{LittleEndian as L, ReadBytesExt};
7use chrono::{DateTime, Duration, TimeZone, Utc};
8use std::collections::HashMap;
9use std::convert::TryInto;
10use std::io::{Read, Seek};
11
12pub mod zlib_reader;
13
14#[derive(Debug, Clone, PartialEq)]
16pub struct SaveFile {
17 pub save_header: i32,
18 pub save_version: i32,
19 pub build_version: i32,
20 pub world_type: String,
21 pub world_properties: WorldProperties,
22 pub session_name: String,
23 pub play_time: Duration,
24 pub save_date: DateTime<Utc>,
25 pub session_visibility: SessionVisiblity,
26 pub editor_object_version: i32,
27 pub mod_meta_data: String,
28 pub is_modded_save: bool,
29 pub save_objects: Vec<SaveObject>,
30}
31
32impl SaveFile {
33 pub fn parse<R>(file: &mut R) -> Result<SaveFile>
43 where
44 R: Read + Seek,
45 {
46 let mut save_file = SaveFile {
50 save_header: file.read_i32::<L>()?,
51 save_version: file.read_i32::<L>()?,
52 build_version: file.read_i32::<L>()?,
53 world_type: read_string(file)?,
54 world_properties: WorldProperties::parse(&read_string(file)?)?,
55 session_name: read_string(file)?,
56 play_time: Duration::seconds(file.read_i32::<L>()?.try_into()?),
57 save_date: SaveFile::convert_date(file.read_i64::<L>()?),
58 session_visibility: SessionVisiblity::from_u8(file.read_u8()?)?,
59 editor_object_version: file.read_i32::<L>()?,
60 mod_meta_data: read_string(file)?,
61 is_modded_save: file.read_i32::<L>()? > 0,
62 save_objects: Vec::new(),
63 };
64
65 let mut decoder = ChunkedZLibReader::new(file)?;
66 let world_object_count = decoder.read_u32::<L>()?;
67 save_file.save_objects.reserve(world_object_count as usize);
68 for _ in 0..world_object_count {
69 save_file
70 .save_objects
71 .push(SaveObject::parse(&mut decoder)?);
72 }
73 Ok(save_file)
74 }
75
76 fn zero_date() -> DateTime<Utc> {
77 chrono::Utc.ymd(1, 1, 1).and_hms(12, 0, 0)
78 }
79
80 fn convert_date(n: i64) -> DateTime<Utc> {
81 SaveFile::zero_date() + Duration::nanoseconds(n) * 100
82 }
83}
84
85impl Default for SaveFile {
86 fn default() -> Self {
87 Self {
88 save_header: Default::default(),
89 save_version: Default::default(),
90 build_version: Default::default(),
91 world_type: Default::default(),
92 world_properties: Default::default(),
93 session_name: Default::default(),
94 play_time: Duration::zero(),
95 save_date: SaveFile::zero_date(),
96 session_visibility: Default::default(),
97 editor_object_version: Default::default(),
98 mod_meta_data: Default::default(),
99 is_modded_save: Default::default(),
100 save_objects: Default::default(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Default)]
106pub struct WorldProperties {
107 pub start_loc: String,
108 pub session_name: String,
109 pub visibility: SessionVisiblity,
110}
111
112impl WorldProperties {
113 pub fn parse(s: &str) -> Result<WorldProperties> {
114 let mut map: HashMap<&str, &str> = s
115 .split('?')
116 .skip(1) .map(|s| {
118 s.split_once("=")
119 .ok_or_else(|| Error::msg(format!("invalid property: {}", s)))
120 })
121 .collect::<Result<HashMap<&str, &str>>>()?;
122
123 let not_found_error = || Error::msg("property not found");
124 Ok(WorldProperties {
125 start_loc: map
126 .remove("startloc")
127 .ok_or_else(not_found_error)?
128 .to_string(),
129 session_name: map
130 .remove("sessionName")
131 .ok_or_else(not_found_error)?
132 .to_string(),
133 visibility: SessionVisiblity::parse(
134 map.remove("Visibility").ok_or_else(not_found_error)?,
135 )?,
136 })
137 }
138}
139
140#[derive(Debug, Copy, Clone, Eq, PartialEq)]
141pub enum SessionVisiblity {
142 SvPrivate,
143 SvFriendsOnly,
144 SvInvalid,
145}
146
147impl SessionVisiblity {
148 pub fn from_u8(n: u8) -> Result<SessionVisiblity> {
149 Ok(match n {
150 0 => SvPrivate,
151 1 => SvFriendsOnly,
152 2 => SvInvalid,
153 _ => return Err(Error::msg(format!("invalid n: {}", n))),
154 })
155 }
156
157 pub fn parse(s: &str) -> Result<SessionVisiblity> {
158 Ok(match s {
159 "SV_Private" => SvPrivate,
160 "SV_FriendsOnly" => SvFriendsOnly,
161 "SV_Invalid" => SvInvalid,
162 _ => return Err(Error::msg(format!("invalid s: {}", s))),
163 })
164 }
165}
166
167impl Default for SessionVisiblity {
168 fn default() -> Self {
169 SvPrivate
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub enum SaveObject {
175 SaveComponent {
176 type_path: String,
177 root_object: String,
178 instance_name: String,
179 parent_entity_name: String,
180 },
181 SaveEntity {
182 type_path: String,
183 root_object: String,
184 instance_name: String,
185 need_transform: bool,
186 rotation: Vector4,
187 position: Vector3,
188 scale: Vector3,
189 was_placed_in_level: bool,
190 },
191}
192
193impl SaveObject {
194 pub fn parse<R>(file: &mut R) -> Result<Self>
195 where
196 R: Read,
197 {
198 let object_type = file.read_i32::<L>()?;
199 Ok(match object_type {
200 0 => SaveObject::SaveComponent {
201 type_path: read_string(file)?,
202 root_object: read_string(file)?,
203 instance_name: read_string(file)?,
204 parent_entity_name: read_string(file)?,
205 },
206 1 => SaveObject::SaveEntity {
207 type_path: read_string(file)?,
208 root_object: read_string(file)?,
209 instance_name: read_string(file)?,
210 need_transform: file.read_i32::<L>()? == 1,
211 rotation: Vector4::parse(file)?,
212 position: Vector3::parse(file)?,
213 scale: Vector3::parse(file)?,
214 was_placed_in_level: file.read_i32::<L>()? == 1,
215 },
216 n => return Err(Error::msg(format!("unknown object type: {}", n))),
217 })
218 }
219}
220
221pub fn read_string<R>(file: &mut R) -> Result<String>
222where
223 R: Read,
224{
225 const MAX_LENGTH: usize = 0x1000;
226 let length_error = || Error::msg("invalid length");
227 let signed_length = file.read_i32::<L>()?;
228
229 Ok(if signed_length < 0 {
230 if signed_length == i32::MIN {
232 return Err(length_error());
233 }
234
235 let mut buffer: Vec<u16> = Vec::new();
236 let length = ((-signed_length) as usize).saturating_sub(1) / 2;
237 if length > MAX_LENGTH {
238 return Err(length_error());
239 }
240 buffer.resize(length, 0);
241 file.read_u16_into::<L>(&mut buffer)?;
242 String::from_utf16_lossy(&buffer)
243 } else {
244 let mut buffer: Vec<u8> = Vec::new();
245 let length = (signed_length as usize).saturating_sub(1);
246 if length > MAX_LENGTH {
247 return Err(length_error());
248 }
249 buffer.resize(length, b'\0');
250 file.read_exact(&mut buffer)?;
251 if length > 0 {
252 file.read_u8()?;
254 }
255 String::from_utf8_lossy(&buffer).into_owned()
256 })
257}
258
259#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
260pub struct Vector2 {
261 pub x: f32,
262 pub y: f32,
263}
264
265impl Vector2 {
266 pub fn parse<R>(file: &mut R) -> Result<Self>
267 where
268 R: Read,
269 {
270 Ok(Self {
271 x: file.read_f32::<L>()?,
272 y: file.read_f32::<L>()?,
273 })
274 }
275}
276
277#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
278pub struct Vector3 {
279 pub x: f32,
280 pub y: f32,
281 pub z: f32,
282}
283
284impl Vector3 {
285 pub fn parse<R>(file: &mut R) -> Result<Self>
286 where
287 R: Read,
288 {
289 Ok(Self {
290 x: file.read_f32::<L>()?,
291 y: file.read_f32::<L>()?,
292 z: file.read_f32::<L>()?,
293 })
294 }
295}
296
297#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd)]
298pub struct Vector4 {
299 pub x: f32,
300 pub y: f32,
301 pub z: f32,
302 pub w: f32,
303}
304
305impl Vector4 {
306 pub fn parse<R>(file: &mut R) -> Result<Self>
307 where
308 R: Read,
309 {
310 Ok(Self {
311 x: file.read_f32::<L>()?,
312 y: file.read_f32::<L>()?,
313 z: file.read_f32::<L>()?,
314 w: file.read_f32::<L>()?,
315 })
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::fs::File;
323 use std::io::{BufReader, Cursor};
324 use std::iter::once;
325
326 #[test]
327 fn parse() {
328 env_logger::builder().is_test(true).try_init().unwrap();
329 let mut file = File::open("test_files/new_world.sav").unwrap();
330 let save_file = SaveFile::parse(&mut file).unwrap();
331 assert_eq!(save_file.save_header, 8);
332 assert_eq!(save_file.save_version, 25);
333 assert_eq!(save_file.build_version, 152331);
334 assert_eq!(save_file.world_type, "Persistent_Level");
335 assert_eq!(save_file.session_name, "test_file");
336 assert_eq!(save_file.save_objects.len(), 13920);
337 assert!(matches!(
338 &save_file.save_objects[0],
339 SaveObject::SaveEntity { type_path, .. }
340 if type_path == "/Script/FactoryGame.FGFoliageRemoval"
341 ));
342
343 SaveFile::parse(&mut File::open("test_files/test_save2.sav").unwrap()).unwrap();
344
345 let file = File::open("test_files/new_world.sav").unwrap();
347 assert!(SaveFile::parse(&mut BufReader::new(file)).is_err());
348 }
349
350 #[test]
351 fn world_properties() {
352 assert!(WorldProperties::parse("").is_err());
353 let string = "?startloc=Grass Fields?sessionName=test_file?Visibility=SV_Private";
354 let result = WorldProperties::parse(string).unwrap();
355 assert_eq!(result.start_loc, "Grass Fields");
356 assert_eq!(result.session_name, "test_file");
357 assert_eq!(result.visibility, SessionVisiblity::SvPrivate);
358 }
359
360 fn to_encoding(b: &[u8]) -> Vec<u8> {
361 (b.len() as i32 + 1) .to_le_bytes()
363 .iter()
364 .chain(b.iter()) .chain(once(&b'\0')) .copied()
367 .collect()
368 }
369
370 #[test]
371 fn test_read_string() {
372 {
373 assert!(read_string(&mut &Vec::new()[..]).is_err());
375 }
376 {
377 let mut data = &0_i32.to_le_bytes()[..];
379 assert_eq!(read_string(&mut data).unwrap(), "");
380 }
381
382 let cases: &[&[u8]] = &[
384 &i32::MIN.to_le_bytes()[..],
385 &(i32::MIN + 1).to_le_bytes()[..],
386 &i32::MAX.to_le_bytes()[..],
387 ];
388 for data in cases {
389 assert!(read_string(&mut &data.to_vec()[..]).is_err());
390 }
391
392 for test_string in &["", "a", "abc"] {
394 let encoded = to_encoding(test_string.as_bytes());
395 assert_eq!(read_string(&mut encoded.as_slice()).unwrap(), *test_string);
396 }
397 {
398 let test_string = "abc";
400 let utf16: Vec<u16> = test_string.encode_utf16().collect();
401 let mut utf16_bytes: Vec<u8> = Vec::new();
402 for n in utf16 {
403 utf16_bytes.extend_from_slice(&n.to_le_bytes());
404 }
405 let encoded: Vec<u8> = (-(utf16_bytes.len() as i32 + 2))
406 .to_le_bytes()
407 .iter()
408 .chain(utf16_bytes.iter())
409 .chain([b'\0', b'\0'].iter())
410 .copied()
411 .collect();
412 assert_eq!(read_string(&mut encoded.as_slice()).unwrap(), test_string);
413 }
414 }
415}