1#![warn(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2#![warn(missing_docs)]
3
4pub mod cursor_ext;
65pub mod custom_version;
67pub mod engine_version;
69pub mod error;
71pub mod game_version;
73pub mod object_version;
75mod ord_ext;
77pub mod properties;
79pub mod savegame_version;
81pub(crate) mod scoped_stack_entry;
82pub mod types;
84
85use std::io::{Cursor, SeekFrom};
86use std::{
87 collections::HashMap,
88 fmt::Debug,
89 io::{Read, Seek, Write},
90};
91
92use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
93use flate2::read::ZlibDecoder;
94use flate2::write::ZlibEncoder;
95use flate2::Compression;
96
97use crate::{
98 cursor_ext::{ReadExt, WriteExt},
99 custom_version::FCustomVersion,
100 engine_version::FEngineVersion,
101 error::{DeserializeError, Error},
102 game_version::{DeserializedGameVersion, GameVersion, PalworldCompressionType, PLZ_MAGIC},
103 object_version::EUnrealEngineObjectUE5Version,
104 ord_ext::OrdExt,
105 properties::{Property, PropertyOptions, PropertyTrait},
106 savegame_version::SaveGameVersion,
107 types::{map::HashableIndexMap, Guid},
108};
109
110pub const FILE_TYPE_GVAS: u32 = u32::from_le_bytes([b'G', b'V', b'A', b'S']);
112
113#[derive(Debug, Clone, PartialEq, Eq)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
116#[cfg_attr(feature = "serde", serde(tag = "type"))]
117pub enum GvasHeader {
118 Version2 {
120 package_file_version: u32,
122 engine_version: FEngineVersion,
124 custom_version_format: u32,
126 custom_versions: HashableIndexMap<Guid, u32>,
128 save_game_class_name: String,
130 },
131 Version3 {
133 package_file_version: u32,
135 package_file_version_ue5: u32,
137 engine_version: FEngineVersion,
139 custom_version_format: u32,
141 custom_versions: HashableIndexMap<Guid, u32>,
143 save_game_class_name: String,
145 },
146}
147
148impl GvasHeader {
149 pub fn read<R: Read + Seek>(cursor: &mut R) -> Result<Self, Error> {
171 let file_type_tag = cursor.read_u32::<LittleEndian>()?;
172 if file_type_tag != FILE_TYPE_GVAS {
173 Err(DeserializeError::InvalidHeader(
174 format!("File type {file_type_tag} not recognized").into_boxed_str(),
175 ))?
176 }
177
178 let save_game_file_version = cursor.read_u32::<LittleEndian>()?;
179 if !save_game_file_version.between(
180 SaveGameVersion::AddedCustomVersions as u32,
181 SaveGameVersion::PackageFileSummaryVersionChange as u32,
182 ) {
183 Err(DeserializeError::InvalidHeader(
184 format!("GVAS version {save_game_file_version} not supported").into_boxed_str(),
185 ))?
186 }
187
188 let package_file_version = cursor.read_u32::<LittleEndian>()?;
189 if !package_file_version.between(0x205, 0x20D) {
190 Err(DeserializeError::InvalidHeader(
191 format!("Package file version {package_file_version} not supported")
192 .into_boxed_str(),
193 ))?
194 }
195
196 let package_file_version_ue5 = if save_game_file_version
198 >= SaveGameVersion::PackageFileSummaryVersionChange as u32
199 {
200 let version = cursor.read_u32::<LittleEndian>()?;
201 if !version.between(
202 EUnrealEngineObjectUE5Version::InitialVersion as u32,
203 EUnrealEngineObjectUE5Version::DataResources as u32,
204 ) {
205 Err(DeserializeError::InvalidHeader(
206 format!("UE5 Package file version {version} is not supported").into_boxed_str(),
207 ))?
208 }
209 Some(version)
210 } else {
211 None
212 };
213
214 let engine_version = FEngineVersion::read(cursor)?;
215 let custom_version_format = cursor.read_u32::<LittleEndian>()?;
216 if custom_version_format != 3 {
217 Err(DeserializeError::InvalidHeader(
218 format!("Custom version format {custom_version_format} not supported")
219 .into_boxed_str(),
220 ))?
221 }
222
223 let custom_versions_len = cursor.read_u32::<LittleEndian>()?;
224 let mut custom_versions = HashableIndexMap::with_capacity(custom_versions_len as usize);
225 for _ in 0..custom_versions_len {
226 let FCustomVersion { key, version } = FCustomVersion::read(cursor)?;
227 custom_versions.insert(key, version);
228 }
229
230 let save_game_class_name = cursor.read_string()?;
231
232 Ok(match package_file_version_ue5 {
233 None => GvasHeader::Version2 {
234 package_file_version,
235 engine_version,
236 custom_version_format,
237 custom_versions,
238 save_game_class_name,
239 },
240 Some(package_file_version_ue5) => GvasHeader::Version3 {
241 package_file_version,
242 package_file_version_ue5,
243 engine_version,
244 custom_version_format,
245 custom_versions,
246 save_game_class_name,
247 },
248 })
249 }
250
251 pub fn write<W: Write>(&self, cursor: &mut W) -> Result<usize, Error> {
270 cursor.write_u32::<LittleEndian>(FILE_TYPE_GVAS)?;
271 match self {
272 GvasHeader::Version2 {
273 package_file_version,
274 engine_version,
275 custom_version_format,
276 custom_versions,
277 save_game_class_name,
278 } => {
279 let mut len = 20;
280 cursor.write_u32::<LittleEndian>(2)?;
281 cursor.write_u32::<LittleEndian>(*package_file_version)?;
282 len += engine_version.write(cursor)?;
283 cursor.write_u32::<LittleEndian>(*custom_version_format)?;
284 cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
285 for (&key, &version) in custom_versions {
286 len += FCustomVersion::new(key, version).write(cursor)?;
287 }
288 len += cursor.write_string(save_game_class_name)?;
289 Ok(len)
290 }
291
292 GvasHeader::Version3 {
293 package_file_version,
294 package_file_version_ue5,
295 engine_version,
296 custom_version_format,
297 custom_versions,
298 save_game_class_name,
299 } => {
300 let mut len = 24;
301 cursor.write_u32::<LittleEndian>(3)?;
302 cursor.write_u32::<LittleEndian>(*package_file_version)?;
303 cursor.write_u32::<LittleEndian>(*package_file_version_ue5)?;
304 len += engine_version.write(cursor)?;
305 cursor.write_u32::<LittleEndian>(*custom_version_format)?;
306 cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
307 for (&key, &version) in custom_versions {
308 len += FCustomVersion::new(key, version).write(cursor)?
309 }
310 len += cursor.write_string(save_game_class_name)?;
311 Ok(len)
312 }
313 }
314 }
315
316 pub fn get_custom_versions(&self) -> &HashableIndexMap<Guid, u32> {
318 match self {
319 GvasHeader::Version2 {
320 custom_versions, ..
321 } => custom_versions,
322 GvasHeader::Version3 {
323 custom_versions, ..
324 } => custom_versions,
325 }
326 }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq)]
331#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
332pub struct GvasFile {
333 #[cfg_attr(
335 feature = "serde",
336 serde(default, skip_serializing_if = "DeserializedGameVersion::is_default")
337 )]
338 pub deserialized_game_version: DeserializedGameVersion,
339 pub header: GvasHeader,
341 pub properties: HashableIndexMap<String, Property>,
343}
344
345impl GvasFile {
346 pub fn read<R: Read + Seek>(cursor: &mut R, game_version: GameVersion) -> Result<Self, Error> {
370 let hints = HashMap::new();
371 Self::read_with_hints(cursor, game_version, &hints)
372 }
373
374 pub fn read_with_hints<R: Read + Seek>(
405 cursor: &mut R,
406 game_version: GameVersion,
407 hints: &HashMap<String, String>,
408 ) -> Result<Self, Error> {
409 let deserialized_game_version: DeserializedGameVersion;
410 let mut cursor = match game_version {
411 GameVersion::Default => {
412 deserialized_game_version = DeserializedGameVersion::Default;
413 let mut data = Vec::new();
414 cursor.read_to_end(&mut data)?;
415 Cursor::new(data)
416 }
417 GameVersion::Palworld => {
418 let decompresed_length = cursor.read_u32::<LittleEndian>()?;
419 let _compressed_length = cursor.read_u32::<LittleEndian>()?;
420
421 let mut magic = [0u8; 3];
422 cursor.read_exact(&mut magic)?;
423 if &magic != PLZ_MAGIC {
424 Err(DeserializeError::InvalidHeader(
425 format!("Invalid PlZ magic {magic:?}").into_boxed_str(),
426 ))?
427 }
428
429 let compression_type = cursor.read_enum()?;
430
431 deserialized_game_version = DeserializedGameVersion::Palworld(compression_type);
432
433 match compression_type {
434 PalworldCompressionType::None => {
435 let mut data = vec![0u8; decompresed_length as usize];
436
437 cursor.read_exact(&mut data)?;
438 Cursor::new(data)
439 }
440 PalworldCompressionType::Zlib => {
441 let mut zlib_data = vec![0u8; decompresed_length as usize];
442
443 let mut decoder = ZlibDecoder::new(cursor);
444 decoder.read_exact(&mut zlib_data)?;
445
446 Cursor::new(zlib_data)
447 }
448 PalworldCompressionType::ZlibTwice => {
449 let decoder = ZlibDecoder::new(cursor);
450 let mut decoder = ZlibDecoder::new(decoder);
451
452 let mut zlib_data = Vec::new();
453 decoder.read_to_end(&mut zlib_data)?;
454
455 Cursor::new(zlib_data)
456 }
457 }
458 }
459 };
460
461 let header = GvasHeader::read(&mut cursor)?;
462
463 let mut options = PropertyOptions {
464 hints,
465 properties_stack: &mut vec![],
466 custom_versions: header.get_custom_versions(),
467 };
468
469 let mut properties = HashableIndexMap::new();
470 loop {
471 let property_name = cursor.read_string()?;
472 if property_name == "None" {
473 break;
474 }
475
476 let property_type = cursor.read_string()?;
477
478 options.properties_stack.push(property_name.clone());
479
480 let property = Property::new(&mut cursor, &property_type, true, &mut options, None)?;
481 properties.insert(property_name, property);
482
483 let _ = options.properties_stack.pop();
484 }
485
486 Ok(GvasFile {
487 deserialized_game_version,
488 header,
489 properties,
490 })
491 }
492
493 pub fn write<W: Write + Seek>(&self, cursor: &mut W) -> Result<(), Error> {
518 let mut writing_cursor = Cursor::new(Vec::new());
519
520 self.header.write(&mut writing_cursor)?;
521
522 let mut options = PropertyOptions {
523 hints: &HashMap::new(),
524 properties_stack: &mut vec![],
525 custom_versions: self.header.get_custom_versions(),
526 };
527
528 for (name, property) in &self.properties {
529 writing_cursor.write_string(name)?;
530 property.write(&mut writing_cursor, true, &mut options)?;
531 }
532 writing_cursor.write_string("None")?;
533 writing_cursor.write_i32::<LittleEndian>(0)?; match self.deserialized_game_version {
536 DeserializedGameVersion::Default => cursor.write_all(&writing_cursor.into_inner())?,
537 DeserializedGameVersion::Palworld(compression_type) => {
538 let decompressed = writing_cursor.into_inner();
539
540 cursor.write_u32::<LittleEndian>(decompressed.len() as u32)?;
541 let compressed_length_pos = cursor.stream_position()?;
542 cursor.write_u32::<LittleEndian>(0)?; cursor.write_all(PLZ_MAGIC)?;
544 cursor.write_enum(compression_type)?;
545
546 match compression_type {
548 PalworldCompressionType::None => cursor.write_all(&decompressed)?,
549 PalworldCompressionType::Zlib => {
550 let mut encoder = ZlibEncoder::new(cursor.by_ref(), Compression::new(6));
551 encoder.write_all(&decompressed)?;
552 encoder.finish()?;
553 }
554 PalworldCompressionType::ZlibTwice => {
555 let encoder = ZlibEncoder::new(cursor.by_ref(), Compression::default());
556 let mut encoder = ZlibEncoder::new(encoder, Compression::default());
557 encoder.write_all(&decompressed)?;
558 encoder.finish()?;
559 }
560 }
561
562 let end_pos = cursor.stream_position()?;
564 cursor.seek(SeekFrom::Start(compressed_length_pos))?;
565 cursor.write_u32::<LittleEndian>((end_pos - (compressed_length_pos + 4)) as u32)?;
566 cursor.seek(SeekFrom::Start(end_pos))?;
567 }
568 }
569 Ok(())
570 }
571}