#![warn(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
#![warn(missing_docs)]
pub mod cursor_ext;
pub mod custom_version;
pub mod engine_version;
pub mod error;
pub mod game_version;
pub mod object_version;
mod ord_ext;
pub mod properties;
pub mod savegame_version;
pub(crate) mod scoped_stack_entry;
pub mod types;
use std::io::{Cursor, SeekFrom};
use std::{
collections::HashMap,
fmt::Debug,
io::{Read, Seek, Write},
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use crate::{
cursor_ext::{ReadExt, WriteExt},
custom_version::FCustomVersion,
engine_version::FEngineVersion,
error::{DeserializeError, Error},
game_version::{DeserializedGameVersion, GameVersion, PalworldCompressionType, PLZ_MAGIC},
object_version::EUnrealEngineObjectUE5Version,
ord_ext::OrdExt,
properties::{Property, PropertyOptions, PropertyTrait},
savegame_version::SaveGameVersion,
types::{map::HashableIndexMap, Guid},
};
pub const FILE_TYPE_GVAS: u32 = u32::from_le_bytes([b'G', b'V', b'A', b'S']);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
pub enum GvasHeader {
Version2 {
package_file_version: u32,
engine_version: FEngineVersion,
custom_version_format: u32,
custom_versions: HashableIndexMap<Guid, u32>,
save_game_class_name: String,
},
Version3 {
package_file_version: u32,
package_file_version_ue5: u32,
engine_version: FEngineVersion,
custom_version_format: u32,
custom_versions: HashableIndexMap<Guid, u32>,
save_game_class_name: String,
},
}
impl GvasHeader {
pub fn read<R: Read + Seek>(cursor: &mut R) -> Result<Self, Error> {
let file_type_tag = cursor.read_u32::<LittleEndian>()?;
if file_type_tag != FILE_TYPE_GVAS {
Err(DeserializeError::InvalidHeader(
format!("File type {file_type_tag} not recognized").into_boxed_str(),
))?
}
let save_game_file_version = cursor.read_u32::<LittleEndian>()?;
if !save_game_file_version.between(
SaveGameVersion::AddedCustomVersions as u32,
SaveGameVersion::PackageFileSummaryVersionChange as u32,
) {
Err(DeserializeError::InvalidHeader(
format!("GVAS version {save_game_file_version} not supported").into_boxed_str(),
))?
}
let package_file_version = cursor.read_u32::<LittleEndian>()?;
if !package_file_version.between(0x205, 0x20D) {
Err(DeserializeError::InvalidHeader(
format!("Package file version {package_file_version} not supported")
.into_boxed_str(),
))?
}
let package_file_version_ue5 = if save_game_file_version
>= SaveGameVersion::PackageFileSummaryVersionChange as u32
{
let version = cursor.read_u32::<LittleEndian>()?;
if !version.between(
EUnrealEngineObjectUE5Version::InitialVersion as u32,
EUnrealEngineObjectUE5Version::DataResources as u32,
) {
Err(DeserializeError::InvalidHeader(
format!("UE5 Package file version {version} is not supported").into_boxed_str(),
))?
}
Some(version)
} else {
None
};
let engine_version = FEngineVersion::read(cursor)?;
let custom_version_format = cursor.read_u32::<LittleEndian>()?;
if custom_version_format != 3 {
Err(DeserializeError::InvalidHeader(
format!("Custom version format {custom_version_format} not supported")
.into_boxed_str(),
))?
}
let custom_versions_len = cursor.read_u32::<LittleEndian>()?;
let mut custom_versions = HashableIndexMap::with_capacity(custom_versions_len as usize);
for _ in 0..custom_versions_len {
let FCustomVersion { key, version } = FCustomVersion::read(cursor)?;
custom_versions.insert(key, version);
}
let save_game_class_name = cursor.read_string()?;
Ok(match package_file_version_ue5 {
None => GvasHeader::Version2 {
package_file_version,
engine_version,
custom_version_format,
custom_versions,
save_game_class_name,
},
Some(package_file_version_ue5) => GvasHeader::Version3 {
package_file_version,
package_file_version_ue5,
engine_version,
custom_version_format,
custom_versions,
save_game_class_name,
},
})
}
pub fn write<W: Write>(&self, cursor: &mut W) -> Result<usize, Error> {
cursor.write_u32::<LittleEndian>(FILE_TYPE_GVAS)?;
match self {
GvasHeader::Version2 {
package_file_version,
engine_version,
custom_version_format,
custom_versions,
save_game_class_name,
} => {
let mut len = 20;
cursor.write_u32::<LittleEndian>(2)?;
cursor.write_u32::<LittleEndian>(*package_file_version)?;
len += engine_version.write(cursor)?;
cursor.write_u32::<LittleEndian>(*custom_version_format)?;
cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
for (&key, &version) in custom_versions {
len += FCustomVersion::new(key, version).write(cursor)?;
}
len += cursor.write_string(save_game_class_name)?;
Ok(len)
}
GvasHeader::Version3 {
package_file_version,
package_file_version_ue5,
engine_version,
custom_version_format,
custom_versions,
save_game_class_name,
} => {
let mut len = 24;
cursor.write_u32::<LittleEndian>(3)?;
cursor.write_u32::<LittleEndian>(*package_file_version)?;
cursor.write_u32::<LittleEndian>(*package_file_version_ue5)?;
len += engine_version.write(cursor)?;
cursor.write_u32::<LittleEndian>(*custom_version_format)?;
cursor.write_u32::<LittleEndian>(custom_versions.len() as u32)?;
for (&key, &version) in custom_versions {
len += FCustomVersion::new(key, version).write(cursor)?
}
len += cursor.write_string(save_game_class_name)?;
Ok(len)
}
}
}
pub fn get_custom_versions(&self) -> &HashableIndexMap<Guid, u32> {
match self {
GvasHeader::Version2 {
custom_versions, ..
} => custom_versions,
GvasHeader::Version3 {
custom_versions, ..
} => custom_versions,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GvasFile {
#[cfg_attr(
feature = "serde",
serde(default, skip_serializing_if = "DeserializedGameVersion::is_default")
)]
pub deserialized_game_version: DeserializedGameVersion,
pub header: GvasHeader,
pub properties: HashableIndexMap<String, Property>,
}
impl GvasFile {
pub fn read<R: Read + Seek>(cursor: &mut R, game_version: GameVersion) -> Result<Self, Error> {
let hints = HashMap::new();
Self::read_with_hints(cursor, game_version, &hints)
}
pub fn read_with_hints<R: Read + Seek>(
cursor: &mut R,
game_version: GameVersion,
hints: &HashMap<String, String>,
) -> Result<Self, Error> {
let deserialized_game_version: DeserializedGameVersion;
let mut cursor = match game_version {
GameVersion::Default => {
deserialized_game_version = DeserializedGameVersion::Default;
let mut data = Vec::new();
cursor.read_to_end(&mut data)?;
Cursor::new(data)
}
GameVersion::Palworld => {
let decompresed_length = cursor.read_u32::<LittleEndian>()?;
let _compressed_length = cursor.read_u32::<LittleEndian>()?;
let mut magic = [0u8; 3];
cursor.read_exact(&mut magic)?;
if &magic != PLZ_MAGIC {
Err(DeserializeError::InvalidHeader(
format!("Invalid PlZ magic {magic:?}").into_boxed_str(),
))?
}
let compression_type = cursor.read_enum()?;
deserialized_game_version = DeserializedGameVersion::Palworld(compression_type);
match compression_type {
PalworldCompressionType::None => {
let mut data = vec![0u8; decompresed_length as usize];
cursor.read_exact(&mut data)?;
Cursor::new(data)
}
PalworldCompressionType::Zlib => {
let mut zlib_data = vec![0u8; decompresed_length as usize];
let mut decoder = ZlibDecoder::new(cursor);
decoder.read_exact(&mut zlib_data)?;
Cursor::new(zlib_data)
}
PalworldCompressionType::ZlibTwice => {
let decoder = ZlibDecoder::new(cursor);
let mut decoder = ZlibDecoder::new(decoder);
let mut zlib_data = Vec::new();
decoder.read_to_end(&mut zlib_data)?;
Cursor::new(zlib_data)
}
}
}
};
let header = GvasHeader::read(&mut cursor)?;
let mut options = PropertyOptions {
hints,
properties_stack: &mut vec![],
custom_versions: header.get_custom_versions(),
};
let mut properties = HashableIndexMap::new();
loop {
let property_name = cursor.read_string()?;
if property_name == "None" {
break;
}
let property_type = cursor.read_string()?;
options.properties_stack.push(property_name.clone());
let property = Property::new(&mut cursor, &property_type, true, &mut options, None)?;
properties.insert(property_name, property);
let _ = options.properties_stack.pop();
}
Ok(GvasFile {
deserialized_game_version,
header,
properties,
})
}
pub fn write<W: Write + Seek>(&self, cursor: &mut W) -> Result<(), Error> {
let mut writing_cursor = Cursor::new(Vec::new());
self.header.write(&mut writing_cursor)?;
let mut options = PropertyOptions {
hints: &HashMap::new(),
properties_stack: &mut vec![],
custom_versions: self.header.get_custom_versions(),
};
for (name, property) in &self.properties {
writing_cursor.write_string(name)?;
property.write(&mut writing_cursor, true, &mut options)?;
}
writing_cursor.write_string("None")?;
writing_cursor.write_i32::<LittleEndian>(0)?;
match self.deserialized_game_version {
DeserializedGameVersion::Default => cursor.write_all(&writing_cursor.into_inner())?,
DeserializedGameVersion::Palworld(compression_type) => {
let decompressed = writing_cursor.into_inner();
cursor.write_u32::<LittleEndian>(decompressed.len() as u32)?;
let compressed_length_pos = cursor.stream_position()?;
cursor.write_u32::<LittleEndian>(0)?; cursor.write_all(PLZ_MAGIC)?;
cursor.write_enum(compression_type)?;
match compression_type {
PalworldCompressionType::None => cursor.write_all(&decompressed)?,
PalworldCompressionType::Zlib => {
let mut encoder = ZlibEncoder::new(cursor.by_ref(), Compression::new(6));
encoder.write_all(&decompressed)?;
encoder.finish()?;
}
PalworldCompressionType::ZlibTwice => {
let encoder = ZlibEncoder::new(cursor.by_ref(), Compression::default());
let mut encoder = ZlibEncoder::new(encoder, Compression::default());
encoder.write_all(&decompressed)?;
encoder.finish()?;
}
}
let end_pos = cursor.stream_position()?;
cursor.seek(SeekFrom::Start(compressed_length_pos))?;
cursor.write_u32::<LittleEndian>((end_pos - (compressed_length_pos + 4)) as u32)?;
cursor.seek(SeekFrom::Start(end_pos))?;
}
}
Ok(())
}
}