diff --git a/Cargo.toml b/Cargo.toml index 0d057a2..bfa1b90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,21 @@ [package] name = "gvas" -description = "Crate for parsing UE4 gvas save files." +description = "Crate for reading and writing UE4+ save files." authors = ["localcc ", "Scott Anderson"] keywords = ["ue4", "unrealengine", "ue5", "gvas", "uasset"] homepage = "https://github.com/localcc/gvas" repository = "https://github.com/localcc/gvas" readme = "README.md" license = "MIT" -version = "0.10.0" +version = "0.11.0" edition = "2024" [dependencies] -enum_dispatch = "0.3.13" -byteorder = "1.5.0" -ordered-float = "5.1.0" -serde = { version = "1.0.228", optional = true, features = ["derive"] } -serde_with = { version = "3.15.1", optional = true, features = ["hex"] } -indexmap = "2.12.0" -thiserror = "2.0.17" -num_enum = "0.7.5" -flate2 = "1.1.5" -cfg_eval = "0.1.2" +binrw = "0.15.1" +chrono = "0.4.45" +modular-bitfield = "0.13.1" +thiserror = "2.0.18" +flate2 = { version="1.1.5", optional = true } [features] -serde = ["dep:serde", "dep:serde_with", "ordered-float/serde", "indexmap/serde"] - -[dev-dependencies] -serde_json = { version = "1.0.145", features = ["float_roundtrip", "preserve_order"] } - -[[test]] -name = "serde" -required-features = ["serde"] +palworld = ["dep:flate2"] diff --git a/resources/test/complete_property_tag.sav b/resources/test/complete_property_tag.sav new file mode 100644 index 0000000..729db80 Binary files /dev/null and b/resources/test/complete_property_tag.sav differ diff --git a/resources/test/medieval_dynasty.sav b/resources/test/medieval_dynasty.sav new file mode 100644 index 0000000..5bf0175 Binary files /dev/null and b/resources/test/medieval_dynasty.sav differ diff --git a/src/cursor_ext.rs b/src/cursor_ext.rs deleted file mode 100644 index 8fc0c44..0000000 --- a/src/cursor_ext.rs +++ /dev/null @@ -1,201 +0,0 @@ -use std::io::{Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - error::{DeserializeError, Error}, - types::Guid, -}; - -/// Extensions for `Read`. -pub trait ReadExt { - /// Reads a GVAS string. - fn read_string(&mut self) -> Result; - /// Reads a GVAS string. - fn read_fstring(&mut self) -> Result, Error>; - /// Reads a GUID. - fn read_guid(&mut self) -> Result; - /// Reads an 8bit boolean value. - fn read_bool(&mut self) -> Result; - /// Reads a 32bit boolean value. - fn read_b32(&mut self) -> Result; - /// Reads an 8bit enum value. - fn read_enum(&mut self) -> Result - where - T: TryFrom; -} - -/// Extensions for `Write`. -pub trait WriteExt { - /// Writes a GVAS string. - fn write_string>(&mut self, v: T) -> Result; - /// Writes a GVAS string. - fn write_fstring(&mut self, v: Option<&str>) -> Result; - /// Writes a GUID. - fn write_guid(&mut self, v: &Guid) -> Result<(), Error>; - /// Writes an 8bit boolean value. - fn write_bool(&mut self, v: bool) -> Result<(), Error>; - /// Writes a 32bit boolean value. - fn write_b32(&mut self, v: bool) -> Result<(), Error>; - /// Writes an 8bit enum value. - fn write_enum(&mut self, v: T) -> Result<(), Error> - where - T: Into + std::fmt::Debug; -} - -impl ReadExt for R { - #[inline] - fn read_string(&mut self) -> Result { - let start_position = self.stream_position()?; - match self.read_fstring()? { - Some(str) => Ok(str), - None => Err(DeserializeError::InvalidString(0, start_position))?, - } - } - - #[inline] - fn read_fstring(&mut self) -> Result, Error> { - let start_position = self.stream_position()?; - let len = self.read_i32::()?; - - if !(-131072..=131072).contains(&len) { - Err(DeserializeError::InvalidString(len, start_position))? - } else if len == 0 { - Ok(None) - } else if len < 0 { - let mut buf = vec![0u16; -len as usize - 1]; - self.read_u16_into::(&mut buf)?; - - let terminator = self.read_u16::()?; - if terminator != 0 { - Err(DeserializeError::InvalidStringTerminator( - terminator, - self.stream_position()?, - ))? - } - - let string = String::from_utf16(&buf[..]) - .map_err(|e| DeserializeError::FromUtf16Error(e, start_position))?; - - Ok(Some(string)) - } else { - let mut buf = vec![0u8; len as usize - 1]; - self.read_exact(&mut buf)?; - - let terminator = self.read_u8()?; - if terminator != 0 { - Err(DeserializeError::InvalidStringTerminator( - terminator as u16, - self.stream_position()?, - ))? - } - - let string = String::from_utf8(buf) - .map_err(|e| DeserializeError::FromUtf8Error(e, start_position))?; - - Ok(Some(string)) - } - } - - #[inline] - fn read_guid(&mut self) -> Result { - let mut guid = Guid::default(); - self.read_exact(&mut guid.0)?; - Ok(guid) - } - - #[inline] - fn read_bool(&mut self) -> Result { - match self.read_u8()? { - 0 => Ok(false), - 1 => Ok(true), - value => Err(DeserializeError::InvalidBoolean( - value as u32, - self.stream_position()?, - ))?, - } - } - - #[inline] - fn read_b32(&mut self) -> Result { - match self.read_u32::()? { - 0 => Ok(false), - 1 => Ok(true), - value => Err(DeserializeError::InvalidBoolean( - value, - self.stream_position()?, - ))?, - } - } - - #[inline] - fn read_enum(&mut self) -> Result - where - T: TryFrom, - { - let value = self.read_i8()?; - let result = T::try_from(value).map_err(|_| { - let name = std::any::type_name::(); - DeserializeError::invalid_enum_value(name, value, self) - })?; - Ok(result) - } -} - -impl WriteExt for W { - #[inline] - fn write_string>(&mut self, v: T) -> Result { - let v = v.as_ref(); - if v.is_ascii() { - // ASCII strings do not require encoding - let len = v.len() + 1; - self.write_i32::(len as i32)?; - let _ = self.write(v.as_bytes())?; - let _ = self.write(&[0u8; 1])?; - Ok(len * 2 + 4) - } else { - // Perform UTF-16 encoding when non-ASCII characters are detected - let words: Vec = v.encode_utf16().collect(); - let len = words.len() + 1; - self.write_i32::(-(len as i32))?; - for word in words { - self.write_u16::(word)?; - } - self.write_u16::(0u16)?; - Ok(len * 2 + 4) - } - } - - fn write_fstring(&mut self, v: Option<&str>) -> Result { - match v { - Some(str) => self.write_string(str), - None => { - self.write_i32::(0)?; - Ok(4) - } - } - } - - #[inline] - fn write_guid(&mut self, v: &Guid) -> Result<(), Error> { - Ok(self.write_all(&v.0)?) - } - - #[inline] - fn write_bool(&mut self, v: bool) -> Result<(), Error> { - Ok(self.write_u8(if v { 1 } else { 0 })?) - } - - #[inline] - fn write_b32(&mut self, v: bool) -> Result<(), Error> { - Ok(self.write_u32::(if v { 1 } else { 0 })?) - } - - #[inline] - fn write_enum(&mut self, v: T) -> Result<(), Error> - where - T: std::fmt::Debug + Into, - { - Ok(self.write_i8(v.into())?) - } -} diff --git a/src/custom_version.rs b/src/custom_version.rs deleted file mode 100644 index b4755c2..0000000 --- a/src/custom_version.rs +++ /dev/null @@ -1,436 +0,0 @@ -//! Custom version information - -use crate::cursor_ext::{ReadExt, WriteExt}; -use crate::engine_version::EngineVersion; -use crate::error::Error; -use crate::types::Guid; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use num_enum::IntoPrimitive; -use std::io::{Read, Seek, Write}; - -/// Stores CustomVersions serialized by UE4 -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FCustomVersion { - /// Key - pub key: Guid, - /// Value - pub version: u32, -} - -impl FCustomVersion { - /// Creates a new instance of `FCustomVersion` - #[inline] - pub fn new(key: Guid, version: u32) -> Self { - FCustomVersion { key, version } - } - - /// Read FCustomVersion from a binary file - #[inline] - pub(crate) fn read(cursor: &mut R) -> Result { - let key = cursor.read_guid()?; - let version = cursor.read_u32::()?; - - Ok(FCustomVersion { key, version }) - } - - /// Write FCustomVersion to a binary file - #[inline] - pub(crate) fn write(&self, cursor: &mut W) -> Result { - cursor.write_guid(&self.key)?; - cursor.write_u32::(self.version)?; - Ok(20) - } -} - -/// Used for predefining custom versions for nicer checking when parsing -pub trait CustomVersionTrait { - /// Mappings from engine version to version number of this custom version - /// - /// # Example - /// UE4_27 -> 13 - /// UE4_23 -> 12 - const VERSION_MAPPINGS: &'static [(EngineVersion, i32)]; - /// Custom version friendly name - const FRIENDLY_NAME: &'static str; - /// Custom version guid - const GUID: Guid; -} - -macro_rules! impl_custom_version_trait { - ($enum_name:ident, $friendly_name:expr, $guid:expr, $($ver_name:ident : $ver:ident),*) => { - impl CustomVersionTrait for $enum_name { - const VERSION_MAPPINGS: &'static [(EngineVersion, i32)] = &[ - $( - (EngineVersion::$ver_name, Self::$ver as i32), - )* - ]; - const FRIENDLY_NAME: &'static str = $friendly_name; - const GUID: Guid = $guid; - } - } -} - -/// Custom serialization version for changes made in Dev-Editor stream. -#[derive(IntoPrimitive)] -#[repr(u32)] -pub enum FEditorObjectVersion { - /// Before any version changes were made - /// Introduced: ObjectVersion.VER_UE4_OLDEST_LOADABLE_PACKAGE - BeforeCustomVersionWasAdded = 0, - - /// Localizable text gathered and stored in packages is now flagged with a localizable text gathering process version - /// Introduced: ObjectVersion.VER_UE4_STREAMABLE_TEXTURE_AABB - GatheredTextProcessVersionFlagging, - - /// Fixed several issues with the gathered text cache stored in package headers - /// Introduced: ObjectVersion.VER_UE4_NAME_HASHES_SERIALIZED - GatheredTextPackageCacheFixesV1, - - /// Added support for "root" meta-data (meta-data not associated with a particular object in a package) - /// Introduced: ObjectVersion.VER_UE4_INSTANCED_STEREO_UNIFORM_REFACTOR - RootMetaDataSupport, - - /// Fixed issues with how Blueprint bytecode was cached - /// Introduced: ObjectVersion.VER_UE4_INSTANCED_STEREO_UNIFORM_REFACTOR - GatheredTextPackageCacheFixesV2, - - /// Updated FFormatArgumentData to allow variant data to be marshaled from a BP into C++ - /// Introduced: ObjectVersion.VER_UE4_INSTANCED_STEREO_UNIFORM_REFACTOR - TextFormatArgumentDataIsVariant, - - /// Changes to SplineComponent - /// Introduced: ObjectVersion.VER_UE4_INSTANCED_STEREO_UNIFORM_REFACTOR - SplineComponentCurvesInStruct, - - /// Updated ComboBox to support toggling the menu open, better controller support - /// Introduced: ObjectVersion.VER_UE4_COMPRESSED_SHADER_RESOURCES - ComboBoxControllerSupportUpdate, - - /// Refactor mesh editor materials - /// Introduced: ObjectVersion.VER_UE4_COMPRESSED_SHADER_RESOURCES - RefactorMeshEditorMaterials, - - /// Added UFontFace assets - /// Introduced: ObjectVersion.VER_UE4_TemplateIndex_IN_COOKED_EXPORTS - AddedFontFaceAssets, - - /// Add UPROPERTY for TMap of Mesh section, so the serialize will be done normally (and export to text will work correctly) - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - UPropertryForMeshSection, - - /// Update the schema of all widget blueprints to use the WidgetGraphSchema - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - WidgetGraphSchema, - - /// Added a specialized content slot to the background blur widget - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - AddedBackgroundBlurContentSlot, - - /// Updated UserDefinedEnums to have stable keyed display names - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - StableUserDefinedEnumDisplayNames, - - /// Added "Inline" option to UFontFace assets - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - AddedInlineFontFaceAssets, - - /// Fix a serialization issue with static mesh FMeshSectionInfoMap FProperty - /// Introduced: ObjectVersion.VER_UE4_ADDED_SEARCHABLE_NAMES - UPropertryForMeshSectionSerialize, - - /// Adding a version bump for the new fast widget construction in case of problems. - /// Introduced: ObjectVersion.VER_UE4_64BIT_EXPORTMAP_SERIALSIZES - FastWidgetTemplates, - - /// Update material thumbnails to be more intelligent on default primitive shape for certain material types - /// Introduced: ObjectVersion.VER_UE4_64BIT_EXPORTMAP_SERIALSIZES - MaterialThumbnailRenderingChanges, - - /// Introducing a new clipping system for Slate/UMG - /// Introduced: ObjectVersion.VER_UE4_ADDED_SWEEP_WHILE_WALKING_FLAG - NewSlateClippingSystem, - - /// MovieScene Meta Data added as native Serialization - /// Introduced: ObjectVersion.VER_UE4_ADDED_SWEEP_WHILE_WALKING_FLAG - MovieSceneMetaDataSerialization, - - /// Text gathered from properties now adds two variants: a version without the package localization ID (for use at runtime), and a version with it (which is editor-only) - /// Introduced: ObjectVersion.VER_UE4_ADDED_SWEEP_WHILE_WALKING_FLAG - GatheredTextEditorOnlyPackageLocId, - - /// Added AlwaysSign to FNumberFormattingOptions - /// Introduced: ObjectVersion.VER_UE4_ADDED_SOFT_OBJECT_PATH - AddedAlwaysSignNumberFormattingOption, - - /// Added additional objects that must be serialized as part of this new material feature - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID - AddedMaterialSharedInputs, - - /// Added morph target section indices - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID - AddedMorphTargetSectionIndices, - - /// Serialize the instanced static mesh render data, to avoid building it at runtime - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID - SerializeInstancedStaticMeshRenderData, - - /// Change to MeshDescription serialization (moved to release) - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID - MeshDescriptionNewSerializationMovedToRelease, - - /// New format for mesh description attributes - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_SUMMARY_LOCALIZATION_ID - MeshDescriptionNewAttributeFormat, - - /// Switch root component of SceneCapture actors from MeshComponent to SceneComponent - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - ChangeSceneCaptureRootComponent, - - /// StaticMesh serializes MeshDescription instead of RawMesh - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - StaticMeshDeprecatedRawMesh, - - /// MeshDescriptionBulkData contains a Guid used as a DDC key - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - MeshDescriptionBulkDataGuid, - - /// Change to MeshDescription serialization (removed FMeshPolygon::HoleContours) - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - MeshDescriptionRemovedHoles, - - /// Change to the WidgetCompoent WindowVisibilty default value - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - ChangedWidgetComponentWindowVisibilityDefault, - - /// Avoid keying culture invariant display strings during serialization to avoid non-deterministic cooking issues - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - CultureInvariantTextSerializationKeyStability, - - /// Change to UScrollBar and UScrollBox thickness property (removed implicit padding of 2, so thickness value must be incremented by 4). - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - ScrollBarThicknessChange, - - /// Deprecated LandscapeHoleMaterial - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - RemoveLandscapeHoleMaterial, - - /// MeshDescription defined by triangles instead of arbitrary polygons - /// Introduced: ObjectVersion.VER_UE4_FIX_WIDE_STRING_CRC - MeshDescriptionTriangles, - - /// Add weighted area and angle when computing the normals - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_OWNER - ComputeWeightedNormals, - - /// SkeletalMesh now can be rebuild in editor, no more need to re-import - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_OWNER - SkeletalMeshBuildRefactor, - - /// Move all SkeletalMesh source data into a private uasset in the same package has the skeletalmesh - /// Introduced: ObjectVersion.VER_UE4_ADDED_PACKAGE_OWNER - SkeletalMeshMoveEditorSourceDataToPrivateAsset, - - /// Parse text only if the number is inside the limits of its type - /// Introduced: ObjectVersion.VER_UE4_NON_OUTER_PACKAGE_IMPORT - NumberParsingOptionsNumberLimitsAndClamping, - - /// Make sure we can have more then 255 material in the skeletal mesh source data - /// Introduced: ObjectVersion.VER_UE4_NON_OUTER_PACKAGE_IMPORT - SkeletalMeshSourceDataSupport16bitOfMaterialNumber, - - /// Introduced: ObjectVersion.VER_UE4_AUTOMATIC_VERSION_PLUS_ONE - VersionPlusOne, - /// Introduced: ObjectVersion.VER_UE4_AUTOMATIC_VERSION - LatestVersion = (FEditorObjectVersion::VersionPlusOne as u32) + 1, -} - -impl_custom_version_trait!( - FEditorObjectVersion, - "FEditorObjectVersion", - Guid::from_u32([0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41]), - VER_UE4_AUTOMATIC_VERSION: LatestVersion, - VER_UE4_AUTOMATIC_VERSION_PLUS_ONE: VersionPlusOne, - VER_UE4_26: SkeletalMeshSourceDataSupport16bitOfMaterialNumber, - VER_UE4_25: SkeletalMeshMoveEditorSourceDataToPrivateAsset, - VER_UE4_24: SkeletalMeshBuildRefactor, - VER_UE4_23: RemoveLandscapeHoleMaterial, - VER_UE4_22: MeshDescriptionRemovedHoles, - VER_UE4_21: MeshDescriptionNewAttributeFormat, - VER_UE4_20: SerializeInstancedStaticMeshRenderData, - VER_UE4_19: AddedMorphTargetSectionIndices, - VER_UE4_17: GatheredTextEditorOnlyPackageLocId, - VER_UE4_16: MaterialThumbnailRenderingChanges, - VER_UE4_15: AddedInlineFontFaceAssets, - VER_UE4_14: AddedFontFaceAssets, - VER_UE4_13: SplineComponentCurvesInStruct, - VER_UE4_12: GatheredTextPackageCacheFixesV1, - VER_UE4_OLDEST_LOADABLE_PACKAGE: BeforeCustomVersionWasAdded -); - -/// Custom serialization version for changes made in //UE5/Release-* stream -#[derive(IntoPrimitive)] -#[repr(u32)] -pub enum FUE5ReleaseStreamObjectVersion { - /// Before any version changes were made - BeforeCustomVersionWasAdded = 0, - - /// Added Lumen reflections to new reflection enum, changed defaults - ReflectionMethodEnum, - - /// Serialize HLOD info in WorldPartitionActorDesc - WorldPartitionActorDescSerializeHLODInfo, - - /// Removing Tessellation from materials and meshes. - RemovingTessellation, - - /// LevelInstance serialize runtime behavior - LevelInstanceSerializeRuntimeBehavior, - - /// Refactoring Pose Asset runtime data structures - PoseAssetRuntimeRefactor, - - /// Serialize the folder path of actor descs - WorldPartitionActorDescSerializeActorFolderPath, - - /// Change hair strands vertex format - HairStrandsVertexFormatChange, - - /// Added max linear and angular speed to Chaos bodies - AddChaosMaxLinearAngularSpeed, - - /// PackedLevelInstance version - PackedLevelInstanceVersion, - - /// PackedLevelInstance bounds fix - PackedLevelInstanceBoundsFix, - - /// Custom property anim graph nodes (linked anim graphs, control rig etc.) now use optional pin manager - CustomPropertyAnimGraphNodesUseOptionalPinManager, - - /// Add native double and int64 support to FFormatArgumentData - TextFormatArgumentData64bitSupport, - - /// Material layer stacks are no longer considered 'static parameters' - MaterialLayerStacksAreNotParameters, - - /// CachedExpressionData is moved from UMaterial to UMaterialInterface - MaterialInterfaceSavedCachedData, - - /// Add support for multiple cloth deformer LODs to be able to raytrace cloth with a different LOD than the one it is rendered with - AddClothMappingLODBias, - - /// Add support for different external actor packaging schemes - AddLevelActorPackagingScheme, - - /// Add support for linking to the attached parent actor in WorldPartitionActorDesc - WorldPartitionActorDescSerializeAttachParent, - - /// Converted AActor GridPlacement to bIsSpatiallyLoaded flag - ConvertedActorGridPlacementToSpatiallyLoadedFlag, - - /// Fixup for bad default value for GridPlacement_DEPRECATED - ActorGridPlacementDeprecateDefaultValueFixup, - - /// PackedLevelActor started using FWorldPartitionActorDesc (not currently checked against but added as a security) - PackedLevelActorUseWorldPartitionActorDesc, - - /// Add support for actor folder objects - AddLevelActorFolders, - - /// Remove FSkeletalMeshLODModel bulk datas - RemoveSkeletalMeshLODModelBulkDatas, - - /// Exclude brightness from the EncodedHDRCubemap, - ExcludeBrightnessFromEncodedHDRCubemap, - - /// Unified volumetric cloud component quality sample count slider between main and reflection views for consistency - VolumetricCloudSampleCountUnification, - - /// Pose asset GUID generated from source AnimationSequence - PoseAssetRawDataGUID, - - /// Convolution bloom now take into account FPostProcessSettings::BloomIntensity for scatter dispersion. - ConvolutionBloomIntensity, - - /// Serialize FHLODSubActors instead of FGuids in WorldPartition HLODActorDesc - WorldPartitionHLODActorDescSerializeHLODSubActors, - - /// Large Worlds - serialize double types as doubles - LargeWorldCoordinates, - - /// Deserialize old BP float&double types as real numbers for pins - BlueprintPinsUseRealNumbers, - - /// Changed shadow defaults for directional light components, version needed to not affect old things - UpdatedDirectionalLightShadowDefaults, - - /// Refresh geometry collections that had not already generated convex bodies. - GeometryCollectionConvexDefaults, - - /// Add faster damping calculations to the cloth simulation and rename previous Damping parameter to LocalDamping. - ChaosClothFasterDamping, - - /// Serialize LandscapeActorGuid in FLandscapeActorDesc sub class. - WorldPartitionLandscapeActorDescSerializeLandscapeActorGuid, - - /// add inertia tensor and rotation of mass to convex - AddedInertiaTensorAndRotationOfMassAddedToConvex, - - /// Storing inertia tensor as vec3 instead of matrix. - ChaosInertiaConvertedToVec3, - - /// For Blueprint real numbers, ensure that legacy float data is serialized as single-precision - SerializeFloatPinDefaultValuesAsSinglePrecision, - - /// Upgrade the BlendMasks array in existing LayeredBoneBlend nodes - AnimLayeredBoneBlendMasks, - - /// Uses RG11B10 format to store the encoded reflection capture data on mobile - StoreReflectionCaptureEncodedHDRDataInRG11B10Format, - - /// Add WithSerializer type trait and implementation for FRawAnimSequenceTrack - RawAnimSequenceTrackSerializer, - - /// Removed font from FEditableTextBoxStyle, and added FTextBlockStyle instead. - RemoveDuplicatedStyleInfo, - - /// Added member reference to linked anim graphs - LinkedAnimGraphMemberReference, - - /// Changed default tangent behavior for new dynamic mesh components - DynamicMeshComponentsDefaultUseExternalTangents, - - /// Added resize methods to media capture - MediaCaptureNewResizeMethods, - - /// Function data stores a map from work to debug operands - RigVMSaveDebugMapInGraphFunctionData, - - /// Changed default Local Exposure Contrast Scale from 1.0 to 0.8 - LocalExposureDefaultChangeFrom1, - - /// Serialize bActorIsListedInSceneOutliner in WorldPartitionActorDesc - WorldPartitionActorDescSerializeActorIsListedInSceneOutliner, - - /// Disabled opencolorio display configuration by default - OpenColorIODisabledDisplayConfigurationDefault, - - /// Serialize ExternalDataLayerAsset in WorldPartitionActorDesc - WorldPartitionExternalDataLayers, - - /// Fix Chaos Cloth fictitious angular scale bug that requires existing parameter rescaling. - ChaosClothFictitiousAngularVelocitySubframeFix, - - /// Store physics thread particles data in single precision - SinglePrecisonParticleDataPT, - - /// Orthographic Near and Far Plane Auto-resolve enabled by default - OrthographicAutoNearFarPlane, -} - -impl_custom_version_trait!( - FUE5ReleaseStreamObjectVersion, - "FUE5ReleaseStreamObjectVersion", - Guid::from_u32([0xD89B5E42, 0x24BD4D46, 0x8412ACA8, 0xDF641779]), -); diff --git a/src/detect.rs b/src/detect.rs new file mode 100644 index 0000000..00cb3fa --- /dev/null +++ b/src/detect.rs @@ -0,0 +1,84 @@ +//! Automatically detect file contents. +//! +//! # Examples +//! +//! ``` +//! use binrw::BinRead; +//! use gvas::{detect::AutoDetectFile, error::{Error, Result}}; +//! use std::{assert_matches, fs::File, io::{Cursor, Read, Seek}}; +//! +//! // Open +//! let mut file = File::open("resources/test/regression_01.bin")?; +//! +//! // Read +//! let mut buf = Vec::new(); +//! let size = file.read_to_end(&mut buf)?; +//! +//! // Parse +//! let mut cursor = Cursor::new(buf); +//! let result = AutoDetectFile::read(&mut cursor)?; +//! +//! // Compare +//! assert_eq!(size, cursor.stream_position()? as usize); +//! assert_matches!(result, AutoDetectFile::GVAS(_)); +//! # Ok::<(), Error>(()) +//! ``` + +use binrw::binrw; + +#[cfg(feature = "palworld")] +use crate::palworld::PalworldSaveGame; +use crate::types::USaveGame; + +#[cfg(not(feature = "palworld"))] +#[binrw] +#[br(little)] +#[derive(Debug, PartialEq)] +pub enum AutoDetectFile { + GVAS(USaveGame), +} + +#[cfg(feature = "palworld")] +#[binrw] +#[br(little)] +#[derive(Debug, PartialEq)] +pub enum AutoDetectFile { + GVAS(USaveGame), + Palworld(PalworldSaveGame), +} + +#[cfg(test)] +mod test { + use std::{ + assert_matches, + fs::File, + io::{Cursor, Read, Seek}, + }; + + use binrw::BinRead; + + use crate::{ + detect::AutoDetectFile, error::Result, test::common::REGRESSION_01_PATH, types::USaveGame, + }; + + #[test] + fn gvas() -> Result<()> { + // Open + let mut file = File::open(REGRESSION_01_PATH)?; + + // Read + let mut buf = Vec::new(); + let size = file.read_to_end(&mut buf)?; + + // Parse + let mut cursor = Cursor::new(buf); + let result = AutoDetectFile::read(&mut cursor)?; + + // Compare + let pos = cursor.stream_position()?; + let pos = usize::try_from(pos)?; + assert_eq!(size, pos); + assert_matches!(result, AutoDetectFile::GVAS(USaveGame { .. })); + Ok(()) + } +} diff --git a/src/engine_version.rs b/src/engine_version.rs deleted file mode 100644 index 4927568..0000000 --- a/src/engine_version.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Engine version information - -use crate::cursor_ext::{ReadExt, WriteExt}; -use crate::error::Error; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use std::fmt::Display; -use std::io::{Read, Seek, Write}; - -/// Stores UE4 version in which the GVAS file was saved -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FEngineVersion { - /// Major version number. - pub major: u16, - /// Minor version number. - pub minor: u16, - /// Patch version number. - pub patch: u16, - /// Build id. - pub change_list: u32, - /// Build id string. - pub branch: String, -} - -impl Display for FEngineVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}.{}.{}-{}+++{}", - self.major, self.minor, self.patch, self.change_list, self.branch - ) - } -} - -impl FEngineVersion { - /// Creates a new instance of `FEngineVersion` - #[inline] - pub fn new(major: u16, minor: u16, patch: u16, change_list: u32, branch: String) -> Self { - FEngineVersion { - major, - minor, - patch, - change_list, - branch, - } - } - - /// Read FEngineVersion from a binary file - #[inline] - pub(crate) fn read(cursor: &mut R) -> Result { - let major = cursor.read_u16::()?; - let minor = cursor.read_u16::()?; - let patch = cursor.read_u16::()?; - let change_list = cursor.read_u32::()?; - let branch = cursor.read_string()?; - Ok(FEngineVersion { - major, - minor, - patch, - change_list, - branch, - }) - } - - /// Write FEngineVersion to a binary file - #[inline] - pub(crate) fn write(&self, cursor: &mut W) -> Result { - cursor.write_u16::(self.major)?; - cursor.write_u16::(self.minor)?; - cursor.write_u16::(self.patch)?; - cursor.write_u32::(self.change_list)?; - let mut len = 10; - len += cursor.write_string(&self.branch)?; - Ok(len) - } - - /// Get [`EngineVersion`] - pub fn get_version(&self) -> EngineVersion { - match (self.major, self.minor) { - (4, 0) => EngineVersion::VER_UE4_0, - (4, 1) => EngineVersion::VER_UE4_1, - (4, 2) => EngineVersion::VER_UE4_2, - (4, 3) => EngineVersion::VER_UE4_3, - (4, 4) => EngineVersion::VER_UE4_4, - (4, 5) => EngineVersion::VER_UE4_5, - (4, 6) => EngineVersion::VER_UE4_6, - (4, 7) => EngineVersion::VER_UE4_7, - (4, 8) => EngineVersion::VER_UE4_8, - (4, 9) => EngineVersion::VER_UE4_9, - (4, 10) => EngineVersion::VER_UE4_10, - (4, 11) => EngineVersion::VER_UE4_11, - (4, 12) => EngineVersion::VER_UE4_12, - (4, 13) => EngineVersion::VER_UE4_13, - (4, 14) => EngineVersion::VER_UE4_14, - (4, 15) => EngineVersion::VER_UE4_15, - (4, 16) => EngineVersion::VER_UE4_16, - (4, 17) => EngineVersion::VER_UE4_17, - (4, 18) => EngineVersion::VER_UE4_18, - (4, 19) => EngineVersion::VER_UE4_19, - (4, 20) => EngineVersion::VER_UE4_20, - (4, 21) => EngineVersion::VER_UE4_21, - (4, 22) => EngineVersion::VER_UE4_22, - (4, 23) => EngineVersion::VER_UE4_23, - (4, 24) => EngineVersion::VER_UE4_24, - (4, 25) => EngineVersion::VER_UE4_25, - (4, 26) => EngineVersion::VER_UE4_26, - (4, 27) => EngineVersion::VER_UE4_27, - (5, 0) => EngineVersion::VER_UE5_0, - (5, 1) => EngineVersion::VER_UE5_1, - (5, 2) => EngineVersion::VER_UE5_2, - _ => EngineVersion::UNKNOWN, - } - } -} - -/// UE4 Engine version enum -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[allow(non_camel_case_types)] -pub enum EngineVersion { - /// Unknown - UNKNOWN, - /// Oldest loadable package - VER_UE4_OLDEST_LOADABLE_PACKAGE, - - /// 4.0 - VER_UE4_0, - /// 4.1 - VER_UE4_1, - /// 4.2 - VER_UE4_2, - /// 4.3 - VER_UE4_3, - /// 4.4 - VER_UE4_4, - /// 4.5 - VER_UE4_5, - /// 4.6 - VER_UE4_6, - /// 4.7 - VER_UE4_7, - /// 4.8 - VER_UE4_8, - /// 4.9 - VER_UE4_9, - /// 4.10 - VER_UE4_10, - /// 4.11 - VER_UE4_11, - /// 4.12 - VER_UE4_12, - /// 4.13 - VER_UE4_13, - /// 4.14 - VER_UE4_14, - /// 4.15 - VER_UE4_15, - /// 4.16 - VER_UE4_16, - /// 4.17 - VER_UE4_17, - /// 4.18 - VER_UE4_18, - /// 4.19 - VER_UE4_19, - /// 4.20 - VER_UE4_20, - /// 4.21 - VER_UE4_21, - /// 4.22 - VER_UE4_22, - /// 4.23 - VER_UE4_23, - /// 4.24 - VER_UE4_24, - /// 4.25 - VER_UE4_25, - /// 4.26 - VER_UE4_26, - /// 4.27 - VER_UE4_27, - - /// 5.0 - VER_UE5_0, - /// 5.1 - VER_UE5_1, - /// 5.2 - VER_UE5_2, - - /// The newest specified version of the Unreal Engine. - VER_UE4_AUTOMATIC_VERSION, - /// Version plus one - VER_UE4_AUTOMATIC_VERSION_PLUS_ONE, -} diff --git a/src/error.rs b/src/error.rs index 2b349d5..449c433 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,129 +1,54 @@ +//! Error types. + use std::{ io, - string::{FromUtf8Error, FromUtf16Error}, + num::{ParseIntError, TryFromIntError}, }; use thiserror::Error; -/// Gets thrown when there is a deserialization error -#[derive(Error, Debug)] -pub enum DeserializeError { - /// If the GVAS header is not valid - #[error("Invalid header: {0}")] - InvalidHeader(Box), - /// If a value has a size that was unexpected, e.g. UInt32Property has 8 bytes size - #[error("Invalid value size, expected {0} got {1} at position {2:#x}")] - InvalidValueSize(u64, u64, u64), - /// If a string has invalid size - #[error("Invalid string size {0} at position {1:#x}")] - InvalidString(i32, u64), - /// Invalid string terminator - #[error("Invalid string terminator {0} at position {1:#x}")] - InvalidStringTerminator(u16, u64), - /// If a boolean has invalid value - #[error("Invalid boolean value {0} at position {1:#x}")] - InvalidBoolean(u32, u64), - /// If a hint is missing. - #[error("Missing hint for struct {0} at path {1} at position {2:#x}")] - MissingHint(Box, Box, u64), - /// If an argument is missing - #[error("Missing argument: {0} at position {1:#x}")] - MissingArgument(Box, u64), - /// If a Property creation fails - #[error("Invalid property {0} at position {1:#x}")] - InvalidProperty(Box, u64), - /// Invalid enum value - #[error("No discriminant in enum `{0}` matches the value `{1}` at position {2:#x}")] - InvalidEnumValue(Box, i8, u64), - /// Invalid array index header - #[error("Unexpected array_index value {0} at position {1:#x}")] - InvalidArrayIndex(u32, u64), - /// Invalid terminator - #[error("Unexpected terminator value {0} at position {1:#x}")] - InvalidTerminator(u8, u64), - /// If a string has invalid UTF-16 formatting - #[error("Invalid UTF-16 string at position {1:#x}")] - FromUtf16Error(#[source] FromUtf16Error, u64), - /// If a string has invalid UTF-8 formatting - #[error("Invalid UTF-8 string at position {1:#x}")] - FromUtf8Error(#[source] FromUtf8Error, u64), -} - -impl DeserializeError { - /// A helper for creating `MissingArgument` errors - #[inline] - pub fn missing_argument(argument_name: A, stream: &mut S) -> Self - where - A: Into>, - S: io::Seek, - { - let position = stream.stream_position().unwrap_or_default(); - Self::MissingArgument(argument_name.into(), position) - } +/// A type alias for [`core::result::Result`]. +pub type Result = core::result::Result; - /// A helper for creating `InvalidProperty` errors - #[inline] - pub fn invalid_property(reason: R, stream: &mut S) -> Self - where - R: Into>, - S: io::Seek, - { - let position = stream.stream_position().unwrap_or_default(); - Self::InvalidProperty(reason.into(), position) - } - - /// A helper for creating `InvalidEnumValue` errors - #[inline] - pub fn invalid_enum_value(name: N, value: i8, stream: &mut S) -> Self - where - N: Into>, - S: io::Seek, - { - let position = stream.stream_position().unwrap_or_default(); - Self::InvalidEnumValue(name.into(), value, position) - } +/// A wrapper for the various error types this crate can emit +#[derive(Debug, Error)] +pub enum Error { + /// A [`ParseGuidError`] occureed. + #[error(transparent)] + ParseGuidError(#[from] ParseGuidError), + /// An [`std::io::Error`] occured + #[error(transparent)] + Io(#[from] io::Error), + /// A [`binrw::Error`] occured + #[error(transparent)] + Binrw(#[from] binrw::Error), + /// A [`TryFromIntError`] occureed. + #[error(transparent)] + TryFromIntError(#[from] TryFromIntError), } -/// Gets thrown when there is a serialization error -#[derive(Error, Debug)] -pub enum SerializeError { - /// A value was invalid - #[error("Invalid value {0}")] - InvalidValue(Box), - /// Struct is missing a field, e.g. struct with type_name `Vector` doesn't have an `X` property - #[error("Struct {0} missing field {1}")] - StructMissingField(Box, Box), +/// An error ocurred while parsing a Guid +#[derive(Debug, Error)] +pub enum ParseGuidError { + #[error("invalid guid length {0}")] + InvalidLength(usize), + #[error(transparent)] + ParseIntError(#[from] ParseIntError), } -impl SerializeError { - /// A helper for creating `InvalidValue` errors - pub fn invalid_value(msg: M) -> Self - where - M: Into>, - { - Self::InvalidValue(msg.into()) - } - - /// A helper for creating `StructMissingField` errors - pub fn struct_missing_field(type_name: T, missing_field: M) -> Self - where - T: Into>, - M: Into>, - { - Self::StructMissingField(type_name.into(), missing_field.into()) - } +/// An error ocurred while decribing a PropertyTag. +#[derive(Debug, Error)] +pub enum PropertyTagError { + #[error("Unsupported method PropertyTag::{0} for {1}")] + Unsupported(Box, String), } -/// A wrapper for the various error types this crate can emit -#[derive(Error, Debug)] -pub enum Error { - /// A `DeserializeError` occurred - #[error(transparent)] - Deserialize(#[from] DeserializeError), - /// A `SerializeError` occurred - #[error(transparent)] - Serialize(#[from] SerializeError), - /// An `std::io::Error` occured - #[error(transparent)] - Io(#[from] io::Error), +pub(crate) fn binrw_custom(pos: u64) -> impl Fn(T) -> binrw::Error +where + T: binrw::error::CustomError + 'static, +{ + move |e| binrw::Error::Custom { + pos, + err: Box::new(e), + } } diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..d474796 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,133 @@ +//! Describes the serialization format used by a save file. +//! +//! Although the GVAS file format is broadly stable across Unreal Engine +//! releases, many individual types have version-dependent binary layouts. +//! Whether a particular field is present, omitted, widened, or serialized +//! differently depends on the package version and custom version GUIDs stored +//! in the [`FSaveGameHeader`]. +//! +//! This module derives a [`SerializationFormat`] from an +//! [`FSaveGameHeader`]. The resulting flags describe the serialization +//! behavior expected throughout the rest of the crate, allowing individual +//! types to select the correct binary representation without repeatedly +//! performing version checks. + +use crate::types::{ + CustomVersion, EEditorObjectVersion, EUE5ReleaseStreamObjectVersion, + EUnrealEngineObjectUE4Version, EUnrealEngineObjectUE5Version, FSaveGameHeader, +}; + +#[derive(Clone, Debug)] +pub struct SerializationFormat { + package_file_version: u32, // EUnrealEngineObjectUE4Version, + package_file_version_ue5: u32, // Option, + release_version: u32, // EUE5ReleaseStreamObjectVersion, + editor_version: u32, // EEditorObjectVersion, +} + +impl SerializationFormat { + #[cfg_attr(not(test), allow(unused))] + #[inline] + pub(crate) const fn from_enums( + package_file_version: EUnrealEngineObjectUE4Version, + package_file_version_ue5: Option, + release_version: EUE5ReleaseStreamObjectVersion, + editor_version: EEditorObjectVersion, + ) -> Self { + Self::from_versions( + package_file_version as u32, + match package_file_version_ue5 { + Some(e) => e as u32, + None => 0, + }, + release_version as u32, + editor_version as u32, + ) + } + + #[inline] + const fn from_versions( + package_file_version: u32, + package_file_version_ue5: u32, + release_version: u32, + editor_version: u32, + ) -> Self { + Self { + package_file_version, + package_file_version_ue5, + release_version, + editor_version, + } + } + + #[inline] + pub fn ftext_history_date_timezone(&self) -> bool { + self.package_file_version >= EUnrealEngineObjectUE4Version::FtextHistoryDateTimezone as u32 + } + + #[inline] + pub fn property_tag_set_map_support(&self) -> bool { + self.package_file_version >= EUnrealEngineObjectUE4Version::PropertyTagSetMapSupport as u32 + } + + #[inline] + pub fn property_guid_in_property_tag(&self) -> bool { + self.package_file_version >= EUnrealEngineObjectUE4Version::PropertyGuidInPropertyTag as u32 + } + + #[inline] + pub fn property_tag_complete_type_name(&self) -> bool { + self.package_file_version_ue5 + >= EUnrealEngineObjectUE5Version::PropertyTagCompleteTypeName as u32 + } + + #[inline] + pub fn fsoftobjectpath_remove_asset_path_fnames(&self) -> bool { + self.package_file_version_ue5 + >= EUnrealEngineObjectUE5Version::FsoftobjectpathRemoveAssetPathFnames as u32 + } + + #[inline] + pub fn text_64bit_support(&self) -> bool { + self.release_version + >= EUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport as u32 + } + + #[inline] + pub fn large_world_coordinates(&self) -> bool { + self.release_version >= EUE5ReleaseStreamObjectVersion::LargeWorldCoordinates as u32 + } + + #[inline] + pub fn include_always_sign(&self) -> bool { + self.editor_version >= EEditorObjectVersion::AddedAlwaysSignNumberFormattingOption as u32 + } + + #[inline] + pub fn culture_invariant_stability(&self) -> bool { + self.editor_version + >= EEditorObjectVersion::CultureInvariantTextSerializationKeyStability as u32 + } +} + +impl FSaveGameHeader { + #[inline] + pub fn serialization_format(&self) -> SerializationFormat { + fn get_custom(header: &FSaveGameHeader) -> u32 { + match &header.custom_versions { + Some(container) => container.get_custom::(), + None => 0, + } + } + let package_file_version = self.package_file_version.version_ue4(); + let package_file_version_ue5 = self.package_file_version.version_ue5(); + let release_version = get_custom::(self); + let editor_version = get_custom::(self); + SerializationFormat::from_versions( + package_file_version, + package_file_version_ue5, + release_version, + editor_version, + ) + } +} diff --git a/src/game_version.rs b/src/game_version.rs deleted file mode 100644 index 3c185c6..0000000 --- a/src/game_version.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Game version enumeration - -use num_enum::{IntoPrimitive, TryFromPrimitive}; - -/// Game version enumeration -/// -/// Used for specifying game versions if a game has custom serialization -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum GameVersion { - /// Default GVAS serialization - Default, - /// Palworld serialization - Palworld, -} - -/// Palworld compression type -#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[repr(i8)] -pub enum PalworldCompressionType { - /// None - None = 0x30, - /// Zlib - Zlib = 0x31, - /// Zlib twice - ZlibTwice = 0x32, -} - -/// Deserialized game version -/// -/// Used for storing additional deserialized information about custom serialization -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum DeserializedGameVersion { - /// Default GVAS serialization - Default, - /// Palworld serialization - Palworld(PalworldCompressionType), -} - -impl Default for DeserializedGameVersion { - #[inline] - fn default() -> Self { - DeserializedGameVersion::Default - } -} - -impl DeserializedGameVersion { - #[cfg(feature = "serde")] - #[inline] - pub(crate) fn is_default(&self) -> bool { - matches!(self, DeserializedGameVersion::Default) - } -} - -/// Palworld save magic -pub(crate) const PLZ_MAGIC: &[u8; 3] = b"PlZ"; diff --git a/src/lib.rs b/src/lib.rs index c72922a..9e43a6f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,571 +1,35 @@ #![warn(clippy::expect_used, clippy::panic, clippy::unwrap_used)] -#![warn(missing_docs)] +#![warn(clippy::use_self)] +// #![warn(missing_docs)] //! Gvas //! -//! UE4 Save File parsing library +//! UE4+ Save File parsing library //! //! # Examples //! -//! ```no_run -//! use gvas::{error::Error, GvasFile}; -//! use std::{ -//! fs::File, -//! }; -//! use gvas::game_version::GameVersion; -//! -//! let mut file = File::open("save.sav")?; -//! let gvas_file = GvasFile::read(&mut file, GameVersion::Default); -//! -//! println!("{:#?}", gvas_file); -//! # Ok::<(), Error>(()) -//! ``` -//! -//! ## Hints -//! -//! If your file fails while parsing with a [`DeserializeError::MissingHint`] error you need hints. -//! When a struct is stored inside ArrayProperty/SetProperty/MapProperty in GvasFile it does not contain type annotations. -//! This means that a library parsing the file must know the type beforehand. That's why you need hints. -//! -//! The error usually looks like this: -//! ```no_run,ignore -//! MissingHint( -//! "StructProperty" /* property type */, -//! "UnLockedMissionParameters.MapProperty.Key.StructProperty" /* property path */, -//! 120550 /* position */) //! ``` -//! To get a hint type you need to look at the position of [`DeserializeError::MissingHint`] error. -//! Then you go to that position in the file and try to determine which type the struct has. -//! Afterwards you parse the file like this: +//! use std::{fs::File, io::{Cursor, Read}, path::Path}; //! +//! use binrw::BinRead; +//! use gvas::types::USaveGame; +//! use gvas::error::{Error, Result}; //! -//! [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint +//! let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/Slot1.sav"); +//! let mut file = File::open(path)?; +//! let mut buf = Vec::new(); +//! let len = file.read_to_end(&mut buf)?; +//! let mut cursor = Cursor::new(buf); +//! let result = USaveGame::read(&mut cursor)?; //! -//! ```no_run -//! use gvas::{error::Error, GvasFile}; -//! use std::{ -//! collections::HashMap, -//! fs::File, -//! }; -//! use gvas::game_version::GameVersion; -//! -//! let mut file = File::open("save.sav")?; -//! -//! let mut hints = HashMap::new(); -//! hints.insert("UnLockedMissionParameters.MapProperty.Key.StructProperty".to_string(), "Guid".to_string()); -//! -//! let gvas_file = GvasFile::read_with_hints(&mut file, GameVersion::Default, &hints); -//! -//! println!("{:#?}", gvas_file); +//! println!("{:#?}", result); //! # Ok::<(), Error>(()) //! ``` -/// Extensions for `Cursor`. -pub mod cursor_ext; -/// Custom version information. -pub mod custom_version; -/// Engine version information. -pub mod engine_version; -/// Error types. +pub mod detect; pub mod error; -/// Game version enumeration. -pub mod game_version; -/// Object version information. -pub mod object_version; -/// Extensions for `Ord`. -mod ord_ext; -/// Property types. -pub mod properties; -/// Savegame version information. -pub mod savegame_version; -pub(crate) mod scoped_stack_entry; -/// Various types. +pub mod format; +pub mod palworld; 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::Compression; -use flate2::read::ZlibDecoder; -use flate2::write::ZlibEncoder; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - custom_version::FCustomVersion, - engine_version::FEngineVersion, - error::{DeserializeError, Error}, - game_version::{DeserializedGameVersion, GameVersion, PLZ_MAGIC, PalworldCompressionType}, - object_version::EUnrealEngineObjectUE5Version, - ord_ext::OrdExt, - properties::{Property, PropertyOptions, PropertyTrait}, - savegame_version::SaveGameVersion, - types::{Guid, map::HashableIndexMap}, -}; - -/// The four bytes 'GVAS' appear at the beginning of every GVAS file. -pub const FILE_TYPE_GVAS: u32 = u32::from_le_bytes(*b"GVAS"); - -/// Stores information about GVAS file, engine version, etc. -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type"))] -pub enum GvasHeader { - /// Version 2 - Version2 { - /// File format version. - package_file_version: u32, - /// Unreal Engine version. - engine_version: FEngineVersion, - /// Custom version format. - custom_version_format: u32, - /// Custom versions. - custom_versions: HashableIndexMap, - /// Save game class name. - save_game_class_name: String, - }, - /// Version 3 - Version3 { - /// File format version (UE4). - package_file_version: u32, - /// File format version (UE5). - package_file_version_ue5: u32, - /// Unreal Engine version. - engine_version: FEngineVersion, - /// Custom version format. - custom_version_format: u32, - /// Custom versions. - custom_versions: HashableIndexMap, - /// Save game class name. - save_game_class_name: String, - }, -} - -impl GvasHeader { - /// Read GvasHeader from a binary file - /// - /// # Errors - /// - /// If this function reads an invalid header it returns [`Error`] - /// - /// # Examples - /// - /// ```no_run - /// use gvas::{error::Error, GvasHeader}; - /// use std::{ - /// fs::File, - /// }; - /// - /// let mut file = File::open("save.sav")?; - /// - /// let gvas_header = GvasHeader::read(&mut file)?; - /// - /// println!("{:#?}", gvas_header); - /// # Ok::<(), Error>(()) - /// ``` - pub fn read(cursor: &mut R) -> Result { - let file_type_tag = cursor.read_u32::()?; - 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::()?; - 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::()?; - if !package_file_version.between(0x205, 0x20D) { - Err(DeserializeError::InvalidHeader( - format!("Package file version {package_file_version} not supported") - .into_boxed_str(), - ))? - } - - // This field is only present in the v3 header - let package_file_version_ue5 = if save_game_file_version - >= SaveGameVersion::PackageFileSummaryVersionChange as u32 - { - let version = cursor.read_u32::()?; - 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::()?; - 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::()?; - 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, - }, - }) - } - - /// Write GvasHeader to a binary file - /// - /// # Examples - /// ```no_run - /// use gvas::{error::Error, GvasHeader}; - /// use std::{ - /// fs::File, - /// io::Cursor, - /// }; - /// - /// let mut file = File::open("save.sav")?; - /// let gvas_header = GvasHeader::read(&mut file)?; - /// - /// let mut writer = Cursor::new(Vec::new()); - /// gvas_header.write(&mut writer)?; - /// println!("{:#?}", writer.get_ref()); - /// # Ok::<(), Error>(()) - /// ``` - pub fn write(&self, cursor: &mut W) -> Result { - cursor.write_u32::(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::(2)?; - cursor.write_u32::(*package_file_version)?; - len += engine_version.write(cursor)?; - cursor.write_u32::(*custom_version_format)?; - cursor.write_u32::(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::(3)?; - cursor.write_u32::(*package_file_version)?; - cursor.write_u32::(*package_file_version_ue5)?; - len += engine_version.write(cursor)?; - cursor.write_u32::(*custom_version_format)?; - cursor.write_u32::(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) - } - } - } - - /// Get custom versions from this header - pub fn get_custom_versions(&self) -> &HashableIndexMap { - match self { - GvasHeader::Version2 { - custom_versions, .. - } => custom_versions, - GvasHeader::Version3 { - custom_versions, .. - } => custom_versions, - } - } -} - -/// Main UE4 save file struct -#[derive(Debug, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct GvasFile { - /// Game version - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "DeserializedGameVersion::is_default") - )] - pub deserialized_game_version: DeserializedGameVersion, - /// GVAS file header. - pub header: GvasHeader, - /// GVAS properties. - pub properties: HashableIndexMap, -} - -impl GvasFile { - /// Read GvasFile from a binary file - /// - /// # Errors - /// - /// If this function reads an invalid file it returns [`Error`] - /// - /// If this function reads a file which needs hints it returns [`DeserializeError::MissingHint`] - /// - /// [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint - /// - /// # Examples - /// - /// ```no_run - /// use gvas::{error::Error, GvasFile}; - /// use std::fs::File; - /// use gvas::game_version::GameVersion; - /// - /// let mut file = File::open("save.sav")?; - /// let gvas_file = GvasFile::read(&mut file, GameVersion::Default); - /// - /// println!("{:#?}", gvas_file); - /// # Ok::<(), Error>(()) - /// ``` - pub fn read(cursor: &mut R, game_version: GameVersion) -> Result { - let hints = HashMap::new(); - Self::read_with_hints(cursor, game_version, &hints) - } - - /// Read GvasFile from a binary file - /// - /// # Errors - /// - /// If this function reads an invalid file it returns [`Error`] - /// - /// If this function reads a file which needs a hint that is missing it returns [`DeserializeError::MissingHint`] - /// - /// [`DeserializeError::MissingHint`]: error/enum.DeserializeError.html#variant.MissingHint - /// - /// # Examples - /// - /// ```no_run - /// use gvas::{error::Error, GvasFile}; - /// use std::{collections::HashMap, fs::File}; - /// use gvas::game_version::GameVersion; - /// - /// let mut file = File::open("save.sav")?; - /// - /// let mut hints = HashMap::new(); - /// hints.insert( - /// "SeasonSave.StructProperty.Seasons.MapProperty.Key.StructProperty".to_string(), - /// "Guid".to_string(), - /// ); - /// - /// let gvas_file = GvasFile::read_with_hints(&mut file, GameVersion::Default, &hints); - /// - /// println!("{:#?}", gvas_file); - /// # Ok::<(), Error>(()) - /// ``` - pub fn read_with_hints( - cursor: &mut R, - game_version: GameVersion, - hints: &HashMap, - ) -> Result { - 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::()?; - let _compressed_length = cursor.read_u32::()?; - - 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, - }) - } - - /// Write GvasFile to a binary file - /// - /// # Errors - /// - /// If the file was modified in a way that makes it invalid this function returns [`Error`] - /// - /// # Examples - /// - /// ```no_run - /// use gvas::{error::Error, GvasFile}; - /// use std::{ - /// fs::File, - /// io::Cursor, - /// }; - /// use gvas::game_version::GameVersion; - /// - /// let mut file = File::open("save.sav")?; - /// let gvas_file = GvasFile::read(&mut file, GameVersion::Default)?; - /// - /// let mut writer = Cursor::new(Vec::new()); - /// gvas_file.write(&mut writer)?; - /// println!("{:#?}", writer.get_ref()); - /// # Ok::<(), Error>(()) - /// ``` - pub fn write(&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::(0)?; // padding - - 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::(decompressed.len() as u32)?; - let compressed_length_pos = cursor.stream_position()?; - cursor.write_u32::(0)?; // Compressed length placeholder, will be updated later - cursor.write_all(PLZ_MAGIC)?; - cursor.write_enum(compression_type)?; - - // Compress and write data directly to the output cursor - 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()?; - } - } - - // Update compressed length - let end_pos = cursor.stream_position()?; - cursor.seek(SeekFrom::Start(compressed_length_pos))?; - cursor.write_u32::((end_pos - (compressed_length_pos + 4)) as u32)?; - cursor.seek(SeekFrom::Start(end_pos))?; - } - } - Ok(()) - } -} +mod test; diff --git a/src/ord_ext.rs b/src/ord_ext.rs deleted file mode 100644 index 1783c57..0000000 --- a/src/ord_ext.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub trait OrdExt { - fn between(&self, start: I, end: I) -> bool; -} - -impl OrdExt for I { - fn between(&self, start: I, end: I) -> bool { - *self >= start && *self <= end - } -} diff --git a/src/palworld.rs b/src/palworld.rs new file mode 100644 index 0000000..17efd77 --- /dev/null +++ b/src/palworld.rs @@ -0,0 +1,245 @@ +#![cfg(feature = "palworld")] + +//! Palworld save wrapper support. +//! +//! Palworld save files contain a 12-byte header followed by a [`USaveGame`] +//! which may be Zlib-deflated. + +use std::io::{Cursor, Read, Seek, Write}; + +use binrw::{BinRead, BinResult, BinWrite, Endian, binrw}; +use flate2::{Compression, read::ZlibDecoder, write::ZlibEncoder}; + +use crate::{error::binrw_custom, types::USaveGame}; + +#[binrw] +#[derive(Debug, PartialEq)] +struct PlZHeader { + uncompressed_size: u32, + compressed_size: u32, + compression: PalworldCompression, +} + +/// A Palworld save file containing a compressed [`USaveGame`]. +/// +/// Palworld wraps the Unreal save payload in a small `PlZ#` header that +/// records the compression method and payload sizes. The header sizes are +/// recalculated when writing. +#[derive(Debug, PartialEq)] +pub struct PalworldSaveGame { + pub compression: PalworldCompression, + pub content: USaveGame, +} + +/// Compression methods used by [`PalworldSaveGame`]. +#[binrw] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PalworldCompression { + /// Uncompressed payload, identified by the `PlZ0` magic. + #[brw(magic = b"PlZ0")] + None, + + /// Payload compressed once with zlib, identified by the `PlZ1` magic. + #[brw(magic = b"PlZ1")] + Zlib, + + /// Payload compressed twice with zlib, identified by the `PlZ2` magic. + #[brw(magic = b"PlZ2")] + ZlibTwice, +} + +impl BinRead for PalworldSaveGame { + type Args<'a> = (); + + fn read_options( + reader: &mut R, + endian: Endian, + (): Self::Args<'_>, + ) -> BinResult { + let pos = reader.stream_position()?; + let header = PlZHeader::read_options(reader, endian, ())?; + let uncompressed_size = header + .uncompressed_size + .try_into() + .map_err(binrw_custom(pos))?; + + let compression = header.compression; + let uncompressed = match compression { + PalworldCompression::None => { + let mut data = vec![0u8; uncompressed_size]; + reader.read_exact(&mut data)?; + data + } + + PalworldCompression::Zlib => { + let mut reader = ZlibDecoder::new(reader); + let mut data = vec![0u8; uncompressed_size]; + reader.read_exact(&mut data)?; + data + } + + PalworldCompression::ZlibTwice => { + let mut reader = ZlibDecoder::new(ZlibDecoder::new(reader)); + let mut data = Vec::with_capacity(uncompressed_size); + let _len = reader.read_to_end(&mut data)?; + data + } + }; + + let reader = &mut Cursor::new(uncompressed); + let content = USaveGame::read_options(reader, endian, ())?; + + let result = Self { + compression, + content, + }; + Ok(result) + } +} + +impl BinWrite for PalworldSaveGame { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: Endian, + _args: Self::Args<'_>, + ) -> BinResult<()> { + // Buffer uncompressed data + let mut uncompressed = Cursor::new(Vec::new()); + self.content.write_options(&mut uncompressed, endian, ())?; + let pos = writer.stream_position()?; + let uncompressed = uncompressed.into_inner(); + let uncompressed_size = uncompressed.len().try_into().map_err(binrw_custom(pos))?; + + // Buffer compressed data + let level = Compression::default(); + let compression = self.compression; + let compressed = match compression { + PalworldCompression::None => uncompressed, + PalworldCompression::Zlib => { + let tmp = Vec::new(); + let mut tmp = ZlibEncoder::new(tmp, level); + tmp.write_all(uncompressed.as_slice())?; + tmp.finish()? + } + PalworldCompression::ZlibTwice => { + let tmp = Vec::new(); + let tmp = ZlibEncoder::new(tmp, level); + let mut tmp = ZlibEncoder::new(tmp, level); + tmp.write_all(uncompressed.as_slice())?; + tmp.finish()?.finish()? + } + }; + let compressed_size = u32::try_from(compressed.len()).map_err(binrw_custom(pos))?; + + // Create a new header + let header = PlZHeader { + uncompressed_size, + compressed_size, + compression, + }; + + // Write + header.write_options(writer, endian, ())?; + writer.write_all(compressed.as_slice())?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use std::{ + assert_matches, + fs::File, + io::{Cursor, Read}, + }; + + use binrw::{BinRead, BinWrite}; + + use crate::{ + error::Result, + palworld::{PalworldCompression, PalworldSaveGame}, + test::common::{PALWORLD_ZLIB_PATH, PALWORLD_ZLIB_TWICE_PATH}, + types::USaveGame, + }; + + #[test] + fn zlib() -> Result<()> { + // Open + let mut file = File::open(PALWORLD_ZLIB_PATH)?; + + // Read + let mut buf = Vec::new(); + let len = file.read_to_end(&mut buf)?; + + // Parse + let mut cursor = Cursor::new(buf); + let result = PalworldSaveGame::read_le(&mut cursor)?; + + // Compare + assert_matches!( + result, + PalworldSaveGame { + compression: PalworldCompression::Zlib, + content: USaveGame { .. } + } + ); + + // Write + let buf2 = vec![0u8; len]; + let mut cursor2 = Cursor::new(buf2); + PalworldSaveGame::write_le(&result, &mut cursor2)?; + + // Read again + let buf2 = cursor2.into_inner(); + let mut cursor2 = Cursor::new(buf2); + let result2 = PalworldSaveGame::read_le(&mut cursor2)?; + + // Compare + assert!(result == result2); + + // Success + Ok(()) + } + + #[test] + fn zlib_twice() -> Result<()> { + // Open + let mut file = File::open(PALWORLD_ZLIB_TWICE_PATH)?; + + // Read + let mut buf = Vec::new(); + let len = file.read_to_end(&mut buf)?; + + // Parse + let mut cursor = Cursor::new(buf); + let result = PalworldSaveGame::read_le(&mut cursor)?; + + // Compare + assert_matches!( + result, + PalworldSaveGame { + compression: PalworldCompression::ZlibTwice, + content: USaveGame { .. }, + } + ); + + // Write + let buf2 = vec![0u8; len]; + let mut cursor2 = Cursor::new(buf2); + PalworldSaveGame::write_le(&result, &mut cursor2)?; + + // Read again + let buf2 = cursor2.into_inner(); + let mut cursor2 = Cursor::new(buf2); + let result2 = PalworldSaveGame::read_le(&mut cursor2)?; + + // Compare + assert!(result == result2); + + // Success + Ok(()) + } +} diff --git a/src/properties/array_property.rs b/src/properties/array_property.rs deleted file mode 100644 index 9977e3e..0000000 --- a/src/properties/array_property.rs +++ /dev/null @@ -1,480 +0,0 @@ -use std::{ - fmt::Debug, - io::{Cursor, Read, Seek, Write}, -}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use ordered_float::OrderedFloat; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::{DeserializeError, Error, SerializeError}, - types::Guid, -}; - -use super::{ - Property, PropertyOptions, PropertyTrait, - enum_property::EnumProperty, - impl_read_header, impl_write, impl_write_header_part, - int_property::{BoolProperty, ByteProperty, BytePropertyValue, FloatProperty, IntProperty}, - name_property::NameProperty, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, -}; - -#[cfg(feature = "serde")] -use serde_with::{hex::Hex, serde_as}; - -/// A property that holds an array of values. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", cfg_eval::cfg_eval, serde_as)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum ArrayProperty { - /// An array of BoolProperty values. - Bools { - /// An array of values. - bools: Vec, - }, - /// An array of ByteProperty values. - Bytes { - /// An array of values. - #[cfg_attr(feature = "serde", serde_as(as = "Hex"))] - bytes: Vec, - }, - /// An array of EnumProperty values. - Enums { - /// An array of values. - enums: Vec, - }, - /// An array of FloatProperty values. - Floats { - /// An array of values. - floats: Vec>, - }, - /// An array of IntProperty values. - Ints { - /// An array of values. - ints: Vec, - }, - /// An array of NameProperty values. - Names { - /// An array of values. - names: Vec>, - }, - /// An array of StrProperty values. - Strings { - /// An array of values. - strings: Vec>, - }, - /// An array of StructProperty values. - Structs { - /// Field name. - field_name: String, - /// Type name. - type_name: String, - /// The unique identifier of the property. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Guid::is_zero"))] - #[cfg_attr(feature = "serde", serde(default))] - guid: Guid, - /// An array of values. - structs: Vec, - }, - /// Any other Property value - Properties { - /// The type of Property in `properties`. - property_type: String, - /// An array of values. - properties: Vec, - }, -} - -macro_rules! validate { - ($cursor:expr, $cond:expr, $($arg:tt)+) => {{ - if !$cond { - Err(DeserializeError::InvalidProperty( - format!($($arg)+).into_boxed_str(), - $cursor.stream_position()?, - ))? - } - }}; -} - -impl ArrayProperty { - /// Creates a new `ArrayProperty` instance. - #[inline] - pub fn new( - property_type: String, - struct_info: Option<(String, String, Guid)>, - properties: Vec, - ) -> Result { - match (property_type.as_str(), struct_info) { - ("BoolProperty", None) => match properties - .iter() - .map(|p| match p { - Property::BoolProperty(BoolProperty { value }) => Ok(*value), - _ => Err(()), - }) - .collect::>() - { - Ok(bools) => Ok(ArrayProperty::Bools { bools }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("ByteProperty", None) => match properties - .iter() - .map(|p| match p { - Property::ByteProperty(ByteProperty { - name: None, - value: BytePropertyValue::Byte(value), - }) => Ok(*value), - _ => Err(()), - }) - .collect::>() - { - Ok(bytes) => Ok(ArrayProperty::Bytes { bytes }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("EnumProperty", None) => match properties - .iter() - .map(|p| match p { - Property::EnumProperty(EnumProperty { - enum_type: None, - value, - }) => Ok(value.to_owned()), - _ => Err(()), - }) - .collect::>() - { - Ok(enums) => Ok(ArrayProperty::Enums { enums }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("IntProperty", None) => match properties - .iter() - .map(|p| match p { - Property::IntProperty(IntProperty { value }) => Ok(*value), - _ => Err(()), - }) - .collect::>() - { - Ok(ints) => Ok(ArrayProperty::Ints { ints }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("FloatProperty", None) => match properties - .iter() - .map(|p| match p { - Property::FloatProperty(FloatProperty { value }) => Ok(value.to_owned()), - _ => Err(()), - }) - .collect::>() - { - Ok(floats) => Ok(ArrayProperty::Floats { floats }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("NameProperty", None) => match properties - .iter() - .map(|p| match p { - Property::NameProperty(NameProperty { - array_index: 0, - value, - }) => Ok(value.to_owned()), - _ => Err(()), - }) - .collect::>() - { - Ok(names) => Ok(ArrayProperty::Names { names }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("StrProperty", None) => match properties - .iter() - .map(|p| match p { - Property::StrProperty(StrProperty { value }) => Ok(value.to_owned()), - _ => Err(()), - }) - .collect::>() - { - Ok(strings) => Ok(ArrayProperty::Strings { strings }), - Err(()) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - }, - - ("StructProperty", Some((field_name, type_name, guid))) => match properties - .iter() - .map(|p| match p { - Property::StructPropertyValue(value) => Ok(value.clone()), - _ => Err(p), - }) - .collect::>() - { - Ok(structs) => Ok(ArrayProperty::Structs { - field_name, - type_name, - guid, - structs, - }), - Err(p) => Err(SerializeError::invalid_value(format!( - "Array property_type {} doesn't match property inside array: {:#?}", - property_type, p - )))?, - }, - - (_, Some(_)) => Err(SerializeError::invalid_value( - "struct_info is only supported for StructProperty", - ))?, - - (_, None) => Ok(ArrayProperty::Properties { - property_type, - properties, - }), - } - } - - pub(crate) fn get_property_type(&self) -> Result { - Ok(match self { - ArrayProperty::Bools { bools: _ } => "BoolProperty".to_string(), - ArrayProperty::Bytes { bytes: _ } => "ByteProperty".to_string(), - ArrayProperty::Enums { enums: _ } => "EnumProperty".to_string(), - ArrayProperty::Floats { floats: _ } => "FloatProperty".to_string(), - ArrayProperty::Ints { ints: _ } => "IntProperty".to_string(), - ArrayProperty::Names { names: _ } => "NameProperty".to_string(), - ArrayProperty::Strings { strings: _ } => "StrProperty".to_string(), - ArrayProperty::Structs { - field_name: _, - type_name: _, - guid: _, - structs: _, - } => "StructProperty".to_string(), - ArrayProperty::Properties { - property_type, - properties: _, - } => property_type.clone(), - }) - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if include_header { - Self::read_header(cursor, options) - } else { - Err(DeserializeError::invalid_property( - "ArrayProperty is not supported in arrays", - cursor, - ))? - } - } - - impl_read_header!(options, length, property_type); - - #[inline] - pub(crate) fn read_body( - cursor: &mut R, - options: &mut PropertyOptions, - length: u32, - property_type: String, - ) -> Result { - let property_count = cursor.read_u32::()?; - let mut properties: Vec = Vec::with_capacity(property_count as usize); - - let mut array_struct_info = None; - - match property_type.as_str() { - "StructProperty" => { - let field_name = cursor.read_string()?; - - let property_type = cursor.read_string()?; - assert_eq!(property_type, "StructProperty"); - let properties_size = cursor.read_u64::()?; - - let struct_name = cursor.read_string()?; - let guid = cursor.read_guid()?; - let terminator = cursor.read_u8()?; - if terminator != 0 { - let position = cursor.stream_position()? - 1; - Err(DeserializeError::InvalidTerminator(terminator, position))? - } - - let properties_start = cursor.stream_position()?; - for _ in 0..property_count { - let value = StructProperty::read_body(cursor, &struct_name, options)?; - properties.push(Property::from(value)); - } - let properties_end = cursor.stream_position()?; - let actual_size = properties_end - properties_start; - validate!( - cursor, - actual_size == properties_size, - "{actual_size} != {properties_size}", - ); - - array_struct_info = Some((field_name, struct_name, guid)); - } - _ => { - let suggested_length = if property_count > 0 && length >= 4 { - Some((length - 4) / property_count) - } else { - None - }; - for _ in 0..property_count { - properties.push(Property::new( - cursor, - &property_type, - false, - options, - suggested_length, - )?) - } - } - }; - - ArrayProperty::new(property_type, array_struct_info, properties) - } -} - -impl PropertyTrait for ArrayProperty { - impl_write!(ArrayProperty, (write_string, fn, get_property_type)); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - match self { - ArrayProperty::Bools { bools } => { - let mut len = 4; - cursor.write_u32::(bools.len() as u32)?; - for b in bools { - let property = Property::from(BoolProperty::new(*b)); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Bytes { bytes } => { - let mut len = 4; - cursor.write_u32::(bytes.len() as u32)?; - for b in bytes { - let property = Property::from(ByteProperty::new_byte(None, *b)); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Enums { enums } => { - let mut len = 4; - cursor.write_u32::(enums.len() as u32)?; - for e in enums { - let property = Property::from(EnumProperty::new(None, e.to_owned())); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Floats { floats } => { - let mut len = 4; - cursor.write_u32::(floats.len() as u32)?; - for f in floats { - let property = Property::from(FloatProperty::new(f.0)); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Ints { ints } => { - let mut len = 4; - cursor.write_u32::(ints.len() as u32)?; - for i in ints { - let property = Property::from(IntProperty::new(i.to_owned())); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Names { names } => { - let mut len = 4; - cursor.write_u32::(names.len() as u32)?; - for n in names { - let property = Property::from(NameProperty::from(n.to_owned())); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Strings { strings } => { - let mut len = 4; - cursor.write_u32::(strings.len() as u32)?; - for s in strings { - let property = Property::from(StrProperty::new(s.to_owned())); - len += property.write(cursor, false, options)?; - } - Ok(len) - } - - ArrayProperty::Structs { - field_name, - type_name, - guid, - structs, - } => { - let mut len = 29; - cursor.write_u32::(structs.len() as u32)?; - len += cursor.write_string(field_name)?; - len += cursor.write_string("StructProperty")?; - - let buf = &mut Cursor::new(Vec::new()); - for property in structs { - len += property.write(buf, false, options)?; - } - let buf = buf.get_ref(); - - cursor.write_u64::(buf.len() as u64)?; - len += cursor.write_string(type_name)?; - cursor.write_guid(guid)?; - cursor.write_u8(0)?; - cursor.write_all(buf)?; - Ok(len) - } - - ArrayProperty::Properties { - property_type: _, - properties, - } => { - let mut len = 4; - cursor.write_u32::(properties.len() as u32)?; - for property in properties { - len += property.write(cursor, false, options)?; - } - Ok(len) - } - } - } -} diff --git a/src/properties/delegate_property.rs b/src/properties/delegate_property.rs deleted file mode 100644 index 8f8e0df..0000000 --- a/src/properties/delegate_property.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// An Unreal script delegate -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Delegate { - /// The object bound to this delegate - pub object: String, - /// Name of the function to call on the bound object - pub function_name: String, -} - -impl Delegate { - /// Creates a new `Delegate` instance - #[inline] - pub fn new(object: String, function_name: String) -> Self { - Delegate { - object, - function_name, - } - } - - #[inline] - pub(crate) fn read(cursor: &mut R) -> Result { - let object = cursor.read_string()?; - let function_name = cursor.read_string()?; - Ok(Delegate { - object, - function_name, - }) - } - - #[inline] - pub(crate) fn write(&self, cursor: &mut W) -> Result { - let mut len = 0; - len += cursor.write_string(&self.object)?; - len += cursor.write_string(&self.function_name)?; - Ok(len) - } -} - -/// Delegate property -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct DelegateProperty { - /// Delegate - pub value: Delegate, -} - -impl DelegateProperty { - /// Creates a new `DelegateProperty` instance - #[inline] - pub fn new(value: Delegate) -> Self { - DelegateProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - fn read_body(cursor: &mut R) -> Result { - let value = Delegate::read(cursor)?; - Ok(DelegateProperty { value }) - } -} - -impl PropertyTrait for DelegateProperty { - impl_write!(DelegateProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = self.value.write(cursor)?; - Ok(len) - } -} - -/// Multicast script delegate -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MulticastScriptDelegate { - /// Delegates - pub delegates: Vec, -} - -impl MulticastScriptDelegate { - /// Creates a new `MulticastScriptDelegate` instance - #[inline] - pub fn new(delegates: Vec) -> Self { - MulticastScriptDelegate { delegates } - } - - #[inline] - pub(crate) fn read(cursor: &mut R) -> Result { - let delegates_len = cursor.read_u32::()?; - let mut delegates = Vec::with_capacity(delegates_len as usize); - for _ in 0..delegates_len { - delegates.push(Delegate::read(cursor)?); - } - - Ok(MulticastScriptDelegate { delegates }) - } - - #[inline] - pub(crate) fn write(&self, cursor: &mut W) -> Result { - cursor.write_u32::(self.delegates.len() as u32)?; - - let mut len = 4; - for delegate in &self.delegates { - len += delegate.write(cursor)?; - } - - Ok(len) - } -} - -/// Multicast inline delegate property -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MulticastInlineDelegateProperty { - /// Delegate - pub value: MulticastScriptDelegate, -} - -impl MulticastInlineDelegateProperty { - /// Creates a new `MulticastInlineDelegateProperty` instance - #[inline] - pub fn new(value: MulticastScriptDelegate) -> Self { - MulticastInlineDelegateProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - pub(crate) fn read_body(cursor: &mut R) -> Result { - let value = MulticastScriptDelegate::read(cursor)?; - Ok(MulticastInlineDelegateProperty { value }) - } -} - -impl PropertyTrait for MulticastInlineDelegateProperty { - impl_write!(MulticastInlineDelegateProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = self.value.write(cursor)?; - Ok(len) - } -} - -/// Multicast sparse delegate property -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct MulticastSparseDelegateProperty { - /// Delegate - pub value: MulticastScriptDelegate, -} - -impl MulticastSparseDelegateProperty { - /// Creates a new `MulticastSparseDelegateProperty` instance - #[inline] - pub fn new(value: MulticastScriptDelegate) -> Self { - MulticastSparseDelegateProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - pub(crate) fn read_body(cursor: &mut R) -> Result { - let value = MulticastScriptDelegate::read(cursor)?; - Ok(MulticastSparseDelegateProperty { value }) - } -} - -impl PropertyTrait for MulticastSparseDelegateProperty { - impl_write!(MulticastSparseDelegateProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = self.value.write(cursor)?; - Ok(len) - } -} diff --git a/src/properties/enum_property.rs b/src/properties/enum_property.rs deleted file mode 100644 index 33693f6..0000000 --- a/src/properties/enum_property.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read_header, impl_write, impl_write_header_part}; - -/// A property that holds an enum value. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct EnumProperty { - /// Enum Type. - pub enum_type: Option, - /// Enum Value. - pub value: String, -} - -impl EnumProperty { - /// Creates a new `EnumProperty` instance. - #[inline] - pub fn new(enum_type: Option, value: String) -> Self { - EnumProperty { enum_type, value } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - ) -> Result { - if include_header { - Self::read_header(cursor) - } else { - Self::read_body(cursor, None) - } - } - - impl_read_header!(enum_type); - - #[inline] - fn read_body(cursor: &mut R, enum_type: Option) -> Result { - let value = cursor.read_string()?; - - Ok(EnumProperty { enum_type, value }) - } -} - -impl PropertyTrait for EnumProperty { - impl_write!(EnumProperty, (write_fstring, enum_type)); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = cursor.write_string(&self.value)?; - - Ok(len) - } -} diff --git a/src/properties/field_path_property.rs b/src/properties/field_path_property.rs deleted file mode 100644 index 0579ad1..0000000 --- a/src/properties/field_path_property.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// Field path -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FieldPath { - /// Path - pub path: Vec, - /// Resolved owner - pub resolved_owner: String, -} - -impl FieldPath { - /// Creates a new `FieldPath` instance - #[inline] - pub fn new(path: Vec, resolved_owner: String) -> Self { - FieldPath { - path, - resolved_owner, - } - } - - #[inline] - pub(crate) fn read(cursor: &mut R) -> Result { - let path_len = cursor.read_u32::()?; - let mut path = Vec::with_capacity(path_len as usize); - for _ in 0..path_len { - path.push(cursor.read_string()?); - } - - let resolved_owner = cursor.read_string()?; - - Ok(FieldPath { - path, - resolved_owner, - }) - } - - #[inline] - pub(crate) fn write(&self, cursor: &mut W) -> Result { - let mut len = 4; - cursor.write_u32::(self.path.len() as u32)?; - - for path_part in &self.path { - len += cursor.write_string(path_part)?; - } - - len += cursor.write_string(&self.resolved_owner)?; - - Ok(len) - } -} - -/// Field path property -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FieldPathProperty { - /// Field path - pub value: FieldPath, -} - -impl FieldPathProperty { - /// Creates a new `FieldPathProperty` instance - #[inline] - pub fn new(value: FieldPath) -> Self { - FieldPathProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - fn read_body(cursor: &mut R) -> Result { - let value = FieldPath::read(cursor)?; - - Ok(FieldPathProperty { value }) - } -} - -impl PropertyTrait for FieldPathProperty { - impl_write!(FieldPathProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = self.value.write(cursor)?; - Ok(len) - } -} diff --git a/src/properties/int_property.rs b/src/properties/int_property.rs deleted file mode 100644 index ab4f7e4..0000000 --- a/src/properties/int_property.rs +++ /dev/null @@ -1,337 +0,0 @@ -use std::{ - fmt::Debug, - io::{Cursor, Read, Seek, Write}, -}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use ordered_float::OrderedFloat; - -use super::{ - PropertyOptions, PropertyTrait, impl_write, - struct_types::{unwrap_value, wrap_type, wrap_value}, -}; -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::{DeserializeError, Error}, -}; - -macro_rules! check_size { - ($cursor:ident, $expected:literal) => { - let value_size = $cursor.read_u64::()?; - if value_size != $expected { - Err(DeserializeError::InvalidValueSize( - $expected, - value_size, - $cursor.stream_position()?, - ))? - } - }; -} - -macro_rules! impl_int_property { - ($name:ident, $ty:ident, $read_method:ident, $write_method:ident, $size:literal) => { - #[doc = concat!("A property that stores a `", stringify!($ty), "`.")] - #[derive(Clone, PartialEq, Eq, Hash)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct $name { - /// Integer value. - pub value: wrap_type!($ty), - } - - impl $name { - #[doc = concat!("Creates a new `", stringify!($name), "` instance.")] - #[inline] - pub fn new(value: $ty) -> Self { - let value = wrap_value!($ty, value); - Self { value } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - ) -> Result { - if include_header { - check_size!(cursor, $size); - let separator = cursor.read_u8()?; - assert_eq!(separator, 0); - } - Ok(Self::new(cursor.$read_method::()?)) - } - } - - impl Debug for $name { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}{}", self.value, stringify!($ty)) - } - } - - impl PropertyTrait for $name { - impl_write!($name); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let value = self.value; - let value = unwrap_value!($ty, value); - cursor.$write_method::(value)?; - - Ok($size) - } - } - }; -} - -/// A property that stores a `i8`. -#[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Int8Property { - /// Integer value. - pub value: i8, -} - -impl Int8Property { - /// Creates a new `Int8Property` instance. - #[inline] - pub fn new(value: i8) -> Self { - Int8Property { value } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - ) -> Result { - if include_header { - check_size!(cursor, 1); - let separator = cursor.read_u8()?; - assert_eq!(separator, 0); - } - Ok(Int8Property { - value: cursor.read_i8()?, - }) - } -} - -impl PropertyTrait for Int8Property { - impl_write!(Int8Property); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - cursor.write_i8(self.value)?; - Ok(1) - } -} - -impl Debug for Int8Property { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}i8", self.value) - } -} - -/// Byte property value -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum BytePropertyValue { - /// Byte value - Byte(u8), - /// Namespaced enum value - Namespaced(String), -} - -/// A property that stores a `u8` or the property's namespaced name. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct ByteProperty { - /// Property name. - pub name: Option, - /// Property value. - #[cfg_attr(feature = "serde", serde(flatten))] - pub value: BytePropertyValue, -} - -impl ByteProperty { - /// Creates a new `ByteProperty` instance. - #[inline] - pub fn new(name: Option, value: BytePropertyValue) -> Self { - ByteProperty { name, value } - } - - /// Creates a new `ByteProperty` instance for a u8 value - #[inline] - pub fn new_byte(name: Option, value: u8) -> Self { - ByteProperty { - name, - value: BytePropertyValue::Byte(value), - } - } - - /// Creates a new `ByteProperty` instance for a namespaced enum value - #[inline] - pub fn new_namespaced(name: Option, value: String) -> Self { - ByteProperty { - name, - value: BytePropertyValue::Namespaced(value), - } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - mut suggested_length: Option, - ) -> Result { - let mut name = None; - if include_header { - let length = cursor.read_u32::()?; - let array_index = cursor.read_u32::()?; - assert_eq!( - array_index, - 0, - "Expected array_index value zero @ {:#x}", - cursor.stream_position()? - 4 - ); - suggested_length = Some(length); - - name = Some(cursor.read_string()?); - let separator = cursor.read_u8()?; - assert_eq!(separator, 0); - } - - // -1 to account for separator - let length = suggested_length.map(|e| e - 1).unwrap_or(1); - - let value = match length { - 1 | 0 => BytePropertyValue::Byte(cursor.read_u8()?), - _ => BytePropertyValue::Namespaced(cursor.read_string()?), - }; - - Ok(ByteProperty { name, value }) - } -} - -impl PropertyTrait for ByteProperty { - #[inline] - fn write( - &self, - cursor: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if !include_header { - return self.write_body(cursor, options); - } - - let mut len = 9; - len += cursor.write_string("ByteProperty")?; - - let buf = &mut Cursor::new(Vec::new()); - len += self.write_body(buf, options)?; - let buf = buf.get_ref(); - - cursor.write_u64::(buf.len() as u64)?; - len += cursor.write_fstring(self.name.as_deref())?; - cursor.write_u8(0)?; - cursor.write_all(buf)?; - - Ok(len) - } - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - match &self.value { - BytePropertyValue::Byte(value) => { - cursor.write_u8(*value)?; - Ok(1) - } - BytePropertyValue::Namespaced(name) => cursor.write_string(name), - } - } -} - -/// A property that stores a `bool`. -#[derive(Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct BoolProperty { - /// Boolean value. - pub value: bool, -} - -impl BoolProperty { - /// Creates a new `BoolProperty` instance. - #[inline] - pub fn new(value: bool) -> Self { - BoolProperty { value } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - ) -> Result { - if include_header { - check_size!(cursor, 0); - } - let value = cursor.read_bool()?; - if include_header { - let indicator = cursor.read_u8()?; - assert_eq!(indicator, 0); - } - Ok(BoolProperty { value }) - } -} - -impl Debug for BoolProperty { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.value) - } -} - -impl PropertyTrait for BoolProperty { - #[inline] - fn write( - &self, - cursor: &mut W, - include_header: bool, - _options: &mut PropertyOptions, - ) -> Result { - let mut len = 0; - if include_header { - len += cursor.write_string("BoolProperty")?; - len += 8; - cursor.write_u64::(0)?; - } - len += 1; - cursor.write_bool(self.value)?; - if include_header { - len += 1; - cursor.write_u8(0)?; - } - Ok(len) - } - - fn write_body(&self, _: &mut W, _: &mut PropertyOptions) -> Result { - unimplemented!() - } -} - -impl_int_property!(FloatProperty, f32, read_f32, write_f32, 4); -impl_int_property!(DoubleProperty, f64, read_f64, write_f64, 8); -impl_int_property!(Int16Property, i16, read_i16, write_i16, 2); -impl_int_property!(UInt16Property, u16, read_u16, write_u16, 2); -impl_int_property!(IntProperty, i32, read_i32, write_i32, 4); -impl_int_property!(UInt32Property, u32, read_u32, write_u32, 4); -impl_int_property!(Int64Property, i64, read_i64, write_i64, 8); -impl_int_property!(UInt64Property, u64, read_u64, write_u64, 8); diff --git a/src/properties/map_property.rs b/src/properties/map_property.rs deleted file mode 100644 index 9ee6bb6..0000000 --- a/src/properties/map_property.rs +++ /dev/null @@ -1,673 +0,0 @@ -use std::{ - hash::Hash, - io::{Cursor, Read, Seek, Write}, -}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::{DeserializeError, Error}, - properties::{ - Property, PropertyOptions, PropertyTrait, - enum_property::EnumProperty, - impl_read_header, impl_write, impl_write_header_part, - int_property::{BoolProperty, IntProperty}, - name_property::NameProperty, - str_property::StrProperty, - }, - scoped_stack_entry::ScopedStackEntry, - types::map::HashableIndexMap, -}; - -/// A property that stores a map of properties to properties. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(untagged))] -pub enum MapProperty { - /// Map - EnumBool { - /// Map entries. - enum_bools: HashableIndexMap, - }, - /// Map - EnumInt { - /// Map entries. - enum_ints: HashableIndexMap, - }, - /// Map - EnumProperty { - /// Value type. - value_type: String, - /// Map entries. - enum_props: HashableIndexMap, - }, - /// Map - NameBool { - /// Map entries. - name_bools: HashableIndexMap, - }, - /// Map - NameInt { - /// Map entries. - name_ints: HashableIndexMap, - }, - /// Map - NameProperty { - /// Value type. - value_type: String, - /// Map entries. - name_props: HashableIndexMap, - }, - /// Map - Properties { - /// Key type name. - key_type: String, - /// Value type name. - value_type: String, - /// Allocation flags. - allocation_flags: u32, - /// Map entries. - #[cfg_attr(feature = "serde", serde(with = "crate::types::map::serde_seq"))] - value: HashableIndexMap, - }, - /// Map - StrBool { - /// Map entries. - str_bools: HashableIndexMap, - }, - /// Map - StrInt { - /// Map entries. - str_ints: HashableIndexMap, - }, - /// Map - StrProperty { - /// Value type. - value_type: String, - /// Map entries. - str_props: HashableIndexMap, - }, - /// Map - StrStr { - /// Map entries. - str_strs: HashableIndexMap>, - }, -} - -impl MapProperty { - /// Creates a new `MapProperty` instance. - #[inline] - pub fn new( - key_type: String, - value_type: String, - allocation_flags: u32, - value: HashableIndexMap, - ) -> Self { - match (key_type.as_str(), value_type.as_str(), allocation_flags) { - ("EnumProperty", "BoolProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::EnumProperty(EnumProperty { - enum_type: None, - value: key, - }), - Property::BoolProperty(BoolProperty { value }), - ) => Ok((key.clone(), *value)), - // _ => Err(e), - _ => Err(()), - }) - .collect::>() - { - Ok(enum_bools) => MapProperty::EnumBool { - enum_bools: HashableIndexMap(enum_bools), - }, - // Err(e) => Err(SerializeError::invalid_value(&format!( - // "Map entry type does not match container type ({}, {}): {:#?}", - // key_type, value_type, e - // )))?, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("EnumProperty", "IntProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::EnumProperty(EnumProperty { - enum_type: None, - value: key, - }), - Property::IntProperty(IntProperty { value }), - ) => Ok((key.clone(), *value)), - _ => Err(()), - }) - .collect::>() - { - Ok(enum_ints) => MapProperty::EnumInt { - enum_ints: HashableIndexMap(enum_ints), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("EnumProperty", _, 0) => { - match value - .iter() - .map(|e| match e { - ( - Property::EnumProperty(EnumProperty { - enum_type: None, - value: key, - }), - value, - ) => Ok((key.clone(), value.clone())), - _ => Err(()), - }) - .collect::>() - { - Ok(enum_props) => MapProperty::EnumProperty { - value_type, - enum_props: HashableIndexMap(enum_props), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - } - } - - ("NameProperty", "BoolProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::NameProperty(NameProperty { - array_index: 0, - value: Some(key), - }), - Property::BoolProperty(BoolProperty { value }), - ) => Ok((key.clone(), *value)), - _ => Err(()), - }) - .collect::>() - { - Ok(name_bools) => MapProperty::NameBool { - name_bools: HashableIndexMap(name_bools), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("NameProperty", "IntProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::NameProperty(NameProperty { - array_index: 0, - value: Some(key), - }), - Property::IntProperty(IntProperty { value }), - ) => Ok((key.clone(), *value)), - _ => Err(()), - }) - .collect::>() - { - Ok(name_ints) => MapProperty::NameInt { - name_ints: HashableIndexMap(name_ints), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("NameProperty", _, 0) => { - match value - .iter() - .map(|e| match e { - ( - Property::NameProperty(NameProperty { - array_index: 0, - value: Some(key), - }), - value, - ) => Ok((key.clone(), value.clone())), - _ => Err(()), - }) - .collect::>() - { - Ok(name_props) => MapProperty::NameProperty { - value_type, - name_props: HashableIndexMap(name_props), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - } - } - - ("StrProperty", "BoolProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::StrProperty(StrProperty { value: Some(key) }), - Property::BoolProperty(BoolProperty { value }), - ) => Ok((key.clone(), *value)), - _ => Err(()), - }) - .collect::>() - { - Ok(str_bools) => MapProperty::StrBool { - str_bools: HashableIndexMap(str_bools), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("StrProperty", "IntProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::StrProperty(StrProperty { value: Some(key) }), - Property::IntProperty(IntProperty { value }), - ) => Ok((key.clone(), *value)), - _ => Err(()), - }) - .collect::>() - { - Ok(str_ints) => MapProperty::StrInt { - str_ints: HashableIndexMap(str_ints), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("StrProperty", "StrProperty", 0) => match value - .iter() - .map(|e| match e { - ( - Property::StrProperty(StrProperty { value: Some(key) }), - Property::StrProperty(StrProperty { value }), - ) => Ok((key.clone(), value.clone())), - _ => Err(()), - }) - .collect::>() - { - Ok(str_strs) => MapProperty::StrStr { - str_strs: HashableIndexMap(str_strs), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - }, - - ("StrProperty", _, 0) => { - match value - .iter() - .map(|e| match e { - (Property::StrProperty(StrProperty { value: Some(key) }), value) => { - Ok((key.clone(), value.clone())) - } - _ => Err(()), - }) - .collect::>() - { - Ok(str_props) => MapProperty::StrProperty { - value_type, - str_props: HashableIndexMap(str_props), - }, - Err(_) => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - } - } - - _ => MapProperty::Properties { - key_type, - value_type, - allocation_flags, - value, - }, - } - } - - #[inline] - pub(crate) fn get_key_type(&self) -> Result<&str, Error> { - Ok(self.key_type()) - } - - #[inline] - pub(crate) fn get_value_type(&self) -> Result<&str, Error> { - Ok(self.value_type()) - } - - #[inline] - fn key_type(&self) -> &str { - match self { - MapProperty::EnumBool { enum_bools: _ } => "EnumProperty", - MapProperty::EnumInt { enum_ints: _ } => "EnumProperty", - MapProperty::EnumProperty { - value_type: _, - enum_props: _, - } => "EnumProperty", - MapProperty::NameBool { name_bools: _ } => "NameProperty", - MapProperty::NameInt { name_ints: _ } => "NameProperty", - MapProperty::NameProperty { - value_type: _, - name_props: _, - } => "NameProperty", - MapProperty::Properties { - key_type, - value_type: _, - allocation_flags: _, - value: _, - } => key_type, - MapProperty::StrBool { str_bools: _ } => "StrProperty", - MapProperty::StrInt { str_ints: _ } => "StrProperty", - MapProperty::StrProperty { - value_type: _, - str_props: _, - } => "StrProperty", - MapProperty::StrStr { str_strs: _ } => "StrProperty", - } - } - - #[inline] - fn value_type(&self) -> &str { - match self { - MapProperty::EnumBool { enum_bools: _ } => "BoolProperty", - MapProperty::EnumInt { enum_ints: _ } => "IntProperty", - MapProperty::EnumProperty { - value_type, - enum_props: _, - } => value_type, - MapProperty::NameBool { name_bools: _ } => "BoolProperty", - MapProperty::NameInt { name_ints: _ } => "IntProperty", - MapProperty::NameProperty { - value_type, - name_props: _, - } => value_type, - MapProperty::Properties { - key_type: _, - value_type, - allocation_flags: _, - value: _, - } => value_type, - MapProperty::StrBool { str_bools: _ } => "BoolProperty", - MapProperty::StrInt { str_ints: _ } => "IntProperty", - MapProperty::StrProperty { - value_type, - str_props: _, - } => value_type, - MapProperty::StrStr { str_strs: _ } => "StrProperty", - } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if include_header { - Self::read_header(cursor, options) - } else { - Err(DeserializeError::invalid_property( - "MapProperty is not supported in arrays", - cursor, - ))? - } - } - - impl_read_header!(options, key_type, value_type); - - #[inline] - fn read_body( - cursor: &mut R, - options: &mut PropertyOptions, - key_type: String, - value_type: String, - ) -> Result { - let allocation_flags = cursor.read_u32::()?; - let element_count = cursor.read_u32::()?; - - let mut map = HashableIndexMap::with_capacity(element_count as usize); - for _ in 0..element_count { - let properties_stack = &mut options.properties_stack; - let key_stack_entry = ScopedStackEntry::new(properties_stack, "Key".to_string()); - let key = Property::new(cursor, &key_type, false, options, None)?; - drop(key_stack_entry); - - let properties_stack = &mut options.properties_stack; - let value_stack_entry = ScopedStackEntry::new(properties_stack, "Value".to_string()); - let value = Property::new(cursor, &value_type, false, options, None)?; - drop(value_stack_entry); - - map.insert(key, value); - } - - Ok(MapProperty::new( - key_type, - value_type, - allocation_flags, - map, - )) - } -} - -impl PropertyTrait for MapProperty { - impl_write!( - MapProperty, - (write_string, fn, get_key_type), - (write_string, fn, get_value_type) - ); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - match self { - MapProperty::EnumBool { - enum_bools: HashableIndexMap(enum_bools), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(enum_bools.len() as u32)?; - let mut len = 8; - for (key, value) in enum_bools { - let k_property = EnumProperty::new(None, key.clone()); - let v_property = BoolProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::EnumInt { - enum_ints: HashableIndexMap(enum_ints), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(enum_ints.len() as u32)?; - let mut len = 8; - for (key, value) in enum_ints { - let k_property = EnumProperty::new(None, key.clone()); - let v_property = IntProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::EnumProperty { - value_type: _, - enum_props: HashableIndexMap(enum_props), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(enum_props.len() as u32)?; - let mut len = 8; - for (key, value) in enum_props { - let property = EnumProperty::new(None, key.clone()); - len += property.write(cursor, false, options)?; - len += value.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::NameBool { - name_bools: HashableIndexMap(name_bools), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(name_bools.len() as u32)?; - let mut len = 8; - for (key, value) in name_bools { - let k_property = NameProperty::from(key.clone()); - let v_property = BoolProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::NameInt { - name_ints: HashableIndexMap(name_ints), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(name_ints.len() as u32)?; - let mut len = 8; - for (key, value) in name_ints { - let k_property = NameProperty::from(key.clone()); - let v_property = IntProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::NameProperty { - value_type: _, - name_props: HashableIndexMap(name_props), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(name_props.len() as u32)?; - let mut len = 8; - for (key, value) in name_props { - let property = NameProperty::from(key.clone()); - len += property.write(cursor, false, options)?; - len += value.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::Properties { - key_type: _, - value_type: _, - allocation_flags, - value: HashableIndexMap(value), - } => { - cursor.write_u32::(*allocation_flags)?; - cursor.write_u32::(value.len() as u32)?; - let mut len = 8; - for (key, value) in value { - len += key.write(cursor, false, options)?; - len += value.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::StrBool { - str_bools: HashableIndexMap(str_bools), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(str_bools.len() as u32)?; - let mut len = 8; - for (key, value) in str_bools { - let k_property = StrProperty::from(key.clone()); - let v_property = BoolProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::StrInt { - str_ints: HashableIndexMap(str_ints), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(str_ints.len() as u32)?; - let mut len = 8; - for (key, value) in str_ints { - let k_property = StrProperty::from(key.clone()); - let v_property = IntProperty::new(*value); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::StrProperty { - value_type: _, - str_props: HashableIndexMap(str_props), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(str_props.len() as u32)?; - let mut len = 8; - for (key, value) in str_props { - let property = StrProperty::from(key.clone()); - len += property.write(cursor, false, options)?; - len += value.write(cursor, false, options)?; - } - Ok(len) - } - - MapProperty::StrStr { - str_strs: HashableIndexMap(str_strs), - } => { - cursor.write_u32::(0)?; - cursor.write_u32::(str_strs.len() as u32)?; - let mut len = 8; - for (key, value) in str_strs { - let k_property = StrProperty::from(key.clone()); - let v_property = StrProperty::new(value.clone()); - len += k_property.write(cursor, false, options)?; - len += v_property.write(cursor, false, options)?; - } - Ok(len) - } - } - } -} diff --git a/src/properties/mod.rs b/src/properties/mod.rs deleted file mode 100644 index 963f515..0000000 --- a/src/properties/mod.rs +++ /dev/null @@ -1,658 +0,0 @@ -use std::{ - collections::HashMap, - fmt::Debug, - hash::Hash, - io::{Read, Seek, Write}, -}; - -use enum_dispatch::enum_dispatch; - -use crate::{ - custom_version::{CustomVersionTrait, FCustomVersion}, - error::{DeserializeError, Error}, - scoped_stack_entry::ScopedStackEntry, - types::{Guid, map::HashableIndexMap}, -}; - -use self::{ - array_property::ArrayProperty, - delegate_property::{ - DelegateProperty, MulticastInlineDelegateProperty, MulticastSparseDelegateProperty, - }, - enum_property::EnumProperty, - field_path_property::FieldPathProperty, - int_property::{ - BoolProperty, ByteProperty, DoubleProperty, FloatProperty, Int8Property, Int16Property, - Int64Property, IntProperty, UInt16Property, UInt32Property, UInt64Property, - }, - map_property::MapProperty, - name_property::NameProperty, - object_property::ObjectProperty, - set_property::SetProperty, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, - text_property::TextProperty, - unknown_property::UnknownProperty, -}; - -/// Module for `ArrayProperty`. -pub mod array_property; -/// Module for delegates -pub mod delegate_property; -/// Module for `EnumProperty`. -pub mod enum_property; -/// Module for `FieldPathProperty` -pub mod field_path_property; -/// Module for `IntProperty` and various integer properties. -pub mod int_property; -/// Module for `MapProperty` -pub mod map_property; -/// Module for `NameProperty` -pub mod name_property; -/// Module for `ObjectProperty` -pub mod object_property; -/// Module for `SetProperty` -pub mod set_property; -/// Module for `StrProperty` -pub mod str_property; -/// Module for `StructProperty` -pub mod struct_property; -/// Module for `StructProperty` sub-types. -pub mod struct_types; -/// Module for `TextProperty` -pub mod text_property; -/// Module for `UnknownProperty` -pub mod unknown_property; - -/// This macro generates a `read` function for reading GVAS property data from a reader. -/// -/// If `include_header` is true, it will read the property header first. -/// -/// This macro must be used in conjunction with a suitable `read_header` function, such as one -/// generated by `impl_read_header!(...)`. -macro_rules! impl_read { - () => { - /// Read GVAS property data from a reader. - /// - /// If `include_header` is true, read the property header first. - #[inline] - pub fn read(reader: &mut R, include_header: bool) -> Result { - if include_header { - Self::read_header(reader) - } else { - Self::read_body(reader) - } - } - }; - - (options) => { - /// Read GVAS property data from a reader. - /// - /// If `include_header` is true, read the property header first. - #[inline] - pub fn read( - reader: &mut R, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if include_header { - Self::read_header(reader, options) - } else { - Self::read_body(reader, options) - } - } - }; - - (array_index) => { - /// Read GVAS property data from a reader. - /// - /// If `include_header` is true, read the property header first. - #[inline] - pub fn read(reader: &mut R, include_header: bool) -> Result { - if include_header { - Self::read_header(reader) - } else { - Self::read_body(reader, 0) - } - } - }; -} - -/// This macro generates a `read_header` function for reading GVAS property headers from a reader. -/// -/// This macro must be used in conjunction with a suitable `read_body` function. -/// -/// ```ignore -/// use std::io::{Read, Seek}; -/// -/// use byteorder::{LittleEndian, ReadBytesExt}; -/// -/// use crate::{ -/// error::Error, -/// properties::{impl_read, impl_read_header}, -/// }; -/// -/// struct ExampleProperty( -/// // ... -/// ); -/// -/// impl ExampleProperty { -/// impl_read!(); -/// impl_read_header!(); -/// fn read_body(reader: &mut R) -> Result { -/// // Read values from reader... -/// Ok(Self ( -/// // ... -/// )) -/// } -/// } -/// ``` -macro_rules! impl_read_header { - (options, length $(, $var:ident)*) => { - /// Read GVAS property data from a reader. - #[inline] - pub fn read_header( - reader: &mut R, - options: &mut PropertyOptions, - ) -> Result { - let length = reader.read_u32::()?; - let array_index = reader.read_u32::()?; - if array_index != 0 { - let position = reader.stream_position()? - 4; - Err($crate::error::DeserializeError::InvalidArrayIndex(array_index, position))? - } - $( - let $var = reader.read_string()?; - )* - let terminator = reader.read_u8()?; - if terminator != 0 { - let position = reader.stream_position()? - 1; - Err($crate::error::DeserializeError::InvalidTerminator(terminator, position))? - } - - let start = reader.stream_position()?; - let result = Self::read_body(reader, options, length $(, $var)*)?; - let end = reader.stream_position()?; - if end - start != length as u64 { - Err($crate::error::DeserializeError::InvalidValueSize(length as u64, end - start, start))? - } - - Ok(result) - } - }; - - (options $(, $var:ident)*) => { - /// Read GVAS property data from a reader. - #[inline] - pub fn read_header( - reader: &mut R, - options: &mut PropertyOptions, - ) -> Result { - let length = reader.read_u32::()?; - let array_index = reader.read_u32::()?; - if array_index != 0 { - let position = reader.stream_position()? - 4; - Err($crate::error::DeserializeError::InvalidArrayIndex(array_index, position))? - } - $( - let $var = reader.read_string()?; - )* - let terminator = reader.read_u8()?; - if terminator != 0 { - let position = reader.stream_position()? - 1; - Err($crate::error::DeserializeError::InvalidTerminator(terminator, position))? - } - - let start = reader.stream_position()?; - let result = Self::read_body(reader, options $(, $var)*)?; - let end = reader.stream_position()?; - if end - start != length as u64 { - Err($crate::error::DeserializeError::InvalidValueSize(length as u64, end - start, start))? - } - - Ok(result) - } - }; - - (array_index $(, $var:ident)*) => { - /// Read GVAS property data from a reader. - #[inline] - pub fn read_header( - reader: &mut R, - ) -> Result { - let length = reader.read_u32::()?; - let array_index = reader.read_u32::()?; - $( - let $var = reader.read_string()?; - )* - let terminator = reader.read_u8()?; - if terminator != 0 { - let position = reader.stream_position()? - 1; - Err($crate::error::DeserializeError::InvalidTerminator(terminator, position))? - } - - let start = reader.stream_position()?; - let result = Self::read_body(reader, array_index $(, Some($var))*)?; - let end = reader.stream_position()?; - if end - start != length as u64 { - Err($crate::error::DeserializeError::InvalidValueSize(length as u64, end - start, start))? - } - - Ok(result) - } - }; - - ($($var:ident $(,)? )*) => { - /// Read GVAS property data from a reader. - #[inline] - pub fn read_header( - reader: &mut R, - ) -> Result { - let length = reader.read_u32::()?; - let array_index = reader.read_u32::()?; - if array_index != 0 { - let position = reader.stream_position()? - 4; - Err($crate::error::DeserializeError::InvalidArrayIndex(array_index, position))? - } - $( - let $var = reader.read_string()?; - )* - let terminator = reader.read_u8()?; - if terminator != 0 { - let position = reader.stream_position()? - 1; - Err($crate::error::DeserializeError::InvalidTerminator(terminator, position))? - } - - let start = reader.stream_position()?; - let result = Self::read_body(reader $(, Some($var))*)?; - let end = reader.stream_position()?; - if end - start != length as u64 { - Err($crate::error::DeserializeError::InvalidValueSize(length as u64, end - start, start))? - } - - Ok(result) - } - }; -} - -pub(crate) use impl_read; -pub(crate) use impl_read_header; - -/// This macro generates a `write` function for writing the property data to a writer. -/// If `include_header` is true, it will write the property header first. -/// -/// # Examples -/// -/// ```ignore -/// impl_write!(ArrayProperty, options, (write_string, property_type)); -/// ``` -/// -/// This generates a `write` function for the `ArrayProperty` type with `PropertyOptions` support, -/// writing `&self.property_name` in to the header using the `write_string` function. -/// -/// ```ignore -/// impl_write!(NameProperty); -/// ``` -/// -/// This generates a basic `write` function for the `NameProperty` type. -/// -/// ```ignore -/// impl_write!( -/// StructProperty, -/// options, -/// (write_string, fn, get_property_name), -/// (write_guid, guid) -/// ); -/// ``` -/// -/// This generateds and advanced `write` function for the `StructProperty` type, writing -/// `&self.get_property_name()?` in to the header using the `write_string` function, and -/// `&self.guid` using the `write_guid` function. -/// -/// # Notes -/// -/// This macro must be used in conjunction with a suitable `write_body` function. -macro_rules! impl_write { - ($property:ident, array_index $(, $header_property:tt)*) => { - #[inline] - fn write( - &self, - writer: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if !include_header { - return self.write_body(writer, options); - } - - let mut len = 9; - let buf = &mut Cursor::new(Vec::new()); - len += self.write_body(buf, options)?; - let buf = buf.get_ref(); - - writer.write_string(stringify!($property))?; - writer.write_u32::(buf.len() as u32)?; - writer.write_u32::(self.array_index)?; - $( - len += impl_write_header_part!(self, writer, $header_property); - )* - writer.write_u8(0)?; - writer.write_all(buf)?; - - Ok(len) - } - }; - - ($property:ident $(, $header_property:tt)*) => { - #[inline] - fn write( - &self, - writer: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if !include_header { - return self.write_body(writer, options); - } - - let mut len = 9; - let buf = &mut Cursor::new(Vec::new()); - len += self.write_body(buf, options)?; - let buf = buf.get_ref(); - - len += writer.write_string(stringify!($property))?; - writer.write_u32::(buf.len() as u32)?; - writer.write_u32::(0)?; - $( - len += impl_write_header_part!(self, writer, $header_property); - )* - writer.write_u8(0)?; - writer.write_all(buf)?; - - Ok(len) - } - }; -} - -/// A helper macro for writing property header parts. -/// -/// This macro is used inside the `impl_write!` macro to write individual parts of a property header. -macro_rules! impl_write_header_part { - ($self:ident, $writer:ident, (write_fstring, $member:ident)) => { - $writer.write_fstring($self.$member.as_deref())? - }; - - ($self:ident, $writer:ident, (write_guid, $member:ident)) => {{ - $writer.write_guid(&$self.$member)?; - 16 - }}; - - ($self:ident, $writer:ident, ($write_fn:ident, $member:ident)) => { - $writer.$write_fn(&$self.$member)? - }; - - ($self:ident, $writer:ident, ($write_fn:ident, fn, $member:ident)) => { - $writer.$write_fn(&$self.$member()?)? - }; -} - -pub(crate) use impl_write; -pub(crate) use impl_write_header_part; - -/// This macro generates a helper function for matching a specific variant of an enum. -/// -/// # Examples -/// -/// ```ignore -/// make_matcher!(MyEnumVariant, get_my_enum_variant); -/// ``` -/// -/// This generates a `get_my_enum_variant` function that returns an `Option<&MyEnumVariant>` -/// if the enum instance is of the `MyEnumVariant` variant. -macro_rules! make_matcher { - ($type:ident, $name:ident, $name_mut:ident) => { - #[doc = concat!("Retrieves the enum value as a `", stringify!($type), "`.")] - #[inline] - pub fn $name(&self) -> Option<&$type> { - match self { - Self::$type(e) => Some(e), - _ => None, - } - } - - #[doc = concat!("Retrieves the mutable enum value as a `", stringify!($type), "`.")] - #[inline] - pub fn $name_mut(&mut self) -> Option<&mut $type> { - match self { - Self::$type(e) => Some(e), - _ => None, - } - } - }; -} - -pub(crate) use make_matcher; - -/// Property options used for reading and writing. -pub struct PropertyOptions<'a> { - /// Hints about property types. - pub hints: &'a HashMap, - /// Tracks the property tree location in a GVAS file. - pub properties_stack: &'a mut Vec, - /// Custom versions - pub custom_versions: &'a HashableIndexMap, -} - -impl PropertyOptions<'_> { - /// Get custom version - #[inline] - pub fn get_custom_version(&self) -> FCustomVersion - where - T: CustomVersionTrait + Into, - { - let key = T::GUID; - let version = self.custom_versions.get(&key).copied().unwrap_or(0); - FCustomVersion { key, version } - } - - /// Check for custom version support - #[inline] - pub fn supports_version(&self, required: T) -> bool - where - T: CustomVersionTrait + Into, - { - self.get_custom_version::().version >= required.into() - } -} - -/// Property traits. -#[enum_dispatch] -pub trait PropertyTrait: Debug + Clone + PartialEq + Eq + Hash { - /// Serialize. - fn write( - &self, - cursor: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result; - - /// Serialize body. - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result; -} - -/// GVAS property types. -#[enum_dispatch(PropertyTrait)] -#[cfg_attr( - feature = "serde", - derive(serde::Serialize, serde::Deserialize), - serde(tag = "type") -)] -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Property { - /// An `ArrayProperty`. - ArrayProperty, - /// A `BoolProperty`. - BoolProperty, - /// A `ByteProperty`. - ByteProperty, - /// A `DoubleProperty`. - DoubleProperty, - /// An `EnumProperty`. - EnumProperty, - /// A `FloatPropertyF`. - FloatProperty, - /// An `Int16Property`. - Int16Property, - /// An `Int64Property`. - Int64Property, - /// An `Int8Property`. - Int8Property, - /// An `IntProperty`. - IntProperty, - /// A `MapProperty`. - MapProperty, - /// A `NameProperty`. - NameProperty, - /// An `ObjectProperty` - ObjectProperty, - /// A `DelegateProperty` - DelegateProperty, - /// A `MulticastInlineDelegateProperty` - MulticastInlineDelegateProperty, - /// A `MulticastSparseDelegateProperty` - MulticastSparseDelegateProperty, - /// A `FieldPathProperty` - FieldPathProperty, - /// A `SetProperty`. - SetProperty, - /// A `StrProperty`. - StrProperty, - /// A `StructProperty`. - StructProperty, - /// A raw `StructPropertyValue`. - StructPropertyValue, - /// A `TextProperty`. - TextProperty, - /// A `UInt16Property`. - UInt16Property, - /// A `UInt32Property`. - UInt32Property, - /// A `UInt64Property`. - UInt64Property, - /// An `UnknownProperty`. - UnknownProperty, -} - -impl Property { - /// Creates a new `Property` instance. - pub fn new( - cursor: &mut R, - value_type: &str, - include_header: bool, - options: &mut PropertyOptions, - suggested_length: Option, - ) -> Result { - let _stack_entry = ScopedStackEntry::new(options.properties_stack, value_type.to_string()); - match value_type { - "Int8Property" => Ok(Int8Property::read(cursor, include_header)?.into()), - "ByteProperty" => { - Ok(ByteProperty::read(cursor, include_header, suggested_length)?.into()) - } - "Int16Property" => Ok(Int16Property::read(cursor, include_header)?.into()), - "UInt16Property" => Ok(UInt16Property::read(cursor, include_header)?.into()), - "IntProperty" => Ok(IntProperty::read(cursor, include_header)?.into()), - "UInt32Property" => Ok(UInt32Property::read(cursor, include_header)?.into()), - "Int64Property" => Ok(Int64Property::read(cursor, include_header)?.into()), - "UInt64Property" => Ok(UInt64Property::read(cursor, include_header)?.into()), - "FloatProperty" => Ok(FloatProperty::read(cursor, include_header)?.into()), - "DoubleProperty" => Ok(DoubleProperty::read(cursor, include_header)?.into()), - "BoolProperty" => Ok(BoolProperty::read(cursor, include_header)?.into()), - "EnumProperty" => Ok(EnumProperty::read(cursor, include_header)?.into()), - "StrProperty" => Ok(StrProperty::read(cursor, include_header)?.into()), - "TextProperty" => Ok(TextProperty::read(cursor, include_header, options)?.into()), - "NameProperty" => Ok(NameProperty::read(cursor, include_header)?.into()), - "ObjectProperty" => Ok(ObjectProperty::read(cursor, include_header)?.into()), - "DelegateProperty" => Ok(DelegateProperty::read(cursor, include_header)?.into()), - "MulticastInlineDelegateProperty" => { - Ok(MulticastInlineDelegateProperty::read(cursor, include_header)?.into()) - } - "MulticastSparseDelegateProperty" => { - Ok(MulticastSparseDelegateProperty::read(cursor, include_header)?.into()) - } - "FieldPathProperty" => Ok(FieldPathProperty::read(cursor, include_header)?.into()), - "StructProperty" => match include_header { - true => Ok(StructProperty::read(cursor, include_header, options)?.into()), - false => { - let struct_path = options.properties_stack.join("."); - let Some(hint) = options.hints.get(&struct_path) else { - Err(DeserializeError::MissingHint( - "StructProperty".into(), - struct_path.into_boxed_str(), - cursor.stream_position()?, - ))? - }; - Ok(StructProperty::read_body(cursor, hint, options)?.into()) - } - }, - "ArrayProperty" => Ok(ArrayProperty::read(cursor, include_header, options)?.into()), - "SetProperty" => Ok(SetProperty::read(cursor, include_header, options)?.into()), - "MapProperty" => Ok(MapProperty::read(cursor, include_header, options)?.into()), - _ => { - if include_header { - return Ok( - UnknownProperty::read_with_header(cursor, value_type.to_string())?.into(), - ); - } - - if let Some(suggested_length) = suggested_length { - return Ok(UnknownProperty::read_with_length( - cursor, - value_type.to_string(), - suggested_length, - )? - .into()); - } - - Err(DeserializeError::invalid_property(value_type, cursor))? - } - } - } - - make_matcher!(ArrayProperty, get_array, get_array_mut); - make_matcher!(EnumProperty, get_enum, get_enum_mut); - make_matcher!(BoolProperty, get_bool, get_bool_mut); - make_matcher!(ByteProperty, get_byte, get_byte_mut); - make_matcher!(DoubleProperty, get_f64, get_f64_mut); - make_matcher!(FloatProperty, get_f32, get_f32_mut); - make_matcher!(Int16Property, get_i16, get_i16_mut); - make_matcher!(Int64Property, get_i64, get_i64_mut); - make_matcher!(Int8Property, get_i8, get_i8_mut); - make_matcher!(IntProperty, get_int, get_int_mut); - make_matcher!(UInt16Property, get_u16, get_u16_mut); - make_matcher!(UInt32Property, get_u32, get_u32_mut); - make_matcher!(UInt64Property, get_u64, get_u64_mut); - make_matcher!(MapProperty, get_map, get_map_mut); - make_matcher!(NameProperty, get_name, get_name_mut); - make_matcher!(ObjectProperty, get_object_ref, get_object_ref_mut); - make_matcher!(DelegateProperty, get_delegate, get_delegate_mut); - make_matcher!( - MulticastInlineDelegateProperty, - get_multicast_inline_delegate, - get_multicast_inline_delegate_mut - ); - make_matcher!( - MulticastSparseDelegateProperty, - get_multicast_sparse_delegate, - get_multicast_sparse_delegate_mut - ); - make_matcher!(FieldPathProperty, get_field_path, get_field_path_mut); - make_matcher!(SetProperty, get_set, get_set_mut); - make_matcher!(StrProperty, get_str, get_str_mut); - make_matcher!(StructProperty, get_struct, get_struct_mut); - make_matcher!(TextProperty, get_text, get_text_mut); - make_matcher!(UnknownProperty, get_unknown, get_unknown_mut); -} diff --git a/src/properties/name_property.rs b/src/properties/name_property.rs deleted file mode 100644 index f695574..0000000 --- a/src/properties/name_property.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{cursor_ext::ReadExt, cursor_ext::WriteExt, error::Error}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// A property that holds a name. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct NameProperty { - /// Array Index - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "is_zero"))] - #[cfg_attr(feature = "serde", serde(default))] - pub array_index: u32, - /// Name value. - pub value: Option, -} - -#[cfg(feature = "serde")] -fn is_zero(num: &u32) -> bool { - *num == 0 -} - -impl From<&str> for NameProperty { - #[inline] - fn from(value: &str) -> Self { - NameProperty::from(Some(value.into())) - } -} - -impl From for NameProperty { - #[inline] - fn from(value: String) -> Self { - Self::from(Some(value)) - } -} - -impl From> for NameProperty { - #[inline] - fn from(value: Option) -> Self { - let array_index: u32 = 0; - NameProperty { array_index, value } - } -} - -impl NameProperty { - impl_read!(array_index); - impl_read_header!(array_index); - - #[inline] - fn read_body(cursor: &mut R, array_index: u32) -> Result { - let value = cursor.read_fstring()?; - Ok(NameProperty { array_index, value }) - } -} - -impl PropertyTrait for NameProperty { - impl_write!(NameProperty, array_index); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _options: &mut PropertyOptions, - ) -> Result { - let len = cursor.write_fstring(self.value.as_deref())?; - Ok(len) - } -} diff --git a/src/properties/object_property.rs b/src/properties/object_property.rs deleted file mode 100644 index 0056bcf..0000000 --- a/src/properties/object_property.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// A property that describes a reference variable to another object which may be nil. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct ObjectProperty { - /// Object reference - pub value: String, -} - -impl From<&str> for ObjectProperty { - #[inline] - fn from(value: &str) -> Self { - ObjectProperty::new(value.into()) - } -} - -impl ObjectProperty { - /// Creates a new `ObjectProperty` instance - #[inline] - pub fn new(value: String) -> Self { - ObjectProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - fn read_body(cursor: &mut R) -> Result { - let value = cursor.read_string()?; - Ok(ObjectProperty { value }) - } -} - -impl PropertyTrait for ObjectProperty { - impl_write!(ObjectProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - cursor.write_string(&self.value) - } -} diff --git a/src/properties/set_property.rs b/src/properties/set_property.rs deleted file mode 100644 index 1bc82db..0000000 --- a/src/properties/set_property.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::{DeserializeError, Error}, -}; - -use super::{ - Property, PropertyOptions, PropertyTrait, impl_read_header, impl_write, impl_write_header_part, -}; - -/// A property that stores a set of properties. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct SetProperty { - /// Property type. - pub property_type: String, - /// Allocation flags. - pub allocation_flags: u32, - /// Properties. - pub properties: Vec, -} - -impl SetProperty { - /// Creates a new `SetProperty` instance. - #[inline] - pub fn new(property_type: String, allocation_flags: u32, properties: Vec) -> Self { - SetProperty { - property_type, - allocation_flags, - properties, - } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if include_header { - Self::read_header(cursor, options) - } else { - Err(DeserializeError::invalid_property( - "SetProperty is not supported in arrays", - cursor, - ))? - } - } - - impl_read_header!(options, length, property_type); - - #[inline] - pub(crate) fn read_body( - cursor: &mut R, - options: &mut PropertyOptions, - length: u32, - property_type: String, - ) -> Result { - let allocation_flags = cursor.read_u32::()?; - - let element_count = cursor.read_u32::()?; - let mut properties: Vec = Vec::with_capacity(element_count as usize); - - if element_count > 0 - && let Some(total_bytes_per_property) = (length - 8).checked_div(element_count) - { - for _ in 0..element_count { - properties.push(Property::new( - cursor, - &property_type, - false, - options, - Some(total_bytes_per_property), - )?) - } - } - - Ok(SetProperty { - property_type, - allocation_flags, - properties, - }) - } -} - -impl PropertyTrait for SetProperty { - impl_write!(SetProperty, (write_string, property_type)); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - cursor.write_u32::(self.allocation_flags)?; - cursor.write_u32::(self.properties.len() as u32)?; - let mut len = 8; - for property in &self.properties { - len += property.write(cursor, false, options)?; - } - - Ok(len) - } -} diff --git a/src/properties/str_property.rs b/src/properties/str_property.rs deleted file mode 100644 index 2737122..0000000 --- a/src/properties/str_property.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// A property that holds a GVAS string value. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct StrProperty { - /// Value of the GVAS string. - pub value: Option, -} - -impl From<&str> for StrProperty { - #[inline] - fn from(value: &str) -> Self { - Self::from(value.to_string()) - } -} - -impl From for StrProperty { - #[inline] - fn from(value: String) -> Self { - Self::new(Some(value)) - } -} - -impl StrProperty { - /// Creates a new `StrProperty` instance. - #[inline] - pub fn new(value: Option) -> Self { - StrProperty { value } - } - - impl_read!(); - impl_read_header!(); - - #[inline] - fn read_body(cursor: &mut R) -> Result { - let value = cursor.read_fstring()?; - Ok(StrProperty { value }) - } -} - -impl PropertyTrait for StrProperty { - impl_write!(StrProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - let len = cursor.write_fstring(self.value.as_deref())?; - Ok(len) - } -} diff --git a/src/properties/struct_property.rs b/src/properties/struct_property.rs deleted file mode 100644 index 8a59413..0000000 --- a/src/properties/struct_property.rs +++ /dev/null @@ -1,625 +0,0 @@ -use std::{ - fmt::Debug, - hash::Hash, - io::{Cursor, Read, Seek, Write}, -}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use indexmap::IndexMap; - -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - custom_version::FUE5ReleaseStreamObjectVersion, - error::{DeserializeError, Error, SerializeError}, - properties::{name_property::NameProperty, struct_types::LinearColor}, - scoped_stack_entry::ScopedStackEntry, - types::{Guid, map::HashableIndexMap}, -}; - -use super::{ - Property, PropertyOptions, PropertyTrait, impl_write, impl_write_header_part, make_matcher, - struct_types::{ - DateTime, IntPoint, QuatD, QuatF, RotatorD, RotatorF, Timespan, Vector2D, Vector2F, - VectorD, VectorF, - }, -}; - -macro_rules! validate { - ($cond:expr, $($arg:tt)+) => {{ - if !$cond { - Err(SerializeError::InvalidValue( - format!($($arg)+).into_boxed_str(), - ))? - } - }}; -} - -/// A property that holds a struct value. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct StructProperty { - /// The unique identifier of the property. - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Guid::is_zero"))] - #[cfg_attr(feature = "serde", serde(default))] - pub guid: Guid, - /// Type name. - pub type_name: String, - /// The value of the property. - #[cfg_attr(feature = "serde", serde(flatten))] - pub value: StructPropertyValue, -} - -/// The possible values of a `StructProperty`. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum StructPropertyValue { - /// A `Vector2F` value. - Vector2F(Vector2F), - /// A `Vector2D` value. - Vector2D(Vector2D), - /// A `VectorF` value. - VectorF(VectorF), - /// A `VectorD` value. - VectorD(VectorD), - /// A `RotatorF` value. - RotatorF(RotatorF), - /// A `RotatorD` value. - RotatorD(RotatorD), - /// A `QuatF` value. - QuatF(QuatF), - /// A `QuatD` value. - QuatD(QuatD), - /// A `DateTime` value. - DateTime(DateTime), - /// A `Timespan` value - Timespan(Timespan), - /// A `Guid` value. - Guid(Guid), - /// A `LinearColor` value. - LinearColor(LinearColor), - /// An `IntPoint` value. - IntPoint(IntPoint), - /// A `GameplayTagContainer` value. - GameplayTagContainer(Vec), - /// A custom struct value. - CustomStruct(HashableIndexMap>), -} - -impl StructProperty { - /// Creates a new `StructProperty` instance. - #[inline] - pub fn new(guid: Guid, type_name: String, value: StructPropertyValue) -> Self { - StructProperty { - guid, - type_name, - value, - } - } - - #[inline] - pub(crate) fn read( - cursor: &mut R, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if include_header { - Self::read_header(cursor, options) - } else { - Err(DeserializeError::invalid_property( - "StructProperty::read() include_header must be true, use read_body() instead", - cursor, - ))? - } - } - - #[inline] - fn read_header( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - let length = cursor.read_u32::()?; - - let array_index = cursor.read_u32::()?; - if array_index != 0 { - let position = cursor.stream_position()? - 4; - Err(DeserializeError::InvalidArrayIndex(array_index, position))? - } - - let type_name = cursor.read_string()?; - - let guid = cursor.read_guid()?; - - let terminator = cursor.read_u8()?; - if terminator != 0 { - let position = cursor.stream_position()? - 1; - Err(DeserializeError::InvalidTerminator(terminator, position))? - } - - let start = cursor.stream_position()?; - let value = Self::read_body(cursor, &type_name, options)?; - let end = cursor.stream_position()?; - if end - start != length as u64 { - Err(DeserializeError::InvalidValueSize( - length as u64, - end - start, - start, - ))? - } - - Ok(StructProperty { - guid, - type_name, - value, - }) - } - - #[inline] - pub(crate) fn read_body( - cursor: &mut R, - type_name: &str, - options: &mut PropertyOptions, - ) -> Result { - let value = match type_name { - "Vector" => StructPropertyValue::read_vector(cursor, options)?, - "Vector2D" => StructPropertyValue::read_vector2(cursor, options)?, - "Rotator" => StructPropertyValue::read_rotator(cursor, options)?, - "Quat" => StructPropertyValue::read_quat(cursor, options)?, - "DateTime" => StructPropertyValue::read_datetime(cursor)?, - "Timespan" => StructPropertyValue::read_timespan(cursor)?, - "LinearColor" => StructPropertyValue::read_linearcolor(cursor)?, - "IntPoint" => StructPropertyValue::read_intpoint(cursor)?, - "Guid" => StructPropertyValue::read_guid(cursor)?, - "GameplayTagContainer" => StructPropertyValue::read_gameplaytagcontainer(cursor)?, - _ => StructPropertyValue::read_custom(cursor, options)?, - }; - Ok(value) - } - - #[inline] - fn get_property_type(&self) -> Result<&str, Error> { - Ok(&self.type_name) - } -} - -fn insert_property(map: &mut IndexMap>, key: String, property: Property) { - let entry = map.entry(key).or_default(); - #[cfg(debug_assertions)] - { - let array_index = match property { - // TODO: Move array_index to the Property layer - Property::NameProperty(NameProperty { array_index, .. }) => array_index, - _ => 0, - }; - let actual_array_index = entry.len() as u32; - // Ensure that the position in the array matches the array_index value, - // otherwise this conversion would cause data loss. - assert_eq!(actual_array_index, array_index); - } - entry.push(property); -} - -impl PropertyTrait for StructProperty { - impl_write!( - StructProperty, - (write_string, fn, get_property_type), - (write_guid, guid) - ); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - self.value.write_body(cursor, options) - } -} - -impl PropertyTrait for StructPropertyValue { - #[inline] - fn write( - &self, - writer: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if !include_header { - return self.write_body(writer, options); - } - Err(SerializeError::invalid_value( - "StructPropertyValue can not be serialized with a header", - ))? - } - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - match self { - StructPropertyValue::Vector2F(vector) => { - validate!( - !options - .supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "Vector2F not supported when LWC is enabled, use Vector2D", - ); - cursor.write_f32::(vector.x.0)?; - cursor.write_f32::(vector.y.0)?; - Ok(8) - } - StructPropertyValue::Vector2D(vector) => { - validate!( - options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "Vector2D not supported when LWC is disabled, use Vector2F", - ); - cursor.write_f64::(vector.x.0)?; - cursor.write_f64::(vector.y.0)?; - Ok(16) - } - StructPropertyValue::VectorF(vector) => { - validate!( - !options - .supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "VectorF not supported when LWC is enabled, use VectorD", - ); - cursor.write_f32::(vector.x.0)?; - cursor.write_f32::(vector.y.0)?; - cursor.write_f32::(vector.z.0)?; - Ok(12) - } - StructPropertyValue::VectorD(vector) => { - validate!( - options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "VectorD not supported when LWC is disabled, use VectorF", - ); - cursor.write_f64::(vector.x.0)?; - cursor.write_f64::(vector.y.0)?; - cursor.write_f64::(vector.z.0)?; - Ok(24) - } - StructPropertyValue::RotatorF(rotator) => { - validate!( - !options - .supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "RotatorF not supported when LWC is enabled, use RotatorD", - ); - cursor.write_f32::(rotator.pitch.0)?; - cursor.write_f32::(rotator.yaw.0)?; - cursor.write_f32::(rotator.roll.0)?; - Ok(12) - } - StructPropertyValue::RotatorD(rotator) => { - validate!( - options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "RotatorD not supported when LWC is disabled, use RotatorF", - ); - cursor.write_f64::(rotator.pitch.0)?; - cursor.write_f64::(rotator.yaw.0)?; - cursor.write_f64::(rotator.roll.0)?; - Ok(24) - } - StructPropertyValue::QuatF(quat) => { - validate!( - !options - .supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "QuatF not supported when LWC is enabled, use QuatD", - ); - cursor.write_f32::(quat.x.0)?; - cursor.write_f32::(quat.y.0)?; - cursor.write_f32::(quat.z.0)?; - cursor.write_f32::(quat.w.0)?; - Ok(16) - } - StructPropertyValue::QuatD(quat) => { - validate!( - options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates), - "QuatD not supported when LWC is disabled, use QuatF", - ); - cursor.write_f64::(quat.x.0)?; - cursor.write_f64::(quat.y.0)?; - cursor.write_f64::(quat.z.0)?; - cursor.write_f64::(quat.w.0)?; - Ok(32) - } - StructPropertyValue::DateTime(date_time) => { - cursor.write_u64::(date_time.ticks)?; - Ok(8) - } - StructPropertyValue::Timespan(date_time) => { - cursor.write_u64::(date_time.ticks)?; - Ok(8) - } - StructPropertyValue::LinearColor(linear_color) => { - cursor.write_f32::(linear_color.r.0)?; - cursor.write_f32::(linear_color.g.0)?; - cursor.write_f32::(linear_color.b.0)?; - cursor.write_f32::(linear_color.a.0)?; - Ok(16) - } - StructPropertyValue::IntPoint(int_point) => { - cursor.write_i32::(int_point.x)?; - cursor.write_i32::(int_point.y)?; - Ok(8) - } - StructPropertyValue::Guid(guid) => { - cursor.write_guid(guid)?; - Ok(16) - } - StructPropertyValue::GameplayTagContainer(tags) => { - let mut len = tags.len(); - cursor.write_i32::(len as i32)?; - for gameplaytag in tags { - len += cursor.write_string(gameplaytag)?; - } - Ok(len) - } - StructPropertyValue::CustomStruct(properties) => { - let mut len = 0; - for (key, values) in properties { - for value in values { - len += cursor.write_string(key)?; - len += value.write(cursor, true, options)?; - } - } - len += cursor.write_string("None")?; - Ok(len) - } - } - } -} - -impl StructPropertyValue { - fn read_custom( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - let mut properties = HashableIndexMap::new(); - loop { - let property_name = cursor.read_string()?; - if property_name == "None" { - break; - } - let property_type = cursor.read_string()?; - let _property_stack_entry = - ScopedStackEntry::new(options.properties_stack, property_name.clone()); - - let property = Property::new(cursor, &property_type, true, options, None)?; - insert_property(&mut properties, property_name, property); - } - Ok(StructPropertyValue::CustomStruct(properties)) - } - - fn read_gameplaytagcontainer(cursor: &mut R) -> Result { - let len = cursor.read_i32::()?; - let mut tags: Vec = Vec::with_capacity(len as usize); - for _ in 0..len { - tags.push(cursor.read_string()?); - } - Ok(Self::GameplayTagContainer(tags)) - } - - fn read_guid(cursor: &mut R) -> Result { - Ok(Self::Guid(cursor.read_guid()?)) - } - - fn read_intpoint(cursor: &mut R) -> Result { - Ok(Self::IntPoint(IntPoint::new( - cursor.read_i32::()?, - cursor.read_i32::()?, - ))) - } - - fn read_linearcolor(cursor: &mut R) -> Result { - Ok(Self::LinearColor(LinearColor::new( - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - ))) - } - - fn read_timespan(cursor: &mut R) -> Result { - Ok(Self::Timespan(Timespan::new( - cursor.read_u64::()?, - ))) - } - - fn read_datetime(cursor: &mut R) -> Result { - Ok(Self::DateTime(DateTime::new( - cursor.read_u64::()?, - ))) - } - - fn read_quat( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - match options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates) { - true => Ok(Self::QuatD(QuatD::new( - cursor.read_f64::()?, - cursor.read_f64::()?, - cursor.read_f64::()?, - cursor.read_f64::()?, - ))), - false => Ok(Self::QuatF(QuatF::new( - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - ))), - } - } - - fn read_rotator( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - match options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates) { - true => Ok(Self::RotatorD(RotatorD::new( - cursor.read_f64::()?, - cursor.read_f64::()?, - cursor.read_f64::()?, - ))), - false => Ok(Self::RotatorF(RotatorF::new( - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - ))), - } - } - - fn read_vector2( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - match options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates) { - true => Ok(Self::Vector2D(Vector2D::new( - cursor.read_f64::()?, - cursor.read_f64::()?, - ))), - false => Ok(Self::Vector2F(Vector2F::new( - cursor.read_f32::()?, - cursor.read_f32::()?, - ))), - } - } - - fn read_vector( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - match options.supports_version(FUE5ReleaseStreamObjectVersion::LargeWorldCoordinates) { - true => Ok(Self::VectorD(VectorD::new( - cursor.read_f64::()?, - cursor.read_f64::()?, - cursor.read_f64::()?, - ))), - false => Ok(Self::VectorF(VectorF::new( - cursor.read_f32::()?, - cursor.read_f32::()?, - cursor.read_f32::()?, - ))), - } - } - make_matcher!(VectorF, get_vector_f, get_vector_f_mut); - make_matcher!(VectorD, get_vector_d, get_vector_d_mut); - make_matcher!(RotatorF, get_rotator_f, get_rotator_f_mut); - make_matcher!(RotatorD, get_rotator_d, get_rotator_d_mut); - make_matcher!(QuatF, get_quat_f, get_quat_f_mut); - make_matcher!(QuatD, get_quat_d, get_quat_d_mut); - make_matcher!(DateTime, get_date_time, get_date_time_mut); - make_matcher!(IntPoint, get_int_point, get_int_point_mut); - make_matcher!(Guid, get_guid, get_guid_mut); - - /// Retrieves the enum value as a `CustomStruct`. - #[inline] - pub fn get_custom_struct(&self) -> Option<&HashableIndexMap>> { - match self { - Self::CustomStruct(properties) => Some(properties), - _ => None, - } - } - - /// Retrieves the mutable enum value as a `CustomStruct`. - #[inline] - pub fn get_custom_struct_mut( - &mut self, - ) -> Option<&mut HashableIndexMap>> { - match self { - Self::CustomStruct(properties) => Some(properties), - _ => None, - } - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(value: Vector2F) -> Self { - StructPropertyValue::Vector2F(value) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(value: Vector2D) -> Self { - StructPropertyValue::Vector2D(value) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(vector: VectorF) -> Self { - StructPropertyValue::VectorF(vector) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(vector: VectorD) -> Self { - StructPropertyValue::VectorD(vector) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(rotator: RotatorF) -> Self { - StructPropertyValue::RotatorF(rotator) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(rotator: RotatorD) -> Self { - StructPropertyValue::RotatorD(rotator) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(quat: QuatF) -> Self { - StructPropertyValue::QuatF(quat) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(quat: QuatD) -> Self { - StructPropertyValue::QuatD(quat) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(date_time: DateTime) -> Self { - StructPropertyValue::DateTime(date_time) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(timespan: Timespan) -> Self { - StructPropertyValue::Timespan(timespan) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(guid: Guid) -> Self { - StructPropertyValue::Guid(guid) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(linear_color: LinearColor) -> Self { - StructPropertyValue::LinearColor(linear_color) - } -} - -impl From for StructPropertyValue { - #[inline] - fn from(int_point: IntPoint) -> Self { - StructPropertyValue::IntPoint(int_point) - } -} diff --git a/src/properties/struct_types.rs b/src/properties/struct_types.rs deleted file mode 100644 index 49f78c8..0000000 --- a/src/properties/struct_types.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::{fmt::Display, hash::Hash}; - -use ordered_float::OrderedFloat; - -macro_rules! unwrap_value { - (f32, $name:ident) => { - $name.0 - }; - (f64, $name:ident) => { - $name.0 - }; - ($type:ident, $name:tt) => { - <$type>::from($name) - }; -} - -macro_rules! wrap_type { - (f32) => { - OrderedFloat - }; - (f64) => { - OrderedFloat - }; - ($type:ident) => { - $type - }; -} - -macro_rules! wrap_value { - (f32, $name:ident) => { - OrderedFloat::from($name) - }; - (f64, $name:ident) => { - OrderedFloat::from($name) - }; - ($type:ty, $name:ident) => { - <$type>::from($name) - }; -} - -pub(crate) use unwrap_value; -pub(crate) use wrap_type; -pub(crate) use wrap_value; - -macro_rules! make_struct { - ( - $name:ident, - $topdoc:expr, - $( - ($field:ident, $type:ident, $doc:expr), - )+ - ) => { - #[doc = $topdoc] - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct $name { - $( - #[doc = $doc] - pub $field: wrap_type!($type), - )+ - } - - impl $name { - #[doc = concat!("Creates a new `", stringify!($name), "` instance.")] - #[inline] - pub fn new( $($field: $type,)+ ) -> Self { - $( - let $field = wrap_value!($type, $field); - )+ - $name { - $($field, )+ - } - } - } - - impl Display for $name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, concat!( - $( - stringify!($field), ": {} ", - )+ - ), $(self.$field, )+) - } - } - }; -} - -make_struct!( - Vector2F, - "A struct that stores a 2D vector.", - (x, f32, "X coordinate."), - (y, f32, "Y coordinate."), -); - -make_struct!( - Vector2D, - "A struct that stores a 2D vector.", - (x, f64, "X coordinate."), - (y, f64, "Y coordinate."), -); - -make_struct!( - VectorF, - "A struct that stores a vector.", - (x, f32, "X coordinate."), - (y, f32, "Y coordinate."), - (z, f32, "Z coordinate."), -); - -make_struct!( - VectorD, - "A struct that stores a vector.", - (x, f64, "X coordinate."), - (y, f64, "Y coordinate."), - (z, f64, "Z coordinate."), -); - -make_struct!( - RotatorF, - "A struct that stores a rotator.", - (pitch, f32, "Euclidean pitch."), - (yaw, f32, "Euclidean yaw."), - (roll, f32, "Euclidean roll."), -); - -make_struct!( - RotatorD, - "A struct that stores a rotator.", - (pitch, f64, "Euclidean pitch."), - (yaw, f64, "Euclidean yaw."), - (roll, f64, "Euclidean roll."), -); - -make_struct!( - QuatF, - "A struct that stores a quaternion.", - (x, f32, "X component."), - (y, f32, "Y component."), - (z, f32, "Z component."), - (w, f32, "Real component."), -); - -make_struct!( - QuatD, - "A struct that stores a quaternion.", - (x, f64, "X component."), - (y, f64, "Y component."), - (z, f64, "Z component."), - (w, f64, "Real component."), -); - -make_struct!( - DateTime, - "A struct that stores a date and time.", - (ticks, u64, "Ticks."), -); - -make_struct!( - Timespan, - "A struct that stores a duration.", - (ticks, u64, "Ticks."), -); - -make_struct!( - LinearColor, - "A structure storing linear color.", - (r, f32, "Red component."), - (g, f32, "Green component."), - (b, f32, "Blue component"), - (a, f32, "Alpha component."), -); - -make_struct!( - IntPoint, - "A struct that stores a 2D integer point.", - (x, i32, "X value."), - (y, i32, "Y value."), -); diff --git a/src/properties/text_property.rs b/src/properties/text_property.rs deleted file mode 100644 index e38b961..0000000 --- a/src/properties/text_property.rs +++ /dev/null @@ -1,966 +0,0 @@ -use std::hash::Hash; -use std::{ - fmt::Debug, - io::{Cursor, Read, Seek, Write}, -}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use ordered_float::OrderedFloat; - -use crate::custom_version::{FEditorObjectVersion, FUE5ReleaseStreamObjectVersion}; -use crate::properties::int_property::UInt64Property; -use crate::properties::struct_types::DateTime; -use crate::types::map::HashableIndexMap; -use crate::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -use super::{PropertyOptions, PropertyTrait, impl_read, impl_read_header, impl_write}; - -/// A property that stores GVAS Text. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct TextProperty { - /// Value - #[cfg_attr(feature = "serde", serde(flatten))] - pub value: FText, -} - -impl TextProperty { - /// Create a new [`TextProperty`] - pub fn new(value: FText) -> Self { - TextProperty { value } - } - - #[inline] - pub(crate) fn read_body( - cursor: &mut R, - options: &mut PropertyOptions, - ) -> Result { - let value = FText::read(cursor, options)?; - Ok(TextProperty { value }) - } - - impl_read!(options); - impl_read_header!(options); -} - -impl PropertyTrait for TextProperty { - impl_write!(TextProperty); - - #[inline] - fn write_body( - &self, - cursor: &mut W, - options: &mut PropertyOptions, - ) -> Result { - let len = self.value.write(cursor, options)?; - Ok(len) - } -} - -/// FText -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FText { - /// Text flags - #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_zero"))] - pub flags: u32, - /// Text history - #[cfg_attr(feature = "serde", serde(flatten))] - pub history: FTextHistory, -} - -#[cfg(feature = "serde")] -#[inline] -fn is_zero(num: &u32) -> bool { - *num == 0 -} - -impl FText { - /// Create a new [`FText`] of none type - pub fn new_none(flags: u32, culture_invariant_string: Option>) -> Self { - FText { - flags, - history: match culture_invariant_string { - Some(culture_invariant_string) => FTextHistory::None { - culture_invariant_string, - }, - None => FTextHistory::Empty {}, - }, - } - } - - /// Create a new [`FText`] of base type - pub fn new_base( - flags: u32, - namespace: Option, - key: Option, - source_string: Option, - ) -> Self { - FText { - flags, - history: FTextHistory::Base { - namespace, - key, - source_string, - }, - } - } - - /// Read [`FText`] from a cursor - #[inline] - pub fn read(cursor: &mut R, options: &PropertyOptions) -> Result { - let flags = cursor.read_u32::()?; - let history = FTextHistory::read(cursor, options)?; - - Ok(FText { flags, history }) - } - - /// Write [`FText`] to a cursor - #[inline] - pub fn write( - &self, - cursor: &mut W, - options: &PropertyOptions, - ) -> Result { - let mut len = 4; - cursor.write_u32::(self.flags)?; - len += self.history.write(cursor, options)?; - Ok(len) - } -} - -/// Text history type -#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] -#[repr(i8)] -pub enum TextHistoryType { - /// None - #[default] - None = -1, - /// Base - Base = 0, - /// Named format - NamedFormat, - /// Ordered format - OrderedFormat, - /// Argument format - ArgumentFormat, - /// As number - AsNumber, - /// As percentage - AsPercent, - /// As currency - AsCurrency, - /// As date - AsDate, - /// As time - AsTime, - /// As datetime - AsDateTime, - /// Transform - Transform, - /// String table entry - StringTableEntry, - /// Text generator - TextGenerator, - /// Uncertain, Back 4 Blood specific serialization - RawText, -} - -/// FText history -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "history"))] -pub enum FTextHistory { - /// Empty - Empty {}, - /// None - None { - /// Culture invariant string - culture_invariant_string: Option, - }, - /// Base text history - Base { - /// Namespace - namespace: Option, - /// Key - key: Option, - /// Source string - source_string: Option, - }, - /// Named format text history - NamedFormat { - /// Source format - source_format: Box, - /// Arguments - arguments: HashableIndexMap, - }, - /// Ordered format text history - OrderedFormat { - /// Source format - source_format: Box, - /// Arguments - arguments: Vec, - }, - /// Argument format text history - ArgumentFormat { - /// Source format - source_format: Box, - /// Arguments - arguments: HashableIndexMap, - }, - /// Convert to number - AsNumber { - /// Source value - source_value: Box, - /// Format options - format_options: Option, - /// Target culture - target_culture: Option, - }, - /// Convert to percentage - AsPercent { - /// Source value - source_value: Box, - /// Format options - format_options: Option, - /// Target culture - target_culture: Option, - }, - /// Convert to currency - AsCurrency { - /// Currency code - currency_code: Option, - /// Source value - source_value: Box, - /// Format options - format_options: Option, - /// Target culture - target_culture: Option, - }, - /// Convert to date - AsDate { - /// Date time - date_time: DateTime, - /// Date style - date_style: DateTimeStyle, - // todo: FTEXT_HISTORY_DATE_TIMEZONE support (needs object version) - /// Target culture - target_culture: String, - }, - /// Convert to time - AsTime { - /// Source date time - source_date_time: DateTime, - /// Time style - time_style: DateTimeStyle, - /// Time zone - time_zone: String, - /// Target culture - target_culture: String, - }, - /// Convert to date time - AsDateTime { - /// Source date time - source_date_time: DateTime, - /// Date style - date_style: DateTimeStyle, - /// Time style - time_style: DateTimeStyle, - /// Time zone - time_zone: String, - /// Target culture - target_culture: String, - }, - /// Transform text - Transform { - /// Source text - source_text: Box, - /// Transform type - #[cfg_attr(feature = "serde", serde(flatten))] - transform_type: TransformType, - }, - /// String table entry - StringTableEntry { - /// Table id - table_id: String, - /// Key - key: String, - }, -} - -impl FTextHistory { - /// Read [`FTextHistory`] from a cursor - #[inline] - pub fn read(cursor: &mut R, options: &PropertyOptions) -> Result { - let history_type = cursor.read_enum()?; - - Ok(match history_type { - TextHistoryType::None => { - if options.supports_version( - FEditorObjectVersion::CultureInvariantTextSerializationKeyStability, - ) { - let has_culture_invariant_string = cursor.read_b32()?; - if has_culture_invariant_string { - let culture_invariant_string = cursor.read_fstring()?; - FTextHistory::None { - culture_invariant_string, - } - } else { - FTextHistory::Empty {} - } - } else { - FTextHistory::Empty {} - } - } - TextHistoryType::Base => { - let namespace = cursor.read_fstring()?; - let key = cursor.read_fstring()?; - let source_string = cursor.read_fstring()?; - - FTextHistory::Base { - namespace, - key, - source_string, - } - } - TextHistoryType::NamedFormat => { - let source_format = Box::new(FText::read(cursor, options)?); - - let argument_count = cursor.read_i32::()?; - let mut arguments = HashableIndexMap::with_capacity(argument_count as usize); - - for _ in 0..argument_count { - let key = cursor.read_string()?; - let value = FormatArgumentValue::read(cursor, options)?; - arguments.insert(key, value); - } - - FTextHistory::NamedFormat { - source_format, - arguments, - } - } - TextHistoryType::OrderedFormat => { - let source_format = Box::new(FText::read(cursor, options)?); - - let count = cursor.read_i32::()?; - let mut arguments = Vec::with_capacity(count as usize); - - for _ in 0..count { - arguments.push(FormatArgumentValue::read(cursor, options)?); - } - - FTextHistory::OrderedFormat { - source_format, - arguments, - } - } - TextHistoryType::ArgumentFormat => { - let source_format = Box::new(FText::read(cursor, options)?); - let count = cursor.read_i32::()?; - let mut arguments = HashableIndexMap::with_capacity(count as usize); - - for _ in 0..count { - let key = cursor.read_string()?; - let value = FormatArgumentValue::read(cursor, options)?; - arguments.insert(key, value); - } - - FTextHistory::ArgumentFormat { - source_format, - arguments, - } - } - TextHistoryType::AsNumber => { - let source_value = Box::new(FormatArgumentValue::read(cursor, options)?); - - let has_format_options = cursor.read_b32()?; - let format_options = if has_format_options { - Some(NumberFormattingOptions::read(cursor)?) - } else { - None - }; - - let target_culture = cursor.read_fstring()?; - - FTextHistory::AsNumber { - source_value, - format_options, - target_culture, - } - } - TextHistoryType::AsPercent => { - let source_value = Box::new(FormatArgumentValue::read(cursor, options)?); - - let has_format_options = cursor.read_b32()?; - let format_options = if has_format_options { - Some(NumberFormattingOptions::read(cursor)?) - } else { - None - }; - - let target_culture = cursor.read_fstring()?; - - FTextHistory::AsPercent { - source_value, - format_options, - target_culture, - } - } - TextHistoryType::AsCurrency => { - let currency_code = cursor.read_fstring()?; - - let source_value = Box::new(FormatArgumentValue::read(cursor, options)?); - - let has_format_options = cursor.read_b32()?; - let format_options = if has_format_options { - Some(NumberFormattingOptions::read(cursor)?) - } else { - None - }; - - let target_culture = cursor.read_fstring()?; - - FTextHistory::AsCurrency { - currency_code, - source_value, - format_options, - target_culture, - } - } - TextHistoryType::AsDate => { - let date_time = DateTime { - ticks: UInt64Property::read(cursor, false)?.value, - }; - let date_style = cursor.read_enum()?; - let target_culture = cursor.read_string()?; - - FTextHistory::AsDate { - date_time, - date_style, - target_culture, - } - } - TextHistoryType::AsTime => { - let source_date_time = DateTime { - ticks: UInt64Property::read(cursor, false)?.value, - }; - let time_style = cursor.read_enum()?; - let time_zone = cursor.read_string()?; - let target_culture = cursor.read_string()?; - - FTextHistory::AsTime { - source_date_time, - time_style, - time_zone, - target_culture, - } - } - TextHistoryType::AsDateTime => { - let source_date_time = DateTime { - ticks: UInt64Property::read(cursor, false)?.value, - }; - let date_style = cursor.read_enum()?; - let time_style = cursor.read_enum()?; - let time_zone = cursor.read_string()?; - let target_culture = cursor.read_string()?; - - FTextHistory::AsDateTime { - source_date_time, - date_style, - time_style, - time_zone, - target_culture, - } - } - TextHistoryType::Transform => { - let source_text = Box::new(FText::read(cursor, options)?); - let transform_type = cursor.read_enum()?; - - FTextHistory::Transform { - source_text, - transform_type, - } - } - TextHistoryType::StringTableEntry => { - let table_id = cursor.read_string()?; - let key = cursor.read_string()?; - - FTextHistory::StringTableEntry { table_id, key } - } - _ => unimplemented!("unimplemented history type: {:?}", history_type), - }) - } - - /// Write [`FTextHistory`] to a cursor - #[inline] - pub fn write( - &self, - cursor: &mut W, - options: &PropertyOptions, - ) -> Result { - match self { - FTextHistory::Empty {} => { - let mut len = 1; - cursor.write_enum(TextHistoryType::None)?; - if options.supports_version( - FEditorObjectVersion::CultureInvariantTextSerializationKeyStability, - ) { - len += 4; - cursor.write_b32(false)?; - } - Ok(len) - } - - FTextHistory::None { - culture_invariant_string, - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::None)?; - if options.supports_version( - FEditorObjectVersion::CultureInvariantTextSerializationKeyStability, - ) { - len += 4; - cursor.write_b32(true)?; - len += cursor.write_fstring(culture_invariant_string.as_deref())?; - } - Ok(len) - } - - FTextHistory::Base { - namespace, - key, - source_string, - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::Base)?; - len += cursor.write_fstring(namespace.as_deref())?; - len += cursor.write_fstring(key.as_deref())?; - len += cursor.write_fstring(source_string.as_deref())?; - Ok(len) - } - - FTextHistory::NamedFormat { - source_format, - arguments: HashableIndexMap(arguments), - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::NamedFormat)?; - len += source_format.write(cursor, options)?; - len += 4; - cursor.write_i32::(arguments.len() as i32)?; - for (key, value) in arguments { - len += cursor.write_string(key)?; - len += value.write(cursor, options)?; - } - Ok(len) - } - - FTextHistory::OrderedFormat { - source_format, - arguments, - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::OrderedFormat)?; - len += source_format.write(cursor, options)?; - len += 4; - cursor.write_i32::(arguments.len() as i32)?; - for argument in arguments { - len += argument.write(cursor, options)?; - } - Ok(len) - } - - FTextHistory::ArgumentFormat { - source_format, - arguments: HashableIndexMap(arguments), - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::ArgumentFormat)?; - len += source_format.write(cursor, options)?; - len += 4; - cursor.write_i32::(arguments.len() as i32)?; - for (key, value) in arguments { - len += cursor.write_string(key)?; - len += value.write(cursor, options)?; - } - Ok(len) - } - - FTextHistory::AsNumber { - source_value, - format_options, - target_culture, - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::AsNumber)?; - len += source_value.write(cursor, options)?; - len += 4; - cursor.write_b32(format_options.is_some())?; - if let Some(format_options) = format_options { - len += format_options.write(cursor)?; - }; - len += cursor.write_fstring(target_culture.as_deref())?; - Ok(len) - } - - FTextHistory::AsPercent { - source_value, - format_options, - target_culture, - } => { - let mut len = 1; - cursor.write_enum(TextHistoryType::AsPercent)?; - len += source_value.write(cursor, options)?; - len += 4; - cursor.write_b32(format_options.is_some())?; - if let Some(format_options) = format_options { - len += format_options.write(cursor)?; - } - len += cursor.write_fstring(target_culture.as_deref())?; - Ok(len) - } - - FTextHistory::AsCurrency { - currency_code, - source_value, - format_options, - target_culture, - } => { - cursor.write_enum(TextHistoryType::AsCurrency)?; - let mut len = 1; - len += cursor.write_fstring(currency_code.as_deref())?; - len += source_value.write(cursor, options)?; - len += 4; - cursor.write_b32(format_options.is_some())?; - if let Some(format_options) = format_options { - len += format_options.write(cursor)?; - } - len += cursor.write_fstring(target_culture.as_deref())?; - Ok(len) - } - - FTextHistory::AsDate { - date_time, - date_style, - target_culture, - } => { - cursor.write_enum(TextHistoryType::AsDate)?; - cursor.write_u64::(date_time.ticks)?; - cursor.write_enum(*date_style)?; - let mut len = 10; - len += cursor.write_string(target_culture)?; - Ok(len) - } - - FTextHistory::AsTime { - source_date_time, - time_style, - time_zone, - target_culture, - } => { - cursor.write_enum(TextHistoryType::AsTime)?; - cursor.write_u64::(source_date_time.ticks)?; - cursor.write_enum(*time_style)?; - let mut len = 10; - len += cursor.write_string(time_zone)?; - len += cursor.write_string(target_culture)?; - Ok(len) - } - - FTextHistory::AsDateTime { - source_date_time, - date_style, - time_style, - time_zone, - target_culture, - } => { - cursor.write_enum(TextHistoryType::AsDateTime)?; - cursor.write_u64::(source_date_time.ticks)?; - cursor.write_enum(*date_style)?; - cursor.write_enum(*time_style)?; - let mut len = 11; - len += cursor.write_string(time_zone.as_str())?; - len += cursor.write_string(target_culture.as_str())?; - Ok(len) - } - - FTextHistory::Transform { - source_text, - transform_type, - } => { - cursor.write_enum(TextHistoryType::Transform)?; - let mut len = 2; - len += source_text.write(cursor, options)?; - cursor.write_enum(*transform_type)?; - Ok(len) - } - - FTextHistory::StringTableEntry { table_id, key } => { - cursor.write_enum(TextHistoryType::StringTableEntry)?; - let mut len = 1; - len += cursor.write_string(table_id)?; - len += cursor.write_string(key)?; - Ok(len) - } - } - } -} - -/// Format argument type -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] -#[repr(i8)] -pub enum FormatArgumentType { - /// Integer (32 bit in most games, 64 bit in Hogwarts Legacy) - Int, - /// Unsigned integer (32 bit) - UInt, - /// Floating point number (32 bit) - Float, - /// Floating point number (64 bit) - Double, - /// FText - Text, - /// ? - Gender, -} - -/// Format argument value -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum FormatArgumentValue { - /// Integer - Int(i32), - /// Unsigned integer - UInt(u32), - /// Float - Float(OrderedFloat), - /// Double - Double(OrderedFloat), - /// FText - Text(FText), - /// 64-bit integer - Int64(i64), - /// 64-bit unsigned integer - UInt64(u64), -} - -impl FormatArgumentValue { - /// Read [`FormatArgumentValue`] from a cursor - #[inline] - pub(crate) fn read( - cursor: &mut R, - options: &PropertyOptions, - ) -> Result { - let format_argument_type = cursor.read_enum()?; - - Ok(match format_argument_type { - FormatArgumentType::Int => match options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ) { - true => FormatArgumentValue::Int64(cursor.read_i64::()?), - false => FormatArgumentValue::Int(cursor.read_i32::()?), - }, - FormatArgumentType::UInt => match options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ) { - true => FormatArgumentValue::UInt64(cursor.read_u64::()?), - false => FormatArgumentValue::UInt(cursor.read_u32::()?), - }, - FormatArgumentType::Float => { - FormatArgumentValue::Float(cursor.read_f32::()?.into()) - } - FormatArgumentType::Double => { - FormatArgumentValue::Double(cursor.read_f64::()?.into()) - } - FormatArgumentType::Text => FormatArgumentValue::Text(FText::read(cursor, options)?), - FormatArgumentType::Gender => unimplemented!(), - }) - } - - /// Write [`FormatArgumentValue`] to a cursor - #[inline] - pub fn write( - &self, - cursor: &mut W, - options: &PropertyOptions, - ) -> Result { - match self { - FormatArgumentValue::Int(value) => { - assert!( - !options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ), - "FormatArgumentValue::Int is not compatible with TextFormatArgumentData64bitSupport" - ); - cursor.write_enum(FormatArgumentType::Int)?; - cursor.write_i32::(*value)?; - Ok(5) - } - FormatArgumentValue::Int64(value) => { - assert!( - options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ), - "FormatArgumentValue::Int64 requires TextFormatArgumentData64bitSupport" - ); - cursor.write_enum(FormatArgumentType::Int)?; - cursor.write_i64::(*value)?; - Ok(9) - } - FormatArgumentValue::UInt(value) => { - assert!( - !options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ), - "FormatArgumentValue::UInt is not compatible with TextFormatArgumentData64bitSupport" - ); - cursor.write_enum(FormatArgumentType::UInt)?; - cursor.write_u32::(*value)?; - Ok(5) - } - FormatArgumentValue::UInt64(value) => { - assert!( - options.supports_version( - FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport, - ), - "FormatArgumentValue::UInt64 requires TextFormatArgumentData64bitSupport" - ); - cursor.write_enum(FormatArgumentType::UInt)?; - cursor.write_u64::(*value)?; - Ok(9) - } - FormatArgumentValue::Float(value) => { - cursor.write_enum(FormatArgumentType::Float)?; - cursor.write_f32::(value.0)?; - Ok(5) - } - FormatArgumentValue::Double(value) => { - cursor.write_enum(FormatArgumentType::Double)?; - cursor.write_f64::(value.0)?; - Ok(9) - } - FormatArgumentValue::Text(value) => { - let mut len = 1; - cursor.write_enum(FormatArgumentType::Text)?; - len += value.write(cursor, options)?; - Ok(len) - } - } - } -} - -/// Rounding mode -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "rounding"))] -#[repr(i8)] -pub enum RoundingMode { - /// Rounds to the nearest place, equidistant ties go to the value which is closest to an even value: 1.5 becomes 2, 0.5 becomes 0 - HalfToEven, - /// Rounds to nearest place, equidistant ties go to the value which is further from zero: -0.5 becomes -1.0, 0.5 becomes 1.0 - HalfFromZero, - /// Rounds to nearest place, equidistant ties go to the value which is closer to zero: -0.5 becomes 0, 0.5 becomes 0. - HalfToZero, - /// Rounds to the value which is further from zero, "larger" in absolute value: 0.1 becomes 1, -0.1 becomes -1 - FromZero, - /// Rounds to the value which is closer to zero, "smaller" in absolute value: 0.1 becomes 0, -0.1 becomes 0 - ToZero, - /// Rounds to the value which is more negative: 0.1 becomes 0, -0.1 becomes -1 - ToNegativeInfinity, - /// Rounds to the value which is more positive: 0.1 becomes 1, -0.1 becomes 0 - ToPositiveInfinity, -} - -/// Number formatting options -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct NumberFormattingOptions { - /// Always include sign - pub always_include_sign: bool, - /// Use grouping - pub use_grouping: bool, - /// Rounding mode - #[cfg_attr(feature = "serde", serde(flatten))] - pub rounding_mode: RoundingMode, - /// Minimum integral digits - pub minimum_integral_digits: i32, - /// Maximum integral digits - pub maximum_integral_digits: i32, - /// Minimum fractional digits - pub minimum_fractional_digits: i32, - /// Maximum fractional digits - pub maximum_fractional_digits: i32, -} - -impl NumberFormattingOptions { - /// Read [`NumberFormattingOptions`] from a cursor - #[inline] - pub fn read(cursor: &mut R) -> Result { - let always_include_sign = cursor.read_b32()?; - let use_grouping = cursor.read_b32()?; - let rounding_mode = cursor.read_enum()?; - let minimum_integral_digits = cursor.read_i32::()?; - let maximum_integral_digits = cursor.read_i32::()?; - let minimum_fractional_digits = cursor.read_i32::()?; - let maximum_fractional_digits = cursor.read_i32::()?; - - Ok(NumberFormattingOptions { - always_include_sign, - use_grouping, - rounding_mode, - minimum_integral_digits, - maximum_integral_digits, - minimum_fractional_digits, - maximum_fractional_digits, - }) - } - - /// Write [`NumberFormattingOptions`] to a cursor - #[inline] - pub fn write(&self, cursor: &mut W) -> Result { - cursor.write_b32(self.always_include_sign)?; - cursor.write_b32(self.use_grouping)?; - cursor.write_enum(self.rounding_mode)?; - cursor.write_i32::(self.minimum_integral_digits)?; - cursor.write_i32::(self.maximum_integral_digits)?; - cursor.write_i32::(self.minimum_fractional_digits)?; - cursor.write_i32::(self.maximum_fractional_digits)?; - - Ok(25) - } -} - -/// Date time style -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[repr(i8)] -pub enum DateTimeStyle { - /// Default - Default, - /// Short - Short, - /// Medium - Medium, - /// Long - Long, - /// Full - Full, -} - -/// Transform type -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "transform"))] -#[repr(i8)] -pub enum TransformType { - /// To lowercase - ToLower = 0, - /// To uppercase - ToUpper, -} diff --git a/src/properties/unknown_property.rs b/src/properties/unknown_property.rs deleted file mode 100644 index e7126b7..0000000 --- a/src/properties/unknown_property.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::io::{Cursor, Read, Seek, Write}; - -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use crate::{cursor_ext::WriteExt, error::Error}; - -use super::{PropertyOptions, PropertyTrait}; - -/// This struct is read when a property is unknown to the deserializer -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct UnknownProperty { - property_name: String, - raw: Vec, -} - -impl UnknownProperty { - /// Creates a new `UnknownProperty` instance. - #[inline] - pub fn new(property_name: String, raw: Vec) -> Self { - UnknownProperty { property_name, raw } - } - - #[inline] - pub(crate) fn read_with_length( - cursor: &mut R, - property_name: String, - length: u32, - ) -> Result { - let mut data = vec![0u8; length as usize]; - cursor.read_exact(&mut data)?; - - Ok(UnknownProperty { - property_name, - raw: data, - }) - } - - #[inline] - pub(crate) fn read_with_header( - cursor: &mut R, - property_name: String, - ) -> Result { - let length = cursor.read_u32::()?; - let array_index = cursor.read_u32::()?; - assert_eq!( - array_index, - 0, - "Expected array_index value zero @ {:#x}", - cursor.stream_position()? - 4 - ); - let separator = cursor.read_u8()?; - assert_eq!(separator, 0); - - UnknownProperty::read_with_length(cursor, property_name, length) - } -} - -impl PropertyTrait for UnknownProperty { - #[inline] - fn write( - &self, - cursor: &mut W, - include_header: bool, - options: &mut PropertyOptions, - ) -> Result { - if !include_header { - return self.write_body(cursor, options); - } - - let buf = &mut Cursor::new(Vec::new()); - let body_len = self.write_body(buf, options)?; - let buf = buf.get_ref(); - - let name_len = cursor.write_string(&self.property_name)?; - cursor.write_u32::(buf.len() as u32)?; - cursor.write_u32::(0)?; - cursor.write_u8(0)?; - cursor.write_all(buf)?; - - Ok(9 + name_len + body_len) - } - - #[inline] - fn write_body( - &self, - cursor: &mut W, - _: &mut PropertyOptions, - ) -> Result { - cursor.write_all(&self.raw)?; - - Ok(self.raw.len()) - } -} diff --git a/src/savegame_version.rs b/src/savegame_version.rs deleted file mode 100644 index 49592b7..0000000 --- a/src/savegame_version.rs +++ /dev/null @@ -1,13 +0,0 @@ -use num_enum::IntoPrimitive; - -/// Save Game File Version from FSaveGameFileVersion::Type -#[derive(IntoPrimitive)] -#[repr(u32)] -pub enum SaveGameVersion { - /// Initial version. - InitialVersion = 1, - /// serializing custom versions into the savegame data to handle that type of versioning - AddedCustomVersions = 2, - /// added a new UE5 version number to FPackageFileSummary - PackageFileSummaryVersionChange = 3, -} diff --git a/src/scoped_stack_entry.rs b/src/scoped_stack_entry.rs deleted file mode 100644 index 414317d..0000000 --- a/src/scoped_stack_entry.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub(crate) struct ScopedStackEntry { - stack: *mut Vec, -} - -/// This struct adds an entry to a Vec on creation and removes an entry when going out of scope -impl ScopedStackEntry { - /// Create new instance of ScopedStackEntry - pub(crate) fn new(stack: &mut Vec, value: T) -> Self { - stack.push(value); - Self { - stack: stack as *mut Vec, - } - } -} - -impl Drop for ScopedStackEntry { - fn drop(&mut self) { - if let Some(stack) = unsafe { self.stack.as_mut() } { - stack.pop(); - } - } -} diff --git a/src/test/common/delegate.rs b/src/test/common/delegate.rs new file mode 100644 index 0000000..1954032 --- /dev/null +++ b/src/test/common/delegate.rs @@ -0,0 +1,251 @@ +use crate::types::{ + FCustomVersion, FCustomVersionContainer, FDelegateProperty, FEngineVersion, FGuid, + FMulticastInlineDelegateProperty, FMulticastSparseDelegateProperty, FPackageFileVersion, + FProperty, FSaveGameHeader, FString, TArray, TaggedProperties, TaggedProperty, USaveGame, +}; + +const DELEGATE_STR: &str = + "/Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.Saver1_Blueprint_2"; + +pub(crate) fn expected() -> USaveGame { + USaveGame { + header: FSaveGameHeader { + package_file_version: FPackageFileVersion::UE4 { file_version: 517 }, + engine_version: FEngineVersion { + major: 4, + minor: 23, + patch: 1, + change_list: 9631420, + branch: FString::from("++UE4+Release-4.23"), + }, + custom_versions: Some(FCustomVersionContainer { + custom_versions: TArray::from([ + FCustomVersion { + key: FGuid::from_u32(0x9C54D522, 0xA8264FBE, 0x94210746, 0x61B482D0), + value: 23, + }, + FCustomVersion { + key: FGuid::from_u32(0xB0D832E4, 0x1F894F0D, 0xACCF7EB7, 0x36FD4AA2), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE1C64328, 0xA22C4D53, 0xA36C8E86, 0x6417BD8C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x375EC13C, 0x06E448FB, 0xB50084F0, 0x262A717E), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + value: 34, + }, + FCustomVersion { + key: FGuid::from_u32(0xCFFC743F, 0x43B04480, 0x939114DF, 0x171D2073), + value: 35, + }, + FCustomVersion { + key: FGuid::from_u32(0xB02B49B5, 0xBB2044E9, 0xA30432B7, 0x52E40360), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4E4105C, 0x59A149B5, 0xA7C540C4, 0x547EDFEE), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x39C831C9, 0x5AE647DC, 0x9A449C17, 0x3E1C8E7C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x78F01B33, 0xEBEA4F98, 0xB9B484EA, 0xCCB95AA2), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x6631380F, 0x2D4D43E0, 0x8009CF27, 0x6956A95A), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x12F88B9F, 0x88754AFC, 0xA67CD90C, 0x383ABD29), + value: 31, + }, + FCustomVersion { + key: FGuid::from_u32(0x7B5AE74C, 0xD2704C10, 0xA9585798, 0x0B212A5A), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0xD7296918, 0x1DD64BDD, 0x9DE264A8, 0x3CC13884), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xC2A15278, 0xBFE74AFE, 0x6C1790FF, 0x531DF755), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6EACA3D4, 0x40EC4CC1, 0xB7868BED, 0x09428FC5), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x29E575DD, 0xE0A34627, 0x9D10D276, 0x232CDCEA), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAF43A65D, 0x7FD34947, 0x98733E8E, 0xD9C1BB05), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x6B266CEC, 0x1EC74B8F, 0xA30BE4D9, 0x0942FC07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x0DF73D61, 0xA23F47EA, 0xB72789E9, 0x0C41499A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x601D1886, 0xAC644F84, 0xAA16D3DE, 0x0DEAC7D6), + value: 27, + }, + FCustomVersion { + key: FGuid::from_u32(0x9DFFBCD6, 0x494F0158, 0xE2211282, 0x3C92A888), + value: 6, + }, + FCustomVersion { + key: FGuid::from_u32(0xF2AED0AC, 0x9AFE416F, 0x8664AA7F, 0xFA26D6FC), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x174F1F0B, 0xB4C645A5, 0xB13F2EE8, 0xD0FB917D), + value: 9, + }, + FCustomVersion { + key: FGuid::from_u32(0x717F9EE7, 0xE9B0493A, 0x88B39132, 0x1B388107), + value: 6, + }, + FCustomVersion { + key: FGuid::from_u32(0x8E7DDCB3, 0x80DA47BB, 0x9FD346A2, 0x93984DF6), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xCB8AB0CD, 0xE78C4BDE, 0xA8621393, 0x14E9EF62), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB680AF2, 0x59EF4BA3, 0xBAA819B5, 0x73C8443D), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xAFE08691, 0x3A0D4952, 0xB673673B, 0x7CF22D1E), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x2EB5FDBD, 0x01AC4D10, 0x8136F38F, 0x3393A5DA), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x509D354F, 0xF6E6492F, 0xA74985B2, 0x073C631C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xA462B7EA, 0xF4994E3A, 0x99C1EC1F, 0x8224E1B2), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x430C4D19, 0x71544970, 0x87699B69, 0xDF90B0E5), + value: 13, + }, + FCustomVersion { + key: FGuid::from_u32(0xAAFE32BD, 0x53954C14, 0xB66A5E25, 0x1032D1DD), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x23AFE18E, 0x4CE14E58, 0x8D61C252, 0xB953BEB7), + value: 8, + }, + FCustomVersion { + key: FGuid::from_u32(0x4A56EB40, 0x10F511DC, 0x92D3347E, 0xB2C96AE7), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xD78A4A00, 0xE8584697, 0xBAA819B5, 0x487D46B4), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x5579F886, 0x933A4C1F, 0x83BA087B, 0x6361B92F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x612FBE52, 0xDA53400B, 0x910D4F91, 0x9FB1857C), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4237A36, 0xCAEA41C9, 0x8FA218F8, 0x58681BF3), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x804E3F75, 0x70884B49, 0xA4D68C06, 0x3C7EB6DC), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x11310AED, 0x2E554D61, 0xAF679AA3, 0xC5A1082C), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAB965196, 0x45D808FC, 0xB7D7228D, 0x78AD569E), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x24BB7AF3, 0x56464F83, 0x1F2F2DC2, 0x49AD96FF), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB26E412, 0x1F154B4D, 0x9372550A, 0x961D2F70), + value: 3, + }, + ]), + }), + save_game_class_name: FString::from("/Script/SaveFileTest.TestSaveGame"), + }, + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("DynamicDelegate"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FDelegateProperty { + object: FString::from(DELEGATE_STR), + function_name: FString::from("FirstBinding"), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("MulticastDelegate"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FMulticastInlineDelegateProperty(TArray::from([ + FDelegateProperty { + object: FString::from(DELEGATE_STR), + function_name: FString::from("FirstBinding"), + }, + FDelegateProperty { + object: FString::from(DELEGATE_STR), + function_name: FString::from("SecondBinding"), + }, + ]))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("MulticastSparseDelegate"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FMulticastSparseDelegateProperty(TArray::from([ + FDelegateProperty { + object: FString::from(DELEGATE_STR), + function_name: FString::from("FirstBinding"), + }, + ]))), + property_guid: FGuid::default(), + }, + ]), + } +} diff --git a/tests/common/features.rs b/src/test/common/features.rs similarity index 100% rename from tests/common/features.rs rename to src/test/common/features.rs diff --git a/tests/common/mod.rs b/src/test/common/mod.rs similarity index 68% rename from tests/common/mod.rs rename to src/test/common/mod.rs index 6a2742f..89f07bc 100644 --- a/tests/common/mod.rs +++ b/src/test/common/mod.rs @@ -12,10 +12,12 @@ pub mod tagcontainer; pub mod vector2d; pub const ASSERT_FAILED_PATH: &str = "resources/test/assert_failed.sav"; +pub const COMPLETE_PROPERTY_TAG_PATH: &str = "resources/test/complete_property_tag.sav"; pub const COMPONENT8_PATH: &str = "resources/test/component8.sav"; pub const DELEGATE_PATH: &str = "resources/test/Delegate.sav"; pub const ENUM_ARRAY_PATH: &str = "resources/test/enum_array.sav"; pub const FEATURES_01_PATH: &str = "resources/test/features_01.bin"; +pub const MEDIEVAL_DYNASTY_PATH: &str = "resources/test/medieval_dynasty.sav"; pub const OPTIONS_PATH: &str = "resources/test/Options.sav"; pub const PACKAGE_VERSION_524_PATH: &str = "resources/test/package_version_524.sav"; pub const PACKAGE_VERSION_525_PATH: &str = "resources/test/package_version_525.sav"; @@ -29,7 +31,34 @@ pub const SLOT1_PATH: &str = "resources/test/Slot1.sav"; pub const SLOT2_PATH: &str = "resources/test/Slot2.sav"; pub const SLOT3_PATH: &str = "resources/test/Slot3.sav"; pub const STRING_TABLE_ENTRY: &str = "resources/test/string_table_entry.sav"; +pub const TAGCONTAINER_PATH: &str = "resources/test/tagcontainer.sav"; pub const TEXT_PROPERTY_NOARRAY: &str = "resources/test/text_property_noarray.bin"; pub const TRANSFORM_PATH: &str = "resources/test/transform.sav"; pub const VECTOR2D_PATH: &str = "resources/test/vector2d.sav"; -pub const TAGCONTAINER_PATH: &str = "resources/test/tagcontainer.sav"; + +pub const ALL_TEST_PATHS: [&str; 22] = [ + ASSERT_FAILED_PATH, + COMPLETE_PROPERTY_TAG_PATH, + COMPONENT8_PATH, + DELEGATE_PATH, + ENUM_ARRAY_PATH, + FEATURES_01_PATH, + MEDIEVAL_DYNASTY_PATH, + OPTIONS_PATH, + PACKAGE_VERSION_524_PATH, + PACKAGE_VERSION_525_PATH, + PROFILE_0_PATH, + REGRESSION_01_PATH, + RO_64BIT_FAV_PATH, + SAVESLOT_03_PATH, + SLOT1_PATH, + SLOT2_PATH, + SLOT3_PATH, + STRING_TABLE_ENTRY, + TAGCONTAINER_PATH, + TEXT_PROPERTY_NOARRAY, + TRANSFORM_PATH, + VECTOR2D_PATH, +]; + +pub const PALWORLD_PATHS: [&str; 2] = [PALWORLD_ZLIB_PATH, PALWORLD_ZLIB_TWICE_PATH]; diff --git a/src/test/common/options.rs b/src/test/common/options.rs new file mode 100644 index 0000000..de8618e --- /dev/null +++ b/src/test/common/options.rs @@ -0,0 +1,244 @@ +use crate::types::{ + FCustomVersion, FCustomVersionContainer, FEngineVersion, FFloatProperty, FGuid, + FPackageFileVersion, FProperty, FSaveGameHeader, FString, TArray, TaggedProperties, + TaggedProperty, USaveGame, +}; + +pub(crate) fn expected() -> USaveGame { + USaveGame { + header: FSaveGameHeader { + package_file_version: FPackageFileVersion::UE4 { file_version: 518 }, + engine_version: FEngineVersion { + major: 4, + minor: 25, + patch: 3, + change_list: 13942748, + branch: FString::from("++UE4+Release-4.25"), + }, + custom_versions: Some(FCustomVersionContainer { + // custom_version_format: 3, + custom_versions: TArray::from([ + FCustomVersion { + key: FGuid::from_u32(0x11310AED, 0x2E554D61, 0xAF679AA3, 0xC5A1082C), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x24BB7AF3, 0x56464F83, 0x1F2F2DC2, 0x49AD96FF), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x76A52329, 0x092345B5, 0x98AED841, 0xCF2F6AD8), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x5FBC6907, 0x55C840AE, 0x8E67F184, 0x5EFFF13F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB26E412, 0x1F154B4D, 0x9372550A, 0x961D2F70), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xFCF57AFA, 0x50764283, 0xB9A9E658, 0xFFA02D32), + value: 61, + }, + FCustomVersion { + key: FGuid::from_u32(0x9C54D522, 0xA8264FBE, 0x94210746, 0x61B482D0), + value: 30, + }, + FCustomVersion { + key: FGuid::from_u32(0xB0D832E4, 0x1F894F0D, 0xACCF7EB7, 0x36FD4AA2), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE1C64328, 0xA22C4D53, 0xA36C8E86, 0x6417BD8C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x375EC13C, 0x06E448FB, 0xB50084F0, 0x262A717E), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + value: 38, + }, + FCustomVersion { + key: FGuid::from_u32(0xCFFC743F, 0x43B04480, 0x939114DF, 0x171D2073), + value: 37, + }, + FCustomVersion { + key: FGuid::from_u32(0xB02B49B5, 0xBB2044E9, 0xA30432B7, 0x52E40360), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4E4105C, 0x59A149B5, 0xA7C540C4, 0x547EDFEE), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x39C831C9, 0x5AE647DC, 0x9A449C17, 0x3E1C8E7C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x78F01B33, 0xEBEA4F98, 0xB9B484EA, 0xCCB95AA2), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x6631380F, 0x2D4D43E0, 0x8009CF27, 0x6956A95A), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x12F88B9F, 0x88754AFC, 0xA67CD90C, 0x383ABD29), + value: 43, + }, + FCustomVersion { + key: FGuid::from_u32(0x7B5AE74C, 0xD2704C10, 0xA9585798, 0x0B212A5A), + value: 12, + }, + FCustomVersion { + key: FGuid::from_u32(0xD7296918, 0x1DD64BDD, 0x9DE264A8, 0x3CC13884), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xC2A15278, 0xBFE74AFE, 0x6C1790FF, 0x531DF755), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6EACA3D4, 0x40EC4CC1, 0xB7868BED, 0x09428FC5), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x29E575DD, 0xE0A34627, 0x9D10D276, 0x232CDCEA), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAF43A65D, 0x7FD34947, 0x98733E8E, 0xD9C1BB05), + value: 7, + }, + FCustomVersion { + key: FGuid::from_u32(0x6B266CEC, 0x1EC74B8F, 0xA30BE4D9, 0x0942FC07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x0DF73D61, 0xA23F47EA, 0xB72789E9, 0x0C41499A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x601D1886, 0xAC644F84, 0xAA16D3DE, 0x0DEAC7D6), + value: 31, + }, + FCustomVersion { + key: FGuid::from_u32(0x9DFFBCD6, 0x494F0158, 0xE2211282, 0x3C92A888), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xF2AED0AC, 0x9AFE416F, 0x8664AA7F, 0xFA26D6FC), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x174F1F0B, 0xB4C645A5, 0xB13F2EE8, 0xD0FB917D), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0x35F94A83, 0xE258406C, 0xA31809F5, 0x9610247C), + value: 37, + }, + FCustomVersion { + key: FGuid::from_u32(0xB68FC16E, 0x8B1B42E2, 0xB453215C, 0x058844FE), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xB2E18506, 0x4273CFC2, 0xA54EF4BB, 0x758BBA07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x54683250, 0x809948AF, 0x8BC89896, 0xFBADF9B7), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x430C4D19, 0x71544970, 0x87699B69, 0xDF90B0E5), + value: 14, + }, + FCustomVersion { + key: FGuid::from_u32(0xAAFE32BD, 0x53954C14, 0xB66A5E25, 0x1032D1DD), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x23AFE18E, 0x4CE14E58, 0x8D61C252, 0xB953BEB7), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0xA462B7EA, 0xF4994E3A, 0x99C1EC1F, 0x8224E1B2), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x2EB5FDBD, 0x01AC4D10, 0x8136F38F, 0x3393A5DA), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x509D354F, 0xF6E6492F, 0xA74985B2, 0x073C631C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x717F9EE7, 0xE9B0493A, 0x88B39132, 0x1B388107), + value: 6, + }, + FCustomVersion { + key: FGuid::from_u32(0x4A56EB40, 0x10F511DC, 0x92D3347E, 0xB2C96AE7), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xD78A4A00, 0xE8584697, 0xBAA819B5, 0x487D46B4), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x5579F886, 0x933A4C1F, 0x83BA087B, 0x6361B92F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x612FBE52, 0xDA53400B, 0x910D4F91, 0x9FB1857C), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4237A36, 0xCAEA41C9, 0x8FA218F8, 0x58681BF3), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x804E3F75, 0x70884B49, 0xA4D68C06, 0x3C7EB6DC), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB680AF2, 0x59EF4BA3, 0xBAA819B5, 0x73C8443D), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x9950B70E, 0xB41A4E17, 0xBBCCFA0D, 0x57817FD6), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xAB965196, 0x45D808FC, 0xB7D7228D, 0x78AD569E), + value: 1, + }, + ]), + }), + save_game_class_name: FString::from("/Game/UI/BP_SaveOptions.BP_SaveOptions_C"), + }, + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("Slider1"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.16610672)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("Slider2"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.28251615)), + property_guid: FGuid::default(), + }, + ]), + } +} diff --git a/tests/common/palworld.rs b/src/test/common/palworld.rs similarity index 100% rename from tests/common/palworld.rs rename to src/test/common/palworld.rs diff --git a/tests/common/profile0.rs b/src/test/common/profile0.rs similarity index 99% rename from tests/common/profile0.rs rename to src/test/common/profile0.rs index a5e6aa0..502e167 100644 --- a/tests/common/profile0.rs +++ b/src/test/common/profile0.rs @@ -10,8 +10,7 @@ pub(crate) fn hints() -> HashMap { pub(crate) const PROFILE_0_JSON: &str = r#"{ "header": { - "type": "Version2", - "package_file_version": 522, + "file_version": 522, "engine_version": { "major": 4, "minor": 27, diff --git a/tests/common/regression.rs b/src/test/common/regression.rs similarity index 97% rename from tests/common/regression.rs rename to src/test/common/regression.rs index 3825fb1..887d31f 100644 --- a/tests/common/regression.rs +++ b/src/test/common/regression.rs @@ -1,7 +1,6 @@ pub const REGRESSION_01_JSON: &str = r#"{ "header": { - "type": "Version2", - "package_file_version": 517, + "file_version": 517, "engine_version": { "major": 4, "minor": 23, diff --git a/src/test/common/saveslot3.rs b/src/test/common/saveslot3.rs new file mode 100644 index 0000000..7e9164f --- /dev/null +++ b/src/test/common/saveslot3.rs @@ -0,0 +1,837 @@ +use std::collections::HashMap; + +use crate::types::{ + CollectionProperties, FCustomVersion, FCustomVersionContainer, FDateTime, FEngineVersion, + FFieldPathProperty, FFloatProperty, FGuid, FIntProperty, FMapProperty, FNameProperty, + FObjectProperty, FPackageFileVersion, FProperty, FSaveGameHeader, FStrProperty, FString, + FStructProperty, MapEntry, PropertyTag, PropertyTagIncompleteGuid, TArray, TaggedProperties, + TaggedProperty, USaveGame, +}; + +pub(crate) fn hints() -> HashMap { + HashMap::from([ + ( + "MinersManualKnownObjects.SetProperty.StructProperty".to_string(), + "Struct".to_string(), + ), + ( + "GameplayDatabase.MapProperty.Value.StructProperty".to_string(), + "Struct".to_string(), + ), + ( + "PlayerAttributes.MapProperty.Key.StructProperty".to_string(), + "Struct".to_string(), + ), + ]) +} + +pub(crate) fn expected() -> USaveGame { + USaveGame { + header: FSaveGameHeader { + package_file_version: FPackageFileVersion::UE4 { file_version: 522 }, + engine_version: FEngineVersion { + major: 4, + minor: 27, + patch: 2, + change_list: 18319896, + branch: FString::from("++UE4+Release-4.27"), + }, + custom_versions: Some(FCustomVersionContainer { + // custom_version_format: 3, + custom_versions: TArray::from([ + FCustomVersion { + key: FGuid::from_u32(0xFCF57AFA, 0x50764283, 0xB9A9E658, 0xFFA02D32), + value: 68, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB26E412, 0x1F154B4D, 0x9372550A, 0x961D2F70), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xA7820CFB, 0x20A74359, 0x8C542C14, 0x9623CF50), + value: 6, + }, + FCustomVersion { + key: FGuid::from_u32(0x82E77C4E, 0x332343A5, 0xB46B13C5, 0x97310DF3), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x11310AED, 0x2E554D61, 0xAF679AA3, 0xC5A1082C), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x24BB7AF3, 0x56464F83, 0x1F2F2DC2, 0x49AD96FF), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x76A52329, 0x092345B5, 0x98AED841, 0xCF2F6AD8), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x5FBC6907, 0x55C840AE, 0x8E67F184, 0x5EFFF13F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x9C54D522, 0xA8264FBE, 0x94210746, 0x61B482D0), + value: 43, + }, + FCustomVersion { + key: FGuid::from_u32(0xB0D832E4, 0x1F894F0D, 0xACCF7EB7, 0x36FD4AA2), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE1C64328, 0xA22C4D53, 0xA36C8E86, 0x6417BD8C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x375EC13C, 0x06E448FB, 0xB50084F0, 0x262A717E), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + value: 40, + }, + FCustomVersion { + key: FGuid::from_u32(0xCFFC743F, 0x43B04480, 0x939114DF, 0x171D2073), + value: 37, + }, + FCustomVersion { + key: FGuid::from_u32(0xB02B49B5, 0xBB2044E9, 0xA30432B7, 0x52E40360), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4E4105C, 0x59A149B5, 0xA7C540C4, 0x547EDFEE), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x39C831C9, 0x5AE647DC, 0x9A449C17, 0x3E1C8E7C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x78F01B33, 0xEBEA4F98, 0xB9B484EA, 0xCCB95AA2), + value: 14, + }, + FCustomVersion { + key: FGuid::from_u32(0x6631380F, 0x2D4D43E0, 0x8009CF27, 0x6956A95A), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x12F88B9F, 0x88754AFC, 0xA67CD90C, 0x383ABD29), + value: 45, + }, + FCustomVersion { + key: FGuid::from_u32(0x7B5AE74C, 0xD2704C10, 0xA9585798, 0x0B212A5A), + value: 13, + }, + FCustomVersion { + key: FGuid::from_u32(0xD7296918, 0x1DD64BDD, 0x9DE264A8, 0x3CC13884), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xC2A15278, 0xBFE74AFE, 0x6C1790FF, 0x531DF755), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6EACA3D4, 0x40EC4CC1, 0xB7868BED, 0x09428FC5), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x29E575DD, 0xE0A34627, 0x9D10D276, 0x232CDCEA), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAF43A65D, 0x7FD34947, 0x98733E8E, 0xD9C1BB05), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0x6B266CEC, 0x1EC74B8F, 0xA30BE4D9, 0x0942FC07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x0DF73D61, 0xA23F47EA, 0xB72789E9, 0x0C41499A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x601D1886, 0xAC644F84, 0xAA16D3DE, 0x0DEAC7D6), + value: 47, + }, + FCustomVersion { + key: FGuid::from_u32(0xE7086368, 0x6B234C58, 0x84391B70, 0x16265E91), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x9DFFBCD6, 0x494F0158, 0xE2211282, 0x3C92A888), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xF2AED0AC, 0x9AFE416F, 0x8664AA7F, 0xFA26D6FC), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x174F1F0B, 0xB4C645A5, 0xB13F2EE8, 0xD0FB917D), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0x35F94A83, 0xE258406C, 0xA31809F5, 0x9610247C), + value: 41, + }, + FCustomVersion { + key: FGuid::from_u32(0xB68FC16E, 0x8B1B42E2, 0xB453215C, 0x058844FE), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xB2E18506, 0x4273CFC2, 0xA54EF4BB, 0x758BBA07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x64F58936, 0xFD1B42BA, 0xBA967289, 0xD5D0FA4E), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6F0ED827, 0xA6094895, 0x9C91998D, 0x90180EA4), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x717F9EE7, 0xE9B0493A, 0x88B39132, 0x1B388107), + value: 8, + }, + FCustomVersion { + key: FGuid::from_u32(0x54683250, 0x809948AF, 0x8BC89896, 0xFBADF9B7), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x430C4D19, 0x71544970, 0x87699B69, 0xDF90B0E5), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0xAAFE32BD, 0x53954C14, 0xB66A5E25, 0x1032D1DD), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x23AFE18E, 0x4CE14E58, 0x8D61C252, 0xB953BEB7), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0xA462B7EA, 0xF4994E3A, 0x99C1EC1F, 0x8224E1B2), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x2EB5FDBD, 0x01AC4D10, 0x8136F38F, 0x3393A5DA), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x509D354F, 0xF6E6492F, 0xA74985B2, 0x073C631C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x4A56EB40, 0x10F511DC, 0x92D3347E, 0xB2C96AE7), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xD78A4A00, 0xE8584697, 0xBAA819B5, 0x487D46B4), + value: 18, + }, + FCustomVersion { + key: FGuid::from_u32(0x5579F886, 0x933A4C1F, 0x83BA087B, 0x6361B92F), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x612FBE52, 0xDA53400B, 0x910D4F91, 0x9FB1857C), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4237A36, 0xCAEA41C9, 0x8FA218F8, 0x58681BF3), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x804E3F75, 0x70884B49, 0xA4D68C06, 0x3C7EB6DC), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB680AF2, 0x59EF4BA3, 0xBAA819B5, 0x73C8443D), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x9950B70E, 0xB41A4E17, 0xBBCCFA0D, 0x57817FD6), + value: 1, + }, + ]), + }), + save_game_class_name: FString::from("/Script/CD.CDSave_GameState"), + }, + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("LastSaveTime"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::DateTime(FDateTime { + ticks: 638160761644140000, + })), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("PlayerClass"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FObjectProperty(FString::from( + "/Game/Character/Player/Blueprints/BP_Soldier.BP_Soldier_C", + ))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("Version"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FIntProperty(3)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("GameplayDatabase"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FMapProperty::Known { + allocation_flags: 0, + key_type: PropertyTag::Incomplete { + property_type: FString::from("NameProperty"), + size: 0, + array_index: 0, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + value_type: PropertyTag::Incomplete { + property_type: FString::from("StructProperty"), + size: 0, + array_index: 0, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + properties: TArray::from([ + MapEntry { + key: FProperty::from(FNameProperty(FString::from( + "unlock.welcomescreen.seen", + ))), + value: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AsFloat"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.0)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AsString"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString(None))), + property_guid: FGuid::default(), + }, + ]), + }), + }, + MapEntry { + key: FProperty::from(FNameProperty(FString::from( + "game.tutorial.finished", + ))), + value: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AsFloat"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(1.0)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AsString"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString(None))), + property_guid: FGuid::default(), + }, + ]), + }), + }, + MapEntry { + key: FProperty::from(FNameProperty(FString::from( + "game.tutorial.skipped", + ))), + value: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AsFloat"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(1.0)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AsString"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString(None))), + property_guid: FGuid::default(), + }, + ]), + }), + }, + MapEntry { + key: FProperty::from(FNameProperty(FString::from( + "dialogs.messages.seen.Rumiko.0.50", + ))), + value: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AsFloat"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(1.0)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AsString"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString(None))), + property_guid: FGuid::default(), + }, + ]), + }), + }, + MapEntry { + key: FProperty::from(FNameProperty(FString::from("codex.Rumiko"))), + value: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AsFloat"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(1.0)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AsString"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString(None))), + property_guid: FGuid::default(), + }, + ]), + }), + }, + ]), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("PlayerAttributes"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FMapProperty::Known { + allocation_flags: 0, + key_type: PropertyTag::Incomplete { + property_type: FString::from("StructProperty"), + size: 0, + array_index: 0, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + value_type: PropertyTag::Incomplete { + property_type: FString::from("FloatProperty"), + size: 0, + array_index: 0, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + properties: TArray::from([ + MapEntry { + key: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AttributeName"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString::from( + "Currency_Blueprints", + ))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("Attribute"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFieldPathProperty { + path: TArray::from([FString::from( + "Currency_Blueprints", + )]), + resolved_owner: FString::from( + "/Script/CD.CDPlayerAttributeSet", + ), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AttributeOwner"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FObjectProperty(FString::from( + "None", + ))), + property_guid: FGuid::default(), + }, + ]), + }), + value: FProperty::from(FFloatProperty(0.0)), + }, + MapEntry { + key: FProperty::from(FStructProperty::Custom { + struct_type: FString::from(""), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("AttributeName"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString::from( + "Currency_Electrum", + ))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("Attribute"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFieldPathProperty { + path: TArray::from([FString::from( + "Currency_Electrum", + )]), + resolved_owner: FString::from( + "/Script/CD.CDPlayerAttributeSet", + ), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AttributeOwner"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FObjectProperty(FString::from( + "None", + ))), + property_guid: FGuid::default(), + }, + ]), + }), + value: FProperty::from(FFloatProperty(0.0)), + }, + ]), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("SecondaryWeaponClass"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FObjectProperty(FString::from( + "/Game/Weapons/RocketLauncher/Blueprints/BP_RocketLauncher.BP_RocketLauncher_C", + ))), + property_guid: FGuid::default(), + }, + ]), + } +} + +pub(crate) const SAVESLOT_03_JSON: &str = r#"{ + "header": { + "file_version": 522, + "engine_version": { + "major": 4, + "minor": 27, + "patch": 2, + "change_list": 18319896, + "branch": "++UE4+Release-4.27" + }, + "custom_version_format": 3, + "custom_versions": { + "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 68, + "12E426FB-4D4B-151F-0A55-7293702F1D96": 3, + "FB0C82A7-5943-A720-142C-548C50CF2396": 6, + "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, + "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, + "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, + "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, + "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, + "22D5549C-BE4F-26A8-4607-2194D082B461": 43, + "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, + "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, + "3CC15E37-FB48-E406-F084-00B57E712A26": 4, + "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, + "3F74FCCF-8044-B043-DF14-919373201D17": 37, + "B5492BB0-E944-20BB-B732-04A36003E452": 3, + "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, + "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, + "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 14, + "0F383166-E043-4D2D-27CF-09805AA95669": 0, + "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 45, + "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, + "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, + "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, + "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, + "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, + "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, + "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, + "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, + "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 47, + "686308E7-584C-236B-701B-3984915E2616": 1, + "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, + "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, + "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, + "834AF935-6C40-58E2-F509-18A37C241096": 41, + "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, + "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, + "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, + "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, + "E79E7F71-3A49-B0E9-3291-B3880781381B": 8, + "50326854-AF48-9980-9698-C88BB7F9ADFB": 0, + "194D0C43-7049-5471-699B-6987E5B090DF": 15, + "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, + "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, + "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, + "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, + "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, + "40EB564A-DC11-F510-7E34-D392E76AC9B2": 2, + "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, + "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, + "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, + "367A23A4-C941-EACA-F818-A28FF31B6858": 4, + "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, + "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, + "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1 + }, + "save_game_class_name": "/Script/CD.CDSave_GameState" + }, + "properties": { + "LastSaveTime": { + "type": "StructProperty", + "type_name": "DateTime", + "DateTime": { + "ticks": 638160761644140000 + } + }, + "PlayerClass": { + "type": "ObjectProperty", + "value": "/Game/Character/Player/Blueprints/BP_Soldier.BP_Soldier_C" + }, + "Version": { + "type": "IntProperty", + "value": 3 + }, + "GameplayDatabase": { + "type": "MapProperty", + "value_type": "StructProperty", + "name_props": { + "unlock.welcomescreen.seen": { + "type": "StructPropertyValue", + "CustomStruct": { + "AsFloat": [ + { + "type": "FloatProperty", + "value": 0.0 + } + ], + "AsString": [ + { + "type": "StrProperty" + } + ] + } + }, + "game.tutorial.finished": { + "type": "StructPropertyValue", + "CustomStruct": { + "AsFloat": [ + { + "type": "FloatProperty", + "value": 1.0 + } + ], + "AsString": [ + { + "type": "StrProperty" + } + ] + } + }, + "game.tutorial.skipped": { + "type": "StructPropertyValue", + "CustomStruct": { + "AsFloat": [ + { + "type": "FloatProperty", + "value": 1.0 + } + ], + "AsString": [ + { + "type": "StrProperty" + } + ] + } + }, + "dialogs.messages.seen.Rumiko.0.50": { + "type": "StructPropertyValue", + "CustomStruct": { + "AsFloat": [ + { + "type": "FloatProperty", + "value": 1.0 + } + ], + "AsString": [ + { + "type": "StrProperty" + } + ] + } + }, + "codex.Rumiko": { + "type": "StructPropertyValue", + "CustomStruct": { + "AsFloat": [ + { + "type": "FloatProperty", + "value": 1.0 + } + ], + "AsString": [ + { + "type": "StrProperty" + } + ] + } + } + } + }, + "PlayerAttributes": { + "type": "MapProperty", + "key_type": "StructProperty", + "value_type": "FloatProperty", + "allocation_flags": 0, + "value": [ + [ + { + "type": "StructPropertyValue", + "CustomStruct": { + "AttributeName": [ + { + "type": "StrProperty", + "value": "Currency_Blueprints" + } + ], + "Attribute": [ + { + "type": "FieldPathProperty", + "value": { + "path": [ + "Currency_Blueprints" + ], + "resolved_owner": "/Script/CD.CDPlayerAttributeSet" + } + } + ], + "AttributeOwner": [ + { + "type": "ObjectProperty", + "value": "None" + } + ] + } + }, + { + "type": "FloatProperty", + "value": 0.0 + } + ], + [ + { + "type": "StructPropertyValue", + "CustomStruct": { + "AttributeName": [ + { + "type": "StrProperty", + "value": "Currency_Electrum" + } + ], + "Attribute": [ + { + "type": "FieldPathProperty", + "value": { + "path": [ + "Currency_Electrum" + ], + "resolved_owner": "/Script/CD.CDPlayerAttributeSet" + } + } + ], + "AttributeOwner": [ + { + "type": "ObjectProperty", + "value": "None" + } + ] + } + }, + { + "type": "FloatProperty", + "value": 0.0 + } + ] + ] + }, + "SecondaryWeaponClass": { + "type": "ObjectProperty", + "value": "/Game/Weapons/RocketLauncher/Blueprints/BP_RocketLauncher.BP_RocketLauncher_C" + } + } +}"#; diff --git a/src/test/common/slot1.rs b/src/test/common/slot1.rs new file mode 100644 index 0000000..76ddb78 --- /dev/null +++ b/src/test/common/slot1.rs @@ -0,0 +1,638 @@ +use crate::types::{ + CollectionProperties, FArrayProperty, FByteProperty, FCustomVersion, FCustomVersionContainer, + FDateTime, FDoubleProperty, FEngineVersion, FFloatProperty, FGuid, FInt8Property, + FInt16Property, FInt64Property, FIntProperty, FPackageFileVersion, FProperty, FPropertyTag, + FSaveGameHeader, FStrProperty, FString, FStructProperty, FUInt16Property, FUInt32Property, + FUInt64Property, PropertyTag, PropertyTagIncompleteGuid, TArray, TaggedProperties, + TaggedProperty, USaveGame, +}; + +#[allow(clippy::approx_constant)] +pub(crate) fn expected() -> USaveGame { + USaveGame { + header: FSaveGameHeader { + package_file_version: FPackageFileVersion::UE4 { file_version: 522 }, + engine_version: FEngineVersion { + major: 4, + minor: 27, + patch: 2, + change_list: 18319896, + branch: FString::from("++UE4+Release-4.27"), + }, + custom_versions: Some(FCustomVersionContainer { + custom_versions: TArray::from([ + FCustomVersion { + key: FGuid::from_u32(0x9C54D522, 0xA8264FBE, 0x94210746, 0x61B482D0), + value: 43, + }, + FCustomVersion { + key: FGuid::from_u32(0xB0D832E4, 0x1F894F0D, 0xACCF7EB7, 0x36FD4AA2), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE1C64328, 0xA22C4D53, 0xA36C8E86, 0x6417BD8C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x375EC13C, 0x06E448FB, 0xB50084F0, 0x262A717E), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + value: 40, + }, + FCustomVersion { + key: FGuid::from_u32(0xCFFC743F, 0x43B04480, 0x939114DF, 0x171D2073), + value: 37, + }, + FCustomVersion { + key: FGuid::from_u32(0xB02B49B5, 0xBB2044E9, 0xA30432B7, 0x52E40360), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4E4105C, 0x59A149B5, 0xA7C540C4, 0x547EDFEE), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x39C831C9, 0x5AE647DC, 0x9A449C17, 0x3E1C8E7C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x78F01B33, 0xEBEA4F98, 0xB9B484EA, 0xCCB95AA2), + value: 14, + }, + FCustomVersion { + key: FGuid::from_u32(0x6631380F, 0x2D4D43E0, 0x8009CF27, 0x6956A95A), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x12F88B9F, 0x88754AFC, 0xA67CD90C, 0x383ABD29), + value: 45, + }, + FCustomVersion { + key: FGuid::from_u32(0x7B5AE74C, 0xD2704C10, 0xA9585798, 0x0B212A5A), + value: 13, + }, + FCustomVersion { + key: FGuid::from_u32(0xD7296918, 0x1DD64BDD, 0x9DE264A8, 0x3CC13884), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xC2A15278, 0xBFE74AFE, 0x6C1790FF, 0x531DF755), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6EACA3D4, 0x40EC4CC1, 0xB7868BED, 0x09428FC5), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x29E575DD, 0xE0A34627, 0x9D10D276, 0x232CDCEA), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAF43A65D, 0x7FD34947, 0x98733E8E, 0xD9C1BB05), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0x6B266CEC, 0x1EC74B8F, 0xA30BE4D9, 0x0942FC07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x0DF73D61, 0xA23F47EA, 0xB72789E9, 0x0C41499A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x601D1886, 0xAC644F84, 0xAA16D3DE, 0x0DEAC7D6), + value: 47, + }, + FCustomVersion { + key: FGuid::from_u32(0xE7086368, 0x6B234C58, 0x84391B70, 0x16265E91), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x9DFFBCD6, 0x494F0158, 0xE2211282, 0x3C92A888), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xF2AED0AC, 0x9AFE416F, 0x8664AA7F, 0xFA26D6FC), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x174F1F0B, 0xB4C645A5, 0xB13F2EE8, 0xD0FB917D), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0x35F94A83, 0xE258406C, 0xA31809F5, 0x9610247C), + value: 41, + }, + FCustomVersion { + key: FGuid::from_u32(0xB68FC16E, 0x8B1B42E2, 0xB453215C, 0x058844FE), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xB2E18506, 0x4273CFC2, 0xA54EF4BB, 0x758BBA07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x64F58936, 0xFD1B42BA, 0xBA967289, 0xD5D0FA4E), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6F0ED827, 0xA6094895, 0x9C91998D, 0x90180EA4), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x717F9EE7, 0xE9B0493A, 0x88B39132, 0x1B388107), + value: 8, + }, + FCustomVersion { + key: FGuid::from_u32(0x54683250, 0x809948AF, 0x8BC89896, 0xFBADF9B7), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x8E7DDCB3, 0x80DA47BB, 0x9FD346A2, 0x93984DF6), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xCB8AB0CD, 0xE78C4BDE, 0xA8621393, 0x14E9EF62), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xAB965196, 0x45D808FC, 0xB7D7228D, 0x78AD569E), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x9950B70E, 0xB41A4E17, 0xBBCCFA0D, 0x57817FD6), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB680AF2, 0x59EF4BA3, 0xBAA819B5, 0x73C8443D), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xAFE08691, 0x3A0D4952, 0xB673673B, 0x7CF22D1E), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x2EB5FDBD, 0x01AC4D10, 0x8136F38F, 0x3393A5DA), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x509D354F, 0xF6E6492F, 0xA74985B2, 0x073C631C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xA462B7EA, 0xF4994E3A, 0x99C1EC1F, 0x8224E1B2), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x430C4D19, 0x71544970, 0x87699B69, 0xDF90B0E5), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0xAAFE32BD, 0x53954C14, 0xB66A5E25, 0x1032D1DD), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x23AFE18E, 0x4CE14E58, 0x8D61C252, 0xB953BEB7), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0x4A56EB40, 0x10F511DC, 0x92D3347E, 0xB2C96AE7), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xD78A4A00, 0xE8584697, 0xBAA819B5, 0x487D46B4), + value: 18, + }, + FCustomVersion { + key: FGuid::from_u32(0x5579F886, 0x933A4C1F, 0x83BA087B, 0x6361B92F), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x612FBE52, 0xDA53400B, 0x910D4F91, 0x9FB1857C), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4237A36, 0xCAEA41C9, 0x8FA218F8, 0x58681BF3), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x804E3F75, 0x70884B49, 0xA4D68C06, 0x3C7EB6DC), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x76A52329, 0x092345B5, 0x98AED841, 0xCF2F6AD8), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x5FBC6907, 0x55C840AE, 0x8E67F184, 0x5EFFF13F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xFCF57AFA, 0x50764283, 0xB9A9E658, 0xFFA02D32), + value: 68, + }, + FCustomVersion { + key: FGuid::from_u32(0x24BB7AF3, 0x56464F83, 0x1F2F2DC2, 0x49AD96FF), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x11310AED, 0x2E554D61, 0xAF679AA3, 0xC5A1082C), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x82E77C4E, 0x332343A5, 0xB46B13C5, 0x97310DF3), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB26E412, 0x1F154B4D, 0x9372550A, 0x961D2F70), + value: 3, + }, + ]), + }), + save_game_class_name: FString::from("/Script/UE4SaveFile.TestSaveGame"), + }, + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("u8_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FByteProperty::Byte(129)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("i8_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FInt8Property(-123)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("ushort_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt16Property(65530)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("short_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FInt16Property(-32764)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("uint32_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt32Property(4294967294)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("int32_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FIntProperty(-2147483647)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("ulong_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt64Property(18446744073709551614)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("long_test"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FInt64Property(-9223372036854775807)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("f_property"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(3.14159)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("d_property"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FDoubleProperty(3.14159265358979)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("str_property"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString::from("Hello world"))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("struct_property"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::Custom { + struct_type: FString::from("CustomStruct"), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([TaggedProperty { + property_name: FString::from("test_field"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt64Property(12345)), + property_guid: FGuid::default(), + }]), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("date_time_property"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::DateTime(FDateTime { + ticks: 637864237380020000, + })), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("array_of_structs"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FArrayProperty::TaggedStruct { + struct_tag: FPropertyTag::Some { + name: FString::from("array_of_structs"), + property_tag: PropertyTag::Incomplete { + property_type: FString::from("StructProperty"), + size: 120, + array_index: 0, + extra: CollectionProperties::Struct { + type_name: FString::from("CustomStruct"), + struct_guid: FGuid::default(), + }, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + }, + values: vec![ + FStructProperty::Custom { + struct_type: FString::from("CustomStruct"), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([TaggedProperty { + property_name: FString::from("test_field"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt64Property(10)), + property_guid: FGuid::default(), + }]), + }, + FStructProperty::Custom { + struct_type: FString::from("CustomStruct"), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([TaggedProperty { + property_name: FString::from("test_field"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FUInt64Property(10)), + property_guid: FGuid::default(), + }]), + }, + ], + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("array_of_ints"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FArrayProperty::Int(TArray::from([ + FIntProperty(12), + FIntProperty(12), + FIntProperty(12), + FIntProperty(12), + FIntProperty(12), + ]))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("array_of_strings"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FArrayProperty::Str(TArray::from([ + FStrProperty(FString::from("Hello world from array")), + FStrProperty(FString::from("Hello world from array")), + FStrProperty(FString::from("Hello world from array")), + ]))), + property_guid: FGuid::default(), + }, + ]), + } +} + +pub const SLOT1_JSON: &str = r#"{ + "header": { + "file_version": 522, + "engine_version": { + "major": 4, + "minor": 27, + "patch": 2, + "change_list": 18319896, + "branch": "++UE4+Release-4.27" + }, + "custom_version_format": 3, + "custom_versions": { + "22D5549C-BE4F-26A8-4607-2194D082B461": 43, + "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, + "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, + "3CC15E37-FB48-E406-F084-00B57E712A26": 4, + "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, + "3F74FCCF-8044-B043-DF14-919373201D17": 37, + "B5492BB0-E944-20BB-B732-04A36003E452": 3, + "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, + "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, + "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 14, + "0F383166-E043-4D2D-27CF-09805AA95669": 0, + "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 45, + "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, + "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, + "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, + "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, + "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, + "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, + "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, + "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, + "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 47, + "686308E7-584C-236B-701B-3984915E2616": 1, + "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, + "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, + "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, + "834AF935-6C40-58E2-F509-18A37C241096": 41, + "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, + "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, + "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, + "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, + "E79E7F71-3A49-B0E9-3291-B3880781381B": 8, + "50326854-AF48-9980-9698-C88BB7F9ADFB": 0, + "B3DC7D8E-BB47-DA80-A246-D39FF64D9893": 1, + "CDB08ACB-DE4B-8CE7-9313-62A862EFE914": 0, + "965196AB-FC08-D845-8D22-D7B79E56AD78": 1, + "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1, + "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, + "9186E0AF-5249-0D3A-3B67-73B61E2DF27C": 2, + "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, + "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, + "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, + "194D0C43-7049-5471-699B-6987E5B090DF": 15, + "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, + "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, + "40EB564A-DC11-F510-7E34-D392E76AC9B2": 2, + "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, + "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, + "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, + "367A23A4-C941-EACA-F818-A28FF31B6858": 4, + "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, + "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, + "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, + "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 68, + "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, + "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, + "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, + "12E426FB-4D4B-151F-0A55-7293702F1D96": 3 + }, + "save_game_class_name": "/Script/UE4SaveFile.TestSaveGame" + }, + "properties": { + "u8_test": { + "type": "ByteProperty", + "name": "None", + "Byte": 129 + }, + "i8_test": { + "type": "Int8Property", + "value": -123 + }, + "ushort_test": { + "type": "UInt16Property", + "value": 65530 + }, + "short_test": { + "type": "Int16Property", + "value": -32764 + }, + "uint32_test": { + "type": "UInt32Property", + "value": 4294967294 + }, + "int32_test": { + "type": "IntProperty", + "value": -2147483647 + }, + "ulong_test": { + "type": "UInt64Property", + "value": 18446744073709551614 + }, + "long_test": { + "type": "Int64Property", + "value": -9223372036854775807 + }, + "f_property": { + "type": "FloatProperty", + "value": 3.14159 + }, + "d_property": { + "type": "DoubleProperty", + "value": 3.14159265358979 + }, + "str_property": { + "type": "StrProperty", + "value": "Hello world" + }, + "struct_property": { + "type": "StructProperty", + "type_name": "CustomStruct", + "CustomStruct": { + "test_field": [ + { + "type": "UInt64Property", + "value": 12345 + } + ] + } + }, + "date_time_property": { + "type": "StructProperty", + "type_name": "DateTime", + "DateTime": { + "ticks": 637864237380020000 + } + }, + "array_of_structs": { + "type": "ArrayProperty", + "field_name": "array_of_structs", + "type_name": "CustomStruct", + "structs": [ + { + "CustomStruct": { + "test_field": [ + { + "type": "UInt64Property", + "value": 10 + } + ] + } + }, + { + "CustomStruct": { + "test_field": [ + { + "type": "UInt64Property", + "value": 10 + } + ] + } + } + ] + }, + "array_of_ints": { + "type": "ArrayProperty", + "ints": [ + 12, + 12, + 12, + 12, + 12 + ] + }, + "array_of_strings": { + "type": "ArrayProperty", + "strings": [ + "Hello world from array", + "Hello world from array", + "Hello world from array" + ] + } + } +}"#; diff --git a/tests/common/tagcontainer.rs b/src/test/common/tagcontainer.rs similarity index 98% rename from tests/common/tagcontainer.rs rename to src/test/common/tagcontainer.rs index 4c8b907..9ee6a7a 100644 --- a/tests/common/tagcontainer.rs +++ b/src/test/common/tagcontainer.rs @@ -1,7 +1,6 @@ pub const TAGCONTAINER_JSON: &str = r#"{ "header": { - "type": "Version2", - "package_file_version": 522, + "file_version": 522, "engine_version": { "major": 4, "minor": 27, diff --git a/src/test/common/vector2d.rs b/src/test/common/vector2d.rs new file mode 100644 index 0000000..e11c44f --- /dev/null +++ b/src/test/common/vector2d.rs @@ -0,0 +1,1513 @@ +use crate::types::{ + FBoolProperty, FCustomVersion, FCustomVersionContainer, FDelegateProperty, FEngineVersion, + FFloatProperty, FGuid, FIntProperty, FMulticastInlineDelegateProperty, FPackageFileVersion, + FProperty, FSaveGameHeader, FStrProperty, FString, FStructProperty, FVector2D, TArray, + TaggedProperties, TaggedProperty, USaveGame, +}; + +const DELEGATE_PREFIX: &str = "/Game/DefaultMap.DefaultMap:PersistentLevel."; + +pub(crate) fn expected() -> USaveGame { + USaveGame { + header: FSaveGameHeader { + package_file_version: FPackageFileVersion::UE5 { + file_version_ue4: 522, + file_version_ue5: 1009, + }, + engine_version: FEngineVersion { + major: 5, + minor: 3, + patch: 2, + change_list: 29314046, + branch: FString::from("++UE5+Release-5.3"), + }, + custom_versions: Some(FCustomVersionContainer { + // custom_version_format: 3, + custom_versions: TArray::from([ + FCustomVersion { + key: FGuid::from_u32(0x9C54D522, 0xA8264FBE, 0x94210746, 0x61B482D0), + value: 44, + }, + FCustomVersion { + key: FGuid::from_u32(0x62915CA3, 0x1C8E4BF7, 0xA30E12C7, 0xC8219DF7), + value: 32, + }, + FCustomVersion { + key: FGuid::from_u32(0xCC400D24, 0xE0E94E7B, 0x9BF9A283, 0xDCC0C027), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xB0D832E4, 0x1F894F0D, 0xACCF7EB7, 0x36FD4AA2), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE1C64328, 0xA22C4D53, 0xA36C8E86, 0x6417BD8C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x375EC13C, 0x06E448FB, 0xB50084F0, 0x262A717E), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + value: 40, + }, + FCustomVersion { + key: FGuid::from_u32(0xCFFC743F, 0x43B04480, 0x939114DF, 0x171D2073), + value: 37, + }, + FCustomVersion { + key: FGuid::from_u32(0xB02B49B5, 0xBB2044E9, 0xA30432B7, 0x52E40360), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4E4105C, 0x59A149B5, 0xA7C540C4, 0x547EDFEE), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x39C831C9, 0x5AE647DC, 0x9A449C17, 0x3E1C8E7C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x78F01B33, 0xEBEA4F98, 0xB9B484EA, 0xCCB95AA2), + value: 20, + }, + FCustomVersion { + key: FGuid::from_u32(0x6631380F, 0x2D4D43E0, 0x8009CF27, 0x6956A95A), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x12F88B9F, 0x88754AFC, 0xA67CD90C, 0x383ABD29), + value: 47, + }, + FCustomVersion { + key: FGuid::from_u32(0x7B5AE74C, 0xD2704C10, 0xA9585798, 0x0B212A5A), + value: 13, + }, + FCustomVersion { + key: FGuid::from_u32(0xD7296918, 0x1DD64BDD, 0x9DE264A8, 0x3CC13884), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0xC2A15278, 0xBFE74AFE, 0x6C1790FF, 0x531DF755), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x6EACA3D4, 0x40EC4CC1, 0xB7868BED, 0x09428FC5), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x29E575DD, 0xE0A34627, 0x9D10D276, 0x232CDCEA), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xAF43A65D, 0x7FD34947, 0x98733E8E, 0xD9C1BB05), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0x6B266CEC, 0x1EC74B8F, 0xA30BE4D9, 0x0942FC07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x0DF73D61, 0xA23F47EA, 0xB72789E9, 0x0C41499A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x601D1886, 0xAC644F84, 0xAA16D3DE, 0x0DEAC7D6), + value: 111, + }, + FCustomVersion { + key: FGuid::from_u32(0x8DBC2C5B, 0x54A743E0, 0xA768FCBB, 0x7DA29060), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x5B4C06B7, 0x24634AF8, 0x805BBF70, 0xCDF5D0DD), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xE7086368, 0x6B234C58, 0x84391B70, 0x16265E91), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0x9DFFBCD6, 0x494F0158, 0xE2211282, 0x3C92A888), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0xF2AED0AC, 0x9AFE416F, 0x8664AA7F, 0xFA26D6FC), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x174F1F0B, 0xB4C645A5, 0xB13F2EE8, 0xD0FB917D), + value: 10, + }, + FCustomVersion { + key: FGuid::from_u32(0x35F94A83, 0xE258406C, 0xA31809F5, 0x9610247C), + value: 41, + }, + FCustomVersion { + key: FGuid::from_u32(0xB68FC16E, 0x8B1B42E2, 0xB453215C, 0x058844FE), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xB2E18506, 0x4273CFC2, 0xA54EF4BB, 0x758BBA07), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x64F58936, 0xFD1B42BA, 0xBA967289, 0xD5D0FA4E), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x697DD581, 0xE64F41AB, 0xAA4A51EC, 0xBEB7B628), + value: 118, + }, + FCustomVersion { + key: FGuid::from_u32(0xD89B5E42, 0x24BD4D46, 0x8412ACA8, 0xDF641779), + value: 47, + }, + FCustomVersion { + key: FGuid::from_u32(0x59DA5D52, 0x12324948, 0xB8785978, 0x70B8E98B), + value: 8, + }, + FCustomVersion { + key: FGuid::from_u32(0x26075A32, 0x730F4708, 0x88E98C32, 0xF1599D05), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x6F0ED827, 0xA6094895, 0x9C91998D, 0x90180EA4), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x30D58BE3, 0x95EA4282, 0xA6E3B159, 0xD8EBB06A), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x717F9EE7, 0xE9B0493A, 0x88B39132, 0x1B388107), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0x68C409FC, 0x70954986, 0x8963ACD2, 0xC4865183), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x430C4D19, 0x71544970, 0x87699B69, 0xDF90B0E5), + value: 15, + }, + FCustomVersion { + key: FGuid::from_u32(0xAAFE32BD, 0x53954C14, 0xB66A5E25, 0x1032D1DD), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x23AFE18E, 0x4CE14E58, 0x8D61C252, 0xB953BEB7), + value: 11, + }, + FCustomVersion { + key: FGuid::from_u32(0xA462B7EA, 0xF4994E3A, 0x99C1EC1F, 0x8224E1B2), + value: 4, + }, + FCustomVersion { + key: FGuid::from_u32(0x2EB5FDBD, 0x01AC4D10, 0x8136F38F, 0x3393A5DA), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x509D354F, 0xF6E6492F, 0xA74985B2, 0x073C631C), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x95A4F03E, 0x7E0B49E4, 0xBA43D356, 0x94FF87D9), + value: 7, + }, + FCustomVersion { + key: FGuid::from_u32(0xB6E31B1C, 0xD29F11EC, 0x857E9F85, 0x6F9970E2), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x4A56EB40, 0x10F511DC, 0x92D3347E, 0xB2C96AE7), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x8417998A, 0xBBC043EC, 0x81B3D119, 0x072D2722), + value: 19, + }, + FCustomVersion { + key: FGuid::from_u32(0xD78A4A00, 0xE8584697, 0xBAA819B5, 0x487D46B4), + value: 18, + }, + FCustomVersion { + key: FGuid::from_u32(0x5579F886, 0x933A4C1F, 0x83BA087B, 0x6361B92F), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x612FBE52, 0xDA53400B, 0x910D4F91, 0x9FB1857C), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xA4237A36, 0xCAEA41C9, 0x8FA218F8, 0x58681BF3), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x804E3F75, 0x70884B49, 0xA4D68C06, 0x3C7EB6DC), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x1ED048F4, 0x2F2E4C68, 0x89D053A4, 0xF18F102D), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xFB680AF2, 0x59EF4BA3, 0xBAA819B5, 0x73C8443D), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x9950B70E, 0xB41A4E17, 0xBBCCFA0D, 0x57817FD6), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x5E1714CD, 0x484E2951, 0x707A89A7, 0x9302AB78), + value: 3, + }, + FCustomVersion { + key: FGuid::from_u32(0x0925477B, 0x763D4001, 0x9D91D673, 0x0B75B411), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x4288211B, 0x454816C6, 0x1A7667B2, 0x507A2A00), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0xDC49959B, 0x53C04DE7, 0x9156EA88, 0x5E7C5D39), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xA7820CFB, 0x20A74359, 0x8C542C14, 0x9623CF50), + value: 27, + }, + FCustomVersion { + key: FGuid::from_u32(0x82E77C4E, 0x332343A5, 0xB46B13C5, 0x97310DF3), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0xE21E1CAA, 0xAF47425E, 0x89BF6AD4, 0x4C44A8BB), + value: 0, + }, + FCustomVersion { + key: FGuid::from_u32(0x134A157E, 0xD5E249A3, 0x8D4E843C, 0x98FE9E31), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0xFCF57AFA, 0x50764283, 0xB9A9E658, 0xFFA02D32), + value: 79, + }, + FCustomVersion { + key: FGuid::from_u32(0x11310AED, 0x2E554D61, 0xAF679AA3, 0xC5A1082C), + value: 17, + }, + FCustomVersion { + key: FGuid::from_u32(0xF6DFBB78, 0xBB50A0E4, 0x4018B84D, 0x60CBAF23), + value: 2, + }, + FCustomVersion { + key: FGuid::from_u32(0x24BB7AF3, 0x56464F83, 0x1F2F2DC2, 0x49AD96FF), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x76A52329, 0x092345B5, 0x98AED841, 0xCF2F6AD8), + value: 5, + }, + FCustomVersion { + key: FGuid::from_u32(0x5FBC6907, 0x55C840AE, 0x8E67F184, 0x5EFFF13F), + value: 1, + }, + FCustomVersion { + key: FGuid::from_u32(0x92738C43, 0x29884D9C, 0x9A3D9BBE, 0x6EFF9FC0), + value: 1, + }, + ]), + }), + save_game_class_name: FString::from( + "/Game/_Blueprints/BP_SettingsSave.BP_SettingsSave_C", + ), + }, + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("SettingsChanged"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FMulticastInlineDelegateProperty(TArray::from([ + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_WaterGauge_C_2147482315", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plow_C_2147482312", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plow_Row_Single_C_2147482309", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plow_Row_3_C_2147482305", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plow_5Row_C_2147482301", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plow_Row_5_C_2147482297", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plant_C_2147482293", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plant_Row_C_2147482286", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plant_Row3_C_2147482280", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Plant_Row5_C_2147482274", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Cultivate_C_2147482268", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Cultivate_Row_C_2147482265", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Cultivate_Row3_C_2147482261", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Cultivate_Row5_C_2147482257", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_PlasticRow_C_2147482253", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Purchase_C_2147482249", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Purchase_1x10_C_2147482242", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Purchase_3Row_C_2147482235", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Purchase_5Row_C_2147482228", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Purchase_10x10_C_2147482221", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Modify_C_2147482214", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Row_C_2147482198", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Row3_C_2147482181", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Harvest_C_2147482164", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Harvest_Row_C_2147482161", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Harvest_Row_3_C_2147482157", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Harvest_Row_5_C_2147482153", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Harvest_Row_C_2147482149", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_AutomatedActionControl_C_2147482145", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_RemovePlaceable_C_2147482142", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_SeedSilo_C_2147482139", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_TractorBarn_C_2147482132", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Sell_C_2147482125", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_FuelStorageTank_C_2147482118", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_ChickenRun_C_2147482115", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_MovePlaceable_C_2147482112", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Beehive_C_2147482109", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_SetPHTool_Row_C_2147482106", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_BiodieselRefinery_C_2147482089", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_OilPress_C_2147482086", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_FlourMill_C_2147482083", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_LargeChickenCoop_C_2147482080", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_CropSign_C_2147482077", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Mulch_C_2147482070", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Mulch_Row_C_2147482054", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Mulch_Row3_C_2147482037", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Warehouse_C_2147482020", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_HarvestSilo_C_2147482013", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_Stockpile_C_2147482008", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_ActionTool_CompostStation_C_2147482001", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged_Event"), + }, + FDelegateProperty { + object: FString::from(format!("{}BP_Renders_C_1", DELEGATE_PREFIX)), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_PlayerPawn_C_2147482331", + DELEGATE_PREFIX + )), + function_name: FString::from("UpdatedSavedSettings"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478921", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478905", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478890", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478875", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478860", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478303", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478288", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478273", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478258", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478243", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478228", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478141", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478126", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478111", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147478096", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477750", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477735", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477720", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477705", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477690", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477675", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477660", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477645", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477189", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + FDelegateProperty { + object: FString::from(format!( + "{}BP_AutomatedTool_C_2147477162", + DELEGATE_PREFIX + )), + function_name: FString::from("SettingsChanged"), + }, + ]))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AudioSettings"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::Custom { + struct_type: FString::from("GameAudioSettings"), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("MasterLevel"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.20348908)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("MusicLevel"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.1511635)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("SFXLevel"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FFloatProperty(0.5436054)), + property_guid: FGuid::default(), + }, + ]), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("GameSettings"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::Custom { + struct_type: FString::from("GameSettings"), + class_name: FString(None), + struct_guid: FGuid::default(), + properties: TaggedProperties::from([ + TaggedProperty { + property_name: FString::from("CurrentSaveSlot"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStrProperty(FString::from("SAVE2"))), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("LoadTutorial"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(false)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("DisplayNewOrders"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(false)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("EscapeExitsTool"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(false)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("UseDarkMode"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(true)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("AnimateDayCycle"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(false)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("EnableTractorCollision"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(false)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("ShowInventory"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FBoolProperty(true)), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("CameraAngle"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FStructProperty::Vector2D(FVector2D { + x: 30.574748247861862, + y: 60.42525175213814, + })), + property_guid: FGuid::default(), + }, + ]), + }), + property_guid: FGuid::default(), + }, + TaggedProperty { + property_name: FString::from("HighScore"), + array_index: 0, + has_binary_or_native_serialize: false, + has_property_extensions: false, + property: FProperty::from(FIntProperty(2649)), + property_guid: FGuid::default(), + }, + ]), + } +} + +pub const VECTOR2D_JSON: &str = r#"{ + "header": { + "file_version_ue4": 522, + "file_version_ue5": 1009, + "engine_version": { + "major": 5, + "minor": 3, + "patch": 2, + "change_list": 29314046, + "branch": "++UE5+Release-5.3" + }, + "custom_version_format": 3, + "custom_versions": { + "22D5549C-BE4F-26A8-4607-2194D082B461": 44, + "A35C9162-F74B-8E1C-C712-0EA3F79D21C8": 32, + "240D40CC-7B4E-E9E0-83A2-F99B27C0C0DC": 0, + "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, + "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, + "3CC15E37-FB48-E406-F084-00B57E712A26": 4, + "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, + "3F74FCCF-8044-B043-DF14-919373201D17": 37, + "B5492BB0-E944-20BB-B732-04A36003E452": 3, + "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, + "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, + "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 20, + "0F383166-E043-4D2D-27CF-09805AA95669": 0, + "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 47, + "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, + "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, + "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, + "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, + "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, + "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, + "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, + "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, + "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 111, + "5B2CBC8D-E043-A754-BBFC-68A76090A27D": 2, + "B7064C5B-F84A-6324-70BF-5B80DDD0F5CD": 10, + "686308E7-584C-236B-701B-3984915E2616": 11, + "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, + "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, + "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, + "834AF935-6C40-58E2-F509-18A37C241096": 41, + "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, + "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, + "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, + "81D57D69-AB41-4FE6-EC51-4AAA28B6B7BE": 118, + "425E9BD8-464D-BD24-A8AC-1284791764DF": 47, + "525DDA59-4849-3212-7859-78B88BE9B870": 8, + "325A0726-0847-0F73-328C-E988059D59F1": 0, + "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, + "E38BD530-8242-EA95-59B1-E3A66AB0EBD8": 1, + "E79E7F71-3A49-B0E9-3291-B3880781381B": 17, + "FC09C468-8649-9570-D2AC-6389835186C4": 3, + "194D0C43-7049-5471-699B-6987E5B090DF": 15, + "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, + "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, + "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, + "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, + "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, + "3EF0A495-E449-0B7E-56D3-43BAD987FF94": 7, + "1C1BE3B6-EC11-9FD2-859F-7E85E270996F": 1, + "40EB564A-DC11-F510-7E34-D392E76AC9B2": 3, + "8A991784-EC43-C0BB-19D1-B38122272D07": 19, + "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, + "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, + "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, + "367A23A4-C941-EACA-F818-A28FF31B6858": 5, + "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, + "F448D01E-684C-2E2F-A453-D0892D108FF1": 1, + "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, + "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1, + "CD14175E-5129-4E48-A789-7A7078AB0293": 3, + "7B472509-0140-3D76-73D6-919D11B4750B": 1, + "1B218842-C616-4845-B267-761A002A7A50": 1, + "9B9549DC-E74D-C053-88EA-5691395D7C5E": 2, + "FB0C82A7-5943-A720-142C-548C50CF2396": 27, + "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, + "AA1C1EE2-5E42-47AF-D46A-BF89BBA8444C": 0, + "7E154A13-A349-E2D5-3C84-4E8D319EFE98": 2, + "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 79, + "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, + "78BBDFF6-E4A0-50BB-4DB8-184023AFCB60": 2, + "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, + "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, + "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, + "438C7392-9C4D-8829-BE9B-3D9AC09FFF6E": 1 + }, + "save_game_class_name": "/Game/_Blueprints/BP_SettingsSave.BP_SettingsSave_C" + }, + "properties": { + "SettingsChanged": { + "type": "MulticastInlineDelegateProperty", + "value": { + "delegates": [ + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_WaterGauge_C_2147482315", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_C_2147482312", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_Single_C_2147482309", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_3_C_2147482305", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_5Row_C_2147482301", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_5_C_2147482297", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_C_2147482293", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row_C_2147482286", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row3_C_2147482280", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row5_C_2147482274", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_C_2147482268", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row_C_2147482265", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row3_C_2147482261", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row5_C_2147482257", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_PlasticRow_C_2147482253", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_C_2147482249", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_1x10_C_2147482242", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_3Row_C_2147482235", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_5Row_C_2147482228", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_10x10_C_2147482221", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Modify_C_2147482214", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Row_C_2147482198", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Row3_C_2147482181", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_C_2147482164", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_C_2147482161", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_3_C_2147482157", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_5_C_2147482153", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_C_2147482149", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_AutomatedActionControl_C_2147482145", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_RemovePlaceable_C_2147482142", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_SeedSilo_C_2147482139", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_TractorBarn_C_2147482132", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Sell_C_2147482125", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_FuelStorageTank_C_2147482118", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_ChickenRun_C_2147482115", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_MovePlaceable_C_2147482112", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Beehive_C_2147482109", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_SetPHTool_Row_C_2147482106", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_BiodieselRefinery_C_2147482089", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_OilPress_C_2147482086", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_FlourMill_C_2147482083", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_LargeChickenCoop_C_2147482080", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_CropSign_C_2147482077", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_C_2147482070", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_Row_C_2147482054", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_Row3_C_2147482037", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Warehouse_C_2147482020", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_HarvestSilo_C_2147482013", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Stockpile_C_2147482008", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_CompostStation_C_2147482001", + "function_name": "SettingsChanged_Event" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_Renders_C_1", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_PlayerPawn_C_2147482331", + "function_name": "UpdatedSavedSettings" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478921", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478905", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478890", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478875", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478860", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478303", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478288", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478273", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478258", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478243", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478228", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478141", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478126", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478111", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478096", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477750", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477735", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477720", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477705", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477690", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477675", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477660", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477645", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477189", + "function_name": "SettingsChanged" + }, + { + "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477162", + "function_name": "SettingsChanged" + } + ] + } + }, + "AudioSettings": { + "type": "StructProperty", + "type_name": "GameAudioSettings", + "CustomStruct": { + "MasterLevel": [ + { + "type": "FloatProperty", + "value": 0.20348908 + } + ], + "MusicLevel": [ + { + "type": "FloatProperty", + "value": 0.1511635 + } + ], + "SFXLevel": [ + { + "type": "FloatProperty", + "value": 0.5436054 + } + ] + } + }, + "GameSettings": { + "type": "StructProperty", + "type_name": "GameSettings", + "CustomStruct": { + "CurrentSaveSlot": [ + { + "type": "StrProperty", + "value": "SAVE2" + } + ], + "LoadTutorial": [ + { + "type": "BoolProperty", + "value": false + } + ], + "DisplayNewOrders": [ + { + "type": "BoolProperty", + "value": false + } + ], + "EscapeExitsTool": [ + { + "type": "BoolProperty", + "value": false + } + ], + "UseDarkMode": [ + { + "type": "BoolProperty", + "value": true + } + ], + "AnimateDayCycle": [ + { + "type": "BoolProperty", + "value": false + } + ], + "EnableTractorCollision": [ + { + "type": "BoolProperty", + "value": false + } + ], + "ShowInventory": [ + { + "type": "BoolProperty", + "value": true + } + ], + "CameraAngle": [ + { + "type": "StructProperty", + "type_name": "Vector2D", + "Vector2D": { + "x": 30.574748247861862, + "y": 60.42525175213814 + } + } + ] + } + }, + "HighScore": { + "type": "IntProperty", + "value": 2649 + } + } +}"#; diff --git a/src/test/errors.rs b/src/test/errors.rs new file mode 100644 index 0000000..22e308e --- /dev/null +++ b/src/test/errors.rs @@ -0,0 +1,305 @@ +use std::assert_matches; +use std::io::Cursor; + +use binrw::BinRead; + +use crate::types::USaveGame; + +const UNEXPECTED_EOF: [u8; 0] = []; + +#[test] +fn test_unexpected_eof() { + let mut reader = Cursor::new(UNEXPECTED_EOF); + let err = USaveGame::read(&mut reader); + assert_matches!( + err, + Err(binrw::Error::Backtrace(binrw::error::Backtrace { error, .. })) + if matches!( + error.as_ref(), + binrw::Error::Io(error) + if format!("{error}") == "failed to fill whole buffer" + ), + ); +} + +const INVALID_HEADER: [u8; 4] = *b"GVAZ"; + +#[test] +fn test_invalid_header() { + let mut reader = Cursor::new(INVALID_HEADER); + let err = USaveGame::read(&mut reader); + assert_matches!( + err, + Err(binrw::Error::Backtrace(binrw::error::Backtrace { error, .. })) + if matches!( + error.as_ref(), + binrw::Error::BadMagic { pos: 0, found } + if format!("{found:?}") == "[71, 86, 65, 90]", + ) + ); +} + +// const INVALID_ARRAY_INDEX: [u8; 8] = [ +// 0, 0, 0, 0, // length +// 1, 0, 0, 0, // array_index +// ]; + +// #[test] +// fn test_invalid_array_index() { +// // StrProperty +// let mut reader = Cursor::new(INVALID_ARRAY_INDEX); +// let result = StrProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 4); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // EnumProperty +// let mut reader = Cursor::new(INVALID_ARRAY_INDEX); +// let result = EnumProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 4); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// let mut options = PropertyOptions { +// hints: &HashMap::new(), +// properties_stack: &mut Vec::new(), +// custom_versions: &HashableIndexMap::new(), +// }; + +// // ArrayProperty +// let mut reader = Cursor::new(INVALID_ARRAY_INDEX); +// let result = ArrayProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 4); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // SetProperty +// let mut reader = Cursor::new(INVALID_ARRAY_INDEX); +// let result = SetProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 4); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // MapProperty +// let mut reader = Cursor::new(INVALID_ARRAY_INDEX); +// let result = MapProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 4); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; +// } + +// const INVALID_TERMINATOR: [u8; 9] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, // terminator +// ]; + +// const INVALID_TERMINATOR_ENUM: [u8; 14] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // enum_type +// 1, // terminator +// ]; + +// const INVALID_TERMINATOR_MAP: [u8; 19] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // key_type +// 1, 0, 0, 0, 0, // value_type +// 1, // terminator +// ]; + +// #[test] +// fn test_invalid_terminator() { +// // StrProperty +// let mut reader = Cursor::new(INVALID_TERMINATOR); +// let result = StrProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 8); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // EnumProperty +// let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); +// let result = EnumProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 13); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// let mut options = PropertyOptions { +// hints: &HashMap::new(), +// properties_stack: &mut Vec::new(), +// custom_versions: &HashableIndexMap::new(), +// }; + +// // ArrayProperty +// let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); +// let result = ArrayProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 13); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // SetProperty +// let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); +// let result = SetProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 13); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // MapProperty +// let mut reader = Cursor::new(INVALID_TERMINATOR_MAP); +// let result = MapProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { +// assert_eq!(value, 1); +// assert_eq!(position, 18); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; +// } + +// const INVALID_LENGTH_STR: [u8; 13] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 0, // terminator +// 0, 0, 0, 0, // string +// ]; + +// const INVALID_LENGTH_ENUM: [u8; 19] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // enum_type +// 0, // terminator +// 1, 0, 0, 0, 0, // string +// ]; + +// const INVALID_LENGTH_ARRAY: [u8; 18] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // property_type +// 0, // terminator +// 0, 0, 0, 0, // element_count +// ]; + +// const INVALID_LENGTH_SET: [u8; 22] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // property_type +// 0, // terminator +// 0, 0, 0, 0, // allocation_flags +// 0, 0, 0, 0, // element_count +// ]; + +// const INVALID_LENGTH_MAP: [u8; 27] = [ +// 0, 0, 0, 0, // length +// 0, 0, 0, 0, // array_index +// 1, 0, 0, 0, 0, // key_type +// 1, 0, 0, 0, 0, // value_type +// 0, // terminator +// 0, 0, 0, 0, // allocation_flags +// 0, 0, 0, 0, // element_count +// ]; + +// #[test] +// fn test_invalid_length() { +// // StrProperty +// let mut reader = Cursor::new(INVALID_LENGTH_STR); +// let result = StrProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { +// assert_eq!(expected, 0); +// assert_eq!(read, 4); +// assert_eq!(position, 9); +// } +// _ => panic!("Unexpected result {result:?}"), +// } + +// // EnumProperty +// let mut reader = Cursor::new(INVALID_LENGTH_ENUM); +// let result = EnumProperty::read_header(&mut reader); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { +// assert_eq!(expected, 0); +// assert_eq!(read, 5); +// assert_eq!(position, 14); +// } +// _ => panic!("Unexpected result {result:?}"), +// } + +// let mut options = PropertyOptions { +// hints: &HashMap::new(), +// properties_stack: &mut Vec::new(), +// custom_versions: &HashableIndexMap::new(), +// }; + +// // ArrayProperty +// let mut reader = Cursor::new(INVALID_LENGTH_ARRAY); +// let result = ArrayProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { +// assert_eq!(expected, 0); +// assert_eq!(read, 4); +// assert_eq!(position, 14); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // SetProperty +// let mut reader = Cursor::new(INVALID_LENGTH_SET); +// let result = SetProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { +// assert_eq!(expected, 0); +// assert_eq!(read, 8); +// assert_eq!(position, 14); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; + +// // MapProperty +// let mut reader = Cursor::new(INVALID_LENGTH_MAP); +// let result = MapProperty::read_header(&mut reader, &mut options); +// match result { +// Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { +// assert_eq!(expected, 0); +// assert_eq!(read, 8); +// assert_eq!(position, 19); +// } +// _ => panic!("Unexpected result {result:?}"), +// }; +// } diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000..e7104b1 --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,8 @@ +#![cfg(test)] + +/// Common test definitions +pub mod common; + +mod errors; +mod name_arrayindex; +mod test_guid; diff --git a/src/test/name_arrayindex.rs b/src/test/name_arrayindex.rs new file mode 100644 index 0000000..0c33312 --- /dev/null +++ b/src/test/name_arrayindex.rs @@ -0,0 +1,61 @@ +use std::{assert_matches, io::Cursor}; + +use binrw::{BinRead, BinWrite}; + +use crate::{ + error::Result, + format::SerializationFormat, + types::{ + CollectionProperties, EEditorObjectVersion, EUE5ReleaseStreamObjectVersion, + EUnrealEngineObjectUE4Version, FNameProperty, FProperty, NAME_NAME_PROPERTY, PropertyTag, + PropertyTagIncompleteGuid, + }, +}; + +#[test] +fn name_property_with_array_index() -> Result<()> { + let data = vec![ + 0x0d, 0x00, 0x00, 0x00, 0x4e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, + 0x79, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x51, 0x55, 0x39, 0x31, 0x5f, 0x49, 0x6e, 0x76, 0x65, 0x73, 0x74, 0x69, 0x67, 0x61, 0x74, + 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x42, 0x32, 0x00, + ]; + + let format = &SerializationFormat::from_enums( + EUnrealEngineObjectUE4Version::PropertyGuidInPropertyTag, + None, + EUE5ReleaseStreamObjectVersion::BeforeCustomVersionWasAdded, + EEditorObjectVersion::BeforeCustomVersionWasAdded, + ); + + // Convert the Vec to a NameProperty + let mut cursor = Cursor::new(data); + let tag = PropertyTag::read_le_args(&mut cursor, (format,))?; + let prop = FProperty::read_le_args(&mut cursor, (format, &tag))?; + + // Compare the parsed value to its expected value + assert_matches!( + tag, + PropertyTag::Incomplete { + ref property_type, + size: 29, + array_index: 1, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid(None), + } if property_type == NAME_NAME_PROPERTY + ); + assert_eq!( + FProperty::from(FNameProperty("QU91_InvestigateTower_B2".into())), + prop + ); + + // Convert the NameProperty back to a Vec + let mut writer = Cursor::new(Vec::new()); + tag.write_le_args(&mut writer, (format,))?; + prop.write_le_args(&mut writer, (format,))?; + + // Compare the two Vecs + assert_eq!(cursor.get_ref(), writer.get_ref()); + + Ok(()) +} diff --git a/tests/gvas_tests/test_guid.rs b/src/test/test_guid.rs similarity index 54% rename from tests/gvas_tests/test_guid.rs rename to src/test/test_guid.rs index 692ed5b..1283123 100644 --- a/tests/gvas_tests/test_guid.rs +++ b/src/test/test_guid.rs @@ -1,6 +1,9 @@ -use std::str::FromStr; +use std::{assert_matches, str::FromStr}; -use gvas::types::Guid; +use crate::{ + error::{ParseGuidError, Result}, + types::FGuid, +}; const GUID_0_BYTES: [u8; 16] = [0; 16]; const GUID_1_BYTES: [u8; 16] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; @@ -14,12 +17,12 @@ const GUID_3_BYTES: [u8; 16] = [ #[test] fn test_guid_constructor() { for (value, expect) in [ - (GUID_0_BYTES, "0"), - (GUID_1_BYTES, "01000000-0000-0000-0000-000000000000"), - (GUID_2_BYTES, "00010203-0405-0607-0809-0A0B0C0D0E0F"), - (GUID_3_BYTES, "3805EC1D-D25F-45A9-EC42-AA6580416AC5"), + (GUID_0_BYTES, "00000000-0000-0000-0000-000000000000"), + (GUID_1_BYTES, "00000001-0000-0000-0000-000000000000"), + (GUID_2_BYTES, "03020100-0706-0504-0b0a-09080f0e0d0c"), + (GUID_3_BYTES, "1dec0538-a945-5fd2-65aa-42ecc56a4180"), ] { - assert_eq!(format!("{}", Guid(value)), expect); + assert_eq!(format!("{}", FGuid::from(value)), expect); } } @@ -31,9 +34,9 @@ fn test_guid_from_u128() { (GUID_2_BYTES, 0x0f0e_0d0c_0b0a_0908_0706_0504_0302_0100_u128), (GUID_3_BYTES, 0xc56a_4180_65aa_42ec_a945_5fd2_1dec_0538_u128), ] { - let guid = Guid(value); + let guid = FGuid::from(value); assert_eq!(u128::from(guid), from_u128); - assert_eq!(guid, Guid::from(from_u128)); + assert_eq!(guid, FGuid::from(from_u128)); } } @@ -45,14 +48,15 @@ fn test_guid_from_u32() { (GUID_2_BYTES, 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c), (GUID_3_BYTES, 0x1dec0538, 0xa9455fd2, 0x65aa42ec, 0xc56a4180), ] { - let guid = Guid(value); + let guid = FGuid::from(value); assert_eq!(<[u32; 4]>::from(guid), [a, b, c, d]); - assert_eq!(guid, Guid::from([a, b, c, d])); + assert_eq!(guid, FGuid::from([a, b, c, d])); } } #[test] -fn test_guid_from_str() { +fn test_guid_from_str() -> Result<()> { + let expected = FGuid::from_u32(0x3805EC1D, 0xD25F45A9, 0xEC42AA65, 0x80416AC5); for valid_guid in [ "3805EC1D-D25F-45A9-EC42-AA6580416AC5", "{3805EC1D-D25F-45A9-EC42-AA6580416AC5}", @@ -61,17 +65,22 @@ fn test_guid_from_str() { "3805ec1dd25f45a9ec42aa6580416ac5", "{3805ec1dd25f45a9ec42aa6580416ac5}", ] { - let guid = Guid::from_str(valid_guid).expect("parsing to succed"); - assert_eq!(u128::from(guid), 0xc56a_4180_65aa_42ec_a945_5fd2_1dec_0538); + let guid = FGuid::from_str(valid_guid)?; + assert_eq!(guid, expected); } + Ok(()) } +const GUID_TOO_SHORT: &str = "3805ec1d-d25f-45a9-ec42-aa658041"; +const GUID_NON_HEX: &str = "x805ec1d-h25f-45a9-ec42-aa6580416ac5"; + #[test] fn test_guid_from_str_invalid() { - for invalid_guid in [ - "3805ec1d-d25f-45a9-ec42-aa658041", // (too short) - "x805ec1d-h25f-45a9-ec42-aa6580416ac5", // (non-hex characters) - ] { - Guid::from_str(invalid_guid).expect_err("parsing to fail"); - } + let err = FGuid::from_str(GUID_TOO_SHORT); + assert_matches!(err, Err(ParseGuidError::InvalidLength(28))); + let err = FGuid::from_str(GUID_NON_HEX); + assert_matches!( + err, + Err(ParseGuidError::ParseIntError(e)) + if format!("{e:?}") == "ParseIntError { kind: InvalidDigit }"); } diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 22705f4..0000000 --- a/src/types.rs +++ /dev/null @@ -1,375 +0,0 @@ -use std::{ - error::Error, - fmt::{Debug, Display}, - hash::Hash, - str::FromStr, -}; - -/// Stores a 128-bit guid (globally unique identifier) -#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)] -pub struct Guid(pub [u8; 16]); - -impl Guid { - /// Create a guid from an array of sixteen u8s - #[inline] - pub const fn from_u8(value: [u8; 16]) -> Self { - Guid(value) - } - - /// Create a guid from an array of four u32s - #[inline] - pub const fn from_u32(value: [u32; 4]) -> Self { - Guid(transmute_4u32_16u8(value)) - } - - /// Create a guid from a u128 - #[inline] - pub const fn from_u128(value: u128) -> Self { - Guid(u128::to_le_bytes(value)) - } - - /// Returns true if the guid is zero. - #[inline] - pub const fn is_zero(&self) -> bool { - Guid::to_u128(self) == 0 - } - - /// Create an array of sixteen u8s from a guid - #[inline] - pub const fn to_u8(&self) -> [u8; 16] { - self.0 - } - - /// Create an array of four u32s from a guid - #[inline] - pub const fn to_u32(&self) -> [u32; 4] { - transmute_16u8_4u32(self.0) - } - - /// Create a u128 from a guid - #[inline] - pub const fn to_u128(&self) -> u128 { - u128::from_le_bytes(self.0) - } -} - -#[inline] -const fn transmute_4u32_16u8(value: [u32; 4]) -> [u8; 16] { - let value_le = [ - value[0].to_le(), - value[1].to_le(), - value[2].to_le(), - value[3].to_le(), - ]; - unsafe { std::mem::transmute(value_le) } -} - -#[inline] -const fn transmute_16u8_4u32(src: [u8; 16]) -> [u32; 4] { - let value: [u32; 4] = unsafe { std::mem::transmute(src) }; - [ - u32::from_le(value[0]), - u32::from_le(value[1]), - u32::from_le(value[2]), - u32::from_le(value[3]), - ] -} - -impl From<[u32; 4]> for Guid { - #[inline] - fn from(value: [u32; 4]) -> Self { - Self::from_u32(value) - } -} - -impl From for [u32; 4] { - #[inline] - fn from(value: Guid) -> Self { - Guid::to_u32(&value) - } -} - -impl From for Guid { - #[inline] - fn from(value: u128) -> Self { - Self::from_u128(value) - } -} - -impl From for u128 { - #[inline] - fn from(value: Guid) -> Self { - Guid::to_u128(&value) - } -} - -impl Debug for Guid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let guid = self.to_string(); - write!(f, "Guid({})", guid) - } -} - -impl Display for Guid { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_zero() { - write!(f, "0")?; - return Ok(()); - } - - write!(f, "{:02X}", self.0[0])?; - write!(f, "{:02X}", self.0[1])?; - write!(f, "{:02X}", self.0[2])?; - write!(f, "{:02X}", self.0[3])?; - - write!(f, "-")?; - - write!(f, "{:02X}", self.0[4])?; - write!(f, "{:02X}", self.0[5])?; - - write!(f, "-")?; - - write!(f, "{:02X}", self.0[6])?; - write!(f, "{:02X}", self.0[7])?; - - write!(f, "-")?; - - write!(f, "{:02X}", self.0[8])?; - write!(f, "{:02X}", self.0[9])?; - - write!(f, "-")?; - - write!(f, "{:02X}", self.0[10])?; - write!(f, "{:02X}", self.0[11])?; - write!(f, "{:02X}", self.0[12])?; - write!(f, "{:02X}", self.0[13])?; - write!(f, "{:02X}", self.0[14])?; - write!(f, "{:02X}", self.0[15])?; - Ok(()) - } -} - -/// An error ocurred while parsing a Guid -#[derive(Debug)] -pub struct ParseGuidError; - -impl Display for ParseGuidError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "invalid GUID syntax") - } -} - -impl Error for ParseGuidError {} - -impl FromStr for Guid { - type Err = ParseGuidError; - - fn from_str(s: &str) -> Result { - let cleaned = s.replace('-', ""); - let cleaned = cleaned.trim(); - let cleaned = cleaned.strip_prefix('{').unwrap_or(cleaned); - let cleaned = cleaned.strip_suffix('}').unwrap_or(cleaned); - - if cleaned.len() == 1 && cleaned == "0" { - return Ok(Guid([0u8; 16])); - } - - if cleaned.len() != 32 { - Err(ParseGuidError)?; - } - let mut guid = Guid(Default::default()); - for i in 0..16 { - guid.0[i] = - u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16).map_err(|_| ParseGuidError)?; - } - Ok(guid) - } -} - -#[cfg(feature = "serde")] -impl serde::Serialize for Guid { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.collect_str(self) - } -} - -#[cfg(feature = "serde")] -impl<'de> serde::Deserialize<'de> for Guid { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Guid::from_str(&s).map_err(serde::de::Error::custom) - } -} - -/// Map types -pub mod map { - use std::{ - fmt::Debug, - hash::Hash, - ops::{Deref, DerefMut}, - }; - - use indexmap::IndexMap; - - /// Wrapper around `IndexMap` to implement Hash and Eq functionality. - #[derive(Debug, Clone, PartialEq, Eq)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] - pub struct HashableIndexMap(pub IndexMap); - - impl HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - /// Create a new map. (Does not allocate.) - #[inline] - pub fn new() -> Self { - Self(IndexMap::new()) - } - - /// Create a new map with capacity for `n` key-value pairs. (Does not - /// allocate if `n` is zero.) - /// - /// Computes in **O(n)** time. - #[inline] - pub fn with_capacity(n: usize) -> Self { - Self(IndexMap::with_capacity(n)) - } - } - - impl Hash for HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - fn hash(&self, state: &mut H) { - for (key, value) in &self.0 { - key.hash(state); - value.hash(state); - } - } - } - - impl From<[(K, V); N]> for HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - /// # Examples - /// - /// ``` - /// use gvas::types::map::HashableIndexMap; - /// - /// let map1 = HashableIndexMap::from([(1, 2), (3, 4)]); - /// let map2: HashableIndexMap<_, _> = [(1, 2), (3, 4)].into(); - /// assert_eq!(map1, map2); - /// ``` - fn from(arr: [(K, V); N]) -> Self { - Self(IndexMap::from_iter(arr)) - } - } - - impl Deref for HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - type Target = IndexMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl DerefMut for HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - impl Default for HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - /// Return an empty [`IndexMap`] - fn default() -> Self { - Self(IndexMap::default()) - } - } - - // Implement IntoIterator for &HashableIndexMap - impl<'a, K, V> IntoIterator for &'a HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - type Item = (&'a K, &'a V); - type IntoIter = indexmap::map::Iter<'a, K, V>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter() - } - } - - // Implement IntoIterator for &mut HashableIndexMap - impl<'a, K, V> IntoIterator for &'a mut HashableIndexMap - where - K: Hash + Eq, - V: Hash, - { - type Item = (&'a K, &'a mut V); - type IntoIter = indexmap::map::IterMut<'a, K, V>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter_mut() - } - } - - /// Functions to serialize and deserialize an [`HashableIndexMap`] as an ordered sequence. - #[cfg(feature = "serde")] - pub mod serde_seq { - use std::hash::Hash; - - use indexmap::map::serde_seq; - use serde::de::{Deserialize, Deserializer}; - use serde::ser::{Serialize, Serializer}; - - use super::HashableIndexMap; - - /// Serializes an [`HashableIndexMap`] as an ordered sequence. - pub fn serialize( - map: &HashableIndexMap, - serializer: T, - ) -> Result - where - K: Serialize + Hash + Eq, - V: Serialize + Hash, - T: Serializer, - { - serde_seq::serialize(&map.0, serializer) - } - - /// Deserializes an [`HashableIndexMap`] from an ordered sequence. - pub fn deserialize<'de, D, K, V>( - deserializer: D, - ) -> Result, D::Error> - where - D: Deserializer<'de>, - K: Deserialize<'de> + Eq + Hash, - V: Deserialize<'de> + Hash, - { - Ok(HashableIndexMap(serde_seq::deserialize(deserializer)?)) - } - } -} diff --git a/src/types/e_editor_object_version.rs b/src/types/e_editor_object_version.rs new file mode 100644 index 0000000..934071e --- /dev/null +++ b/src/types/e_editor_object_version.rs @@ -0,0 +1,52 @@ +use crate::types::{CustomVersion, FGuid}; + +/// Custom serialization version for changes made in Dev-Editor stream. +/// See: Engine/Source/Runtime/Core/Public/UObject/EditorObjectVersion.h +pub enum EEditorObjectVersion { + BeforeCustomVersionWasAdded = 0, + GatheredTextProcessVersionFlagging, + GatheredTextPackageCacheFixesV1, + RootMetaDataSupport, + GatheredTextPackageCacheFixesV2, + TextFormatArgumentDataIsVariant, + SplineComponentCurvesInStruct, + ComboBoxControllerSupportUpdate, + RefactorMeshEditorMaterials, + AddedFontFaceAssets, + UPropertryForMeshSection, + WidgetGraphSchema, + AddedBackgroundBlurContentSlot, + StableUserDefinedEnumDisplayNames, + AddedInlineFontFaceAssets, + UPropertryForMeshSectionSerialize, + FastWidgetTemplates, + MaterialThumbnailRenderingChanges, + NewSlateClippingSystem, + MovieSceneMetaDataSerialization, + GatheredTextEditorOnlyPackageLocId, + AddedAlwaysSignNumberFormattingOption, + AddedMaterialSharedInputs, + AddedMorphTargetSectionIndices, + SerializeInstancedStaticMeshRenderData, + MeshDescriptionNewSerializationMovedToRelease, + MeshDescriptionNewAttributeFormat, + ChangeSceneCaptureRootComponent, + StaticMeshDeprecatedRawMesh, + MeshDescriptionBulkDataGuid, + MeshDescriptionRemovedHoles, + ChangedWidgetComponentWindowVisibilityDefault, + CultureInvariantTextSerializationKeyStability, + ScrollBarThicknessChange, + RemoveLandscapeHoleMaterial, + MeshDescriptionTriangles, + ComputeWeightedNormals, + SkeletalMeshBuildRefactor, + SkeletalMeshMoveEditorSourceDataToPrivateAsset, + NumberParsingOptionsNumberLimitsAndClamping, + SkeletalMeshSourceDataSupport16bitOfMaterialNumber, + AutomaticVersionPlusOne, +} + +impl CustomVersion for EEditorObjectVersion { + const GUID: FGuid = FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41); +} diff --git a/src/types/e_property_tag_flags.rs b/src/types/e_property_tag_flags.rs new file mode 100644 index 0000000..5b2819a --- /dev/null +++ b/src/types/e_property_tag_flags.rs @@ -0,0 +1,19 @@ +#![allow(clippy::expect_used)] + +use binrw::binrw; +use modular_bitfield::{bitfield, prelude::B3}; + +#[bitfield] +#[binrw] +#[br(map = Self::from_bytes)] +#[bw(map = |&x| Self::into_bytes(x))] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct EPropertyTagFlags { + pub has_array_index: bool, + pub has_property_guid: bool, + pub has_property_extensions: bool, + pub has_binary_or_native_serialize: bool, + pub bool_true: bool, + #[skip] + padding: B3, +} diff --git a/src/types/e_ue5_release_stream_object_version.rs b/src/types/e_ue5_release_stream_object_version.rs new file mode 100644 index 0000000..93aba11 --- /dev/null +++ b/src/types/e_ue5_release_stream_object_version.rs @@ -0,0 +1,40 @@ +use crate::types::{CustomVersion, FGuid}; + +/// Custom serialization version for changes made in //UE5/Release-* stream +/// See: Engine/Source/Runtime/Core/Public/UObject/UE5ReleaseStreamObjectVersion.h +pub enum EUE5ReleaseStreamObjectVersion { + BeforeCustomVersionWasAdded = 0, + ReflectionMethodEnum, + WorldPartitionActorDescSerializeHLODInfo, + RemovingTessellation, + LevelInstanceSerializeRuntimeBehavior, + PoseAssetRuntimeRefactor, + WorldPartitionActorDescSerializeActorFolderPath, + HairStrandsVertexFormatChange, + AddChaosMaxLinearAngularSpeed, + PackedLevelInstanceVersion, + PackedLevelInstanceBoundsFix, + CustomPropertyAnimGraphNodesUseOptionalPinManager, + TextFormatArgumentData64bitSupport, + MaterialLayerStacksAreNotParameters, + MaterialInterfaceSavedCachedData, + AddClothMappingLODBias, + AddLevelActorPackagingScheme, + WorldPartitionActorDescSerializeAttachParent, + ConvertedActorGridPlacementToSpatiallyLoadedFlag, + ActorGridPlacementDeprecateDefaultValueFixup, + PackedLevelActorUseWorldPartitionActorDesc, + AddLevelActorFolders, + RemoveSkeletalMeshLODModelBulkDatas, + ExcludeBrightnessFromEncodedHDRCubemap, + VolumetricCloudSampleCountUnification, + PoseAssetRawDataGUID, + ConvolutionBloomIntensity, + WorldPartitionHLODActorDescSerializeHLODSubActors, + LargeWorldCoordinates, + AutomaticVersionPlusOne, +} + +impl CustomVersion for EUE5ReleaseStreamObjectVersion { + const GUID: FGuid = FGuid::from_u32(0xD89B5E42, 0x24BD4D46, 0x8412ACA8, 0xDF641779); +} diff --git a/src/types/e_unreal_engine_object_ue4_version.rs b/src/types/e_unreal_engine_object_ue4_version.rs new file mode 100644 index 0000000..c7ef169 --- /dev/null +++ b/src/types/e_unreal_engine_object_ue4_version.rs @@ -0,0 +1,12 @@ +use binrw::binrw; + +#[binrw] +#[brw(repr = u32)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum EUnrealEngineObjectUE4Version { + OldestLoadablePackage = 214, + FtextHistoryDateTimezone = 422, + PropertyGuidInPropertyTag = 503, + PropertyTagSetMapSupport = 509, + AutomaticVersionPlusOne, +} diff --git a/src/object_version.rs b/src/types/e_unreal_engine_object_ue5_version.rs similarity index 71% rename from src/object_version.rs rename to src/types/e_unreal_engine_object_ue5_version.rs index fd873b3..02e2d7d 100644 --- a/src/object_version.rs +++ b/src/types/e_unreal_engine_object_ue5_version.rs @@ -1,8 +1,8 @@ -use num_enum::IntoPrimitive; +use binrw::binrw; -/// UE5 object versions. -#[derive(IntoPrimitive)] -#[repr(u32)] +#[binrw] +#[brw(repr = u32)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum EUnrealEngineObjectUE5Version { /// The original UE5 version, at the time this was added the UE4 version was 522, so UE5 will start from 1000 to show a clear difference InitialVersion = 1000, @@ -44,4 +44,21 @@ pub enum EUnrealEngineObjectUE5Version { /// Added property tag complete type name and serialization type PropertyTagCompleteTypeName, + + /// Changed UE::AssetRegistry::WritePackageData to include PackageBuildDependencies + AssetRegistryPackageBuildDependencies, + + /// Added meta data serialization offset to for saved, versioned packages + MetadataSerializationOffset, + + /// Added VCells to the object graph + VerseCells, + + /// Changed PackageFileSummary to write FIoHash PackageSavedHash instead of FGuid Guid + PackageSavedHash, + + /// OS shadow serialization of subobjects + OsSubObjectShadowSerialization, + + AutomaticVersionPlusOne, } diff --git a/src/types/f_array_property.rs b/src/types/f_array_property.rs new file mode 100644 index 0000000..5b1be26 --- /dev/null +++ b/src/types/f_array_property.rs @@ -0,0 +1,215 @@ +use binrw::binrw; + +use crate::{ + error::PropertyTagError, + format::SerializationFormat, + types::{ + CollectionProperties, FFloatProperty, FGuid, FIntProperty, FNameProperty, FObjectProperty, + FPropertyTag, FPropertyTypeName, FSoftObjectProperty, FStrProperty, FString, + FStructProperty, FTextProperty, NAME_BOOL_PROPERTY, NAME_BYTE_PROPERTY, NAME_ENUM_PROPERTY, + NAME_FLOAT_PROPERTY, NAME_INT_PROPERTY, NAME_NAME_PROPERTY, NAME_OBJECT_PROPERTY, + NAME_SOFT_OBJECT_PROPERTY, NAME_STR_PROPERTY, NAME_STRUCT_PROPERTY, NAME_TEXT_PROPERTY, + PropertyTag, TArray, + }, +}; + +impl FPropertyTag { + #[inline] + fn array_struct_type_name(&self) -> Result<&str, PropertyTagError> { + match self { + Self::Some { + property_tag: + PropertyTag::Incomplete { + extra: + CollectionProperties::Struct { + type_name: FString(Some(type_name)), + .. + }, + .. + }, + .. + } => Ok(type_name.as_ref()), + _ => Err(PropertyTagError::Unsupported( + "array_struct_type_name".into(), + format!("{self:?}"), + )), + } + } + + #[inline] + fn array_struct_guid(&self) -> Result { + match self { + Self::Some { + property_tag: + PropertyTag::Incomplete { + extra: CollectionProperties::Struct { struct_guid, .. }, + .. + }, + .. + } => Ok(*struct_guid), + _ => Err(PropertyTagError::Unsupported( + "array_struct_guid".into(), + format!("{self:?}"), + )), + } + } + + #[inline] + fn as_some(&self) -> Result<&PropertyTag, PropertyTagError> { + match self { + Self::Some { property_tag, .. } => Ok(property_tag), + Self::None => Err(PropertyTagError::Unsupported( + "as_some".into(), + format!("{self:?}"), + )), + } + } +} + +impl PropertyTag { + #[inline] + fn array_struct_suggested_size(&self, property_count: u32) -> Option { + if property_count > 0 + && let size = self.size() + && size >= 4 + { + Some((size - 4) / property_count) + } else { + None + } + } +} + +#[binrw] +#[br(import(format: &SerializationFormat, t: &PropertyTag, inner_type: &str))] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FArrayProperty { + #[br(pre_assert(inner_type == NAME_BOOL_PROPERTY))] + Bool(#[br(count = t.size())] Vec), + + #[br(pre_assert(inner_type == NAME_BYTE_PROPERTY))] + Byte(#[br(count = t.size())] Vec), + + #[br(pre_assert(inner_type == NAME_ENUM_PROPERTY))] + Enum( + #[br(try_calc = t.enum_type())] + #[bw(ignore)] + FPropertyTypeName, + TArray, + ), + + #[br(pre_assert(inner_type == NAME_FLOAT_PROPERTY))] + Float(TArray), + + #[br(pre_assert(inner_type == NAME_INT_PROPERTY))] + Int(TArray), + + #[br(pre_assert(inner_type == NAME_NAME_PROPERTY))] + Name(TArray), + + #[br(pre_assert(inner_type == NAME_OBJECT_PROPERTY))] + Object(TArray), + + #[br(pre_assert(inner_type == NAME_SOFT_OBJECT_PROPERTY))] + SoftObject(#[br(args(format))] TArray), + + #[br(pre_assert(inner_type == NAME_STR_PROPERTY))] + Str(TArray), + + #[br(pre_assert(inner_type == NAME_STRUCT_PROPERTY && format.property_tag_complete_type_name()))] + #[bw(assert(format.property_tag_complete_type_name()))] + Struct { + #[br(temp)] + #[br(try_calc = t.array_struct_type())] + #[bw(ignore)] + meta: (&str, &str, FGuid), + + #[br(calc = meta.0.into())] + #[bw(ignore)] + type_name: Box, + + #[br(calc = meta.1.into())] + #[bw(ignore)] + class_name: Box, + + #[br(calc = meta.2)] + #[bw(ignore)] + struct_guid: FGuid, + + #[br(temp, restore_position)] + #[bw(ignore)] + property_count: u32, + + #[br(args(format, t.array_struct_suggested_size(property_count), &type_name, Some(&class_name), struct_guid,))] + #[bw(args(format))] + values: TArray, + }, + + #[br(pre_assert(inner_type == NAME_STRUCT_PROPERTY && !format.property_tag_complete_type_name()))] + #[bw(assert(!format.property_tag_complete_type_name()))] + TaggedStruct { + #[br(temp)] + #[bw(try_calc(u32::try_from(values.len())))] + count: u32, + + #[brw(args(format))] + struct_tag: FPropertyTag, + + #[br(temp)] + #[br(try_calc = struct_tag.as_some())] + #[bw(ignore)] + struct_t: &PropertyTag, + + #[br(temp)] + #[br(try_calc = struct_tag.array_struct_type_name())] + #[bw(ignore)] + type_name: &str, + + #[br(temp)] + #[br(try_calc = struct_tag.array_struct_guid())] + #[bw(ignore)] + struct_guid: FGuid, + + #[br(temp, restore_position)] + #[bw(ignore)] + property_count: u32, + + #[br(temp, calc = { + if property_count > 0 && let size = t.size() && size >= 4 { + Some((size - 4) / property_count) + } else { + None + } + })] + #[bw(ignore)] + size: Option, + + #[br(count = count)] + #[br(args { inner: (format, t.array_struct_suggested_size(property_count), type_name, None, struct_guid, ) })] + #[bw(args(format))] + values: Vec, + }, + + #[br(pre_assert(inner_type == NAME_TEXT_PROPERTY))] + Text(#[brw(args(format))] TArray), +} + +impl FArrayProperty { + pub(crate) fn element_property_type_name(&self) -> &str { + match self { + Self::Bool(..) => NAME_BOOL_PROPERTY, + Self::Byte(..) => NAME_BYTE_PROPERTY, + Self::Enum(..) => NAME_ENUM_PROPERTY, + Self::Float(..) => NAME_FLOAT_PROPERTY, + Self::Int(..) => NAME_INT_PROPERTY, + Self::Name(..) => NAME_NAME_PROPERTY, + Self::Object(..) => NAME_OBJECT_PROPERTY, + Self::SoftObject(..) => NAME_SOFT_OBJECT_PROPERTY, + Self::Str(..) => NAME_STR_PROPERTY, + Self::Struct { .. } => NAME_STRUCT_PROPERTY, + Self::TaggedStruct { .. } => NAME_STRUCT_PROPERTY, + Self::Text(..) => NAME_TEXT_PROPERTY, + } + } +} diff --git a/src/types/f_bool_property.rs b/src/types/f_bool_property.rs new file mode 100644 index 0000000..7ef5fba --- /dev/null +++ b/src/types/f_bool_property.rs @@ -0,0 +1,26 @@ +use binrw::BinRead; + +use crate::{ + error::PropertyTagError, + types::{CollectionProperties, PropertyTag}, +}; + +impl PropertyTag { + fn bool_value(&self) -> Result { + match self { + Self::Incomplete { + extra: CollectionProperties::Bool { value }, + .. + } => Ok(*value != 0), + Self::Complete { flags, .. } => Ok(flags.bool_true()), + Self::Incomplete { .. } => Err(PropertyTagError::Unsupported( + "bool_value".into(), + format!("{self:?}"), + )), + } + } +} + +#[derive(BinRead, Debug, PartialEq)] +#[br(import(t: &PropertyTag))] +pub struct FBoolProperty(#[br(try_calc = t.bool_value())] pub bool); diff --git a/src/types/f_byte_property.rs b/src/types/f_byte_property.rs new file mode 100644 index 0000000..0a18c43 --- /dev/null +++ b/src/types/f_byte_property.rs @@ -0,0 +1,13 @@ +use binrw::binrw; + +use crate::types::{FEnumProperty, PropertyTag}; + +#[binrw] +#[br(import(t: &PropertyTag))] +#[derive(Debug, PartialEq)] +pub enum FByteProperty { + #[br(pre_assert(t.size() <= 1))] + Byte(u8), + #[br(pre_assert(t.size() > 1))] + Enum(#[br(args(t))] FEnumProperty), +} diff --git a/src/types/f_custom_version_container.rs b/src/types/f_custom_version_container.rs new file mode 100644 index 0000000..dbd31a2 --- /dev/null +++ b/src/types/f_custom_version_container.rs @@ -0,0 +1,41 @@ +use binrw::binrw; + +use crate::types::{FCustomVersion, FGuid, TArray}; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq, Eq)] +enum ECustomVersionSerializationFormat { + Unknown, + Guids, + Enums, + Optimized, +} + +pub type FCustomVersionArray = TArray; + +pub trait CustomVersion { + const GUID: FGuid; +} + +#[binrw] +#[derive(Debug, PartialEq)] +pub struct FCustomVersionContainer { + #[br(temp, assert(custom_version_format == ECustomVersionSerializationFormat::Optimized))] + #[bw(calc(ECustomVersionSerializationFormat::Optimized))] + custom_version_format: ECustomVersionSerializationFormat, + pub custom_versions: FCustomVersionArray, +} + +impl FCustomVersionContainer { + pub fn get(&self, version: FGuid) -> u32 { + self.custom_versions + .iter() + .find(|v| v.key == version) + .map_or(0, |v| v.value) + } + + pub fn get_custom(&self) -> u32 { + self.get(V::GUID) + } +} diff --git a/src/types/f_date_time.rs b/src/types/f_date_time.rs new file mode 100644 index 0000000..bf295a7 --- /dev/null +++ b/src/types/f_date_time.rs @@ -0,0 +1,52 @@ +use std::fmt::Display; + +use chrono::{DateTime, Utc}; + +use crate::types::FDateTime; + +const NANOS_PER_TICK: i64 = 100; +const TICKS_PER_SECOND: i64 = 10_000_000; +const UNIX_OFFSET_SECS: i64 = 62_135_596_800; +const UNIX_OFFSET_TICKS: i64 = UNIX_OFFSET_SECS * TICKS_PER_SECOND; + +impl FDateTime { + pub fn from_date_time(dt: DateTime) -> Option { + let ticks = dt + .timestamp_nanos_opt()? + .checked_div(NANOS_PER_TICK)? + .checked_add(UNIX_OFFSET_TICKS)?; + Some(Self { ticks }) + } + + pub fn to_datetime(&self) -> Option> { + let unix_nanos = self + .ticks + .checked_sub(UNIX_OFFSET_TICKS)? + .checked_mul(NANOS_PER_TICK)?; + Some(DateTime::from_timestamp_nanos(unix_nanos)) + } + + pub fn now() -> Self { + Utc::now().into() + } + + pub fn format_datetime(&self) -> String { + match self.to_datetime() { + Some(dt) => dt.format("%Y-%m-%d %H:%M:%S%.f UTC").to_string(), + None => format!("pre-epoch date (ticks={})", self.ticks), + } + } +} + +impl Display for FDateTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.format_datetime()) + } +} + +impl From> for FDateTime { + fn from(dt: DateTime) -> Self { + Self::from_date_time(dt) + .unwrap_or_else(|| unimplemented!("Unable to create FDateTime from {dt}")) + } +} diff --git a/src/types/f_enum_property.rs b/src/types/f_enum_property.rs new file mode 100644 index 0000000..3886f86 --- /dev/null +++ b/src/types/f_enum_property.rs @@ -0,0 +1,13 @@ +use binrw::binrw; + +use crate::types::{FPropertyTypeName, FString, PropertyTag}; + +#[binrw] +#[br(import(t: &PropertyTag))] +#[derive(Debug, PartialEq)] +pub struct FEnumProperty( + #[br(try_calc = t.enum_type())] + #[bw(ignore)] + pub FPropertyTypeName, + pub FString, +); diff --git a/src/types/f_guid.rs b/src/types/f_guid.rs new file mode 100644 index 0000000..43a6855 --- /dev/null +++ b/src/types/f_guid.rs @@ -0,0 +1,298 @@ +use std::{fmt::Display, str::FromStr}; + +use binrw::binrw; + +use crate::error::ParseGuidError; + +/// Enumerates known GUID formats. +#[derive(Clone, Copy, Debug)] +pub enum EGuidFormats { + /// 32 digits. + /// + /// For example: "00000000000000000000000000000000" + Digits, + + /// 32 digits in lowercase + /// + /// For example: "0123abc456def789abcd123ef4a5b6c7" + DigitsLower, + + /// 32 digits separated by hyphens. + /// + /// For example: 00000000-0000-0000-0000-000000000000 + DigitsWithHyphens, + + /// 32 digits separated by hyphens, in lowercase as described by RFC 4122. + /// + /// For example: bd048ce3-358b-46c5-8cee-627c719418f8 + DigitsWithHyphensLower, + + /// 32 digits separated by hyphens and enclosed in braces. + /// + /// For example: {00000000-0000-0000-0000-000000000000} + DigitsWithHyphensInBraces, + + /// 32 digits separated by hyphens and enclosed in parentheses. + /// + /// For example: (00000000-0000-0000-0000-000000000000) + DigitsWithHyphensInParentheses, + + /// Comma-separated hexadecimal values enclosed in braces. + /// + /// For example: {0x00000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}} + HexValuesInBraces, + + /// This format is currently used by the FUniqueObjectGuid class. + /// + /// For example: 00000000-00000000-00000000-00000000 + UniqueObjectGuid, + + /// Base64 characters with dashes and underscores instead of pluses and slashes (respectively) + /// + /// For example: AQsMCQ0PAAUKCgQEBAgADQ + Short, + + /// Base-36 encoded, compatible with case-insensitive OS file systems (such as Windows). + /// + /// For example: 1DPF6ARFCM4XH5RMWPU8TGR0J + Base36Encoded, +} + +#[binrw] +#[derive(Copy, Clone, Default, Hash, PartialEq, Eq)] +pub struct FGuid { + a: u32, + b: u32, + c: u32, + d: u32, +} + +impl std::fmt::Debug for FGuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_valid() { + write!( + f, + "FGuid::from_u32(0x{:08X}, 0x{:08X}, 0x{:08X}, 0x{:08X})", + self.a, self.b, self.c, self.d + ) + } else { + write!(f, "FGuid::default()") + } + } +} + +impl FGuid { + #[inline] + pub const fn from_u32(a: u32, b: u32, c: u32, d: u32) -> Self { + Self { a, b, c, d } + } + + #[inline] + pub const fn from_u32s(value: [u32; 4]) -> Self { + let [a, b, c, d] = value; + Self::from_u32(a, b, c, d) + } + + #[inline] + pub const fn from_u8(value: [u8; 16]) -> Self { + let [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] = value; + let a = u32::from_le_bytes([a, b, c, d]); + let b = u32::from_le_bytes([e, f, g, h]); + let c = u32::from_le_bytes([i, j, k, l]); + let d = u32::from_le_bytes([m, n, o, p]); + Self::from_u32(a, b, c, d) + } + + #[inline] + pub const fn from_u128(value: u128) -> Self { + Self::from_u8(value.to_le_bytes()) + } + + #[inline] + pub const fn to_u8(self) -> [u8; 16] { + let [a, b, c, d] = self.a.to_le_bytes(); + let [e, f, g, h] = self.b.to_le_bytes(); + let [i, j, k, l] = self.c.to_le_bytes(); + let [m, n, o, p] = self.d.to_le_bytes(); + [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p] + } + + #[inline] + pub const fn to_u32(self) -> [u32; 4] { + [self.a, self.b, self.c, self.d] + } + + #[inline] + pub const fn to_u128(self) -> u128 { + u128::from_le_bytes(self.to_u8()) + } + + #[inline] + pub const fn is_valid(&self) -> bool { + (self.a | self.b | self.c | self.d) != 0 + } + + pub fn format( + &self, + f: &mut std::fmt::Formatter<'_>, + format: EGuidFormats, + ) -> std::fmt::Result { + match format { + EGuidFormats::DigitsWithHyphens => write!( + f, + "{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}", + self.a, + self.b >> 16, + self.b & 0xFFFF, + self.c >> 16, + self.c & 0xFFFF, + self.d + ), + EGuidFormats::DigitsWithHyphensLower => write!( + f, + "{:08x}-{:04x}-{:04x}-{:04x}-{:04x}{:08x}", + self.a, + self.b >> 16, + self.b & 0xFFFF, + self.c >> 16, + self.c & 0xFFFF, + self.d + ), + EGuidFormats::DigitsWithHyphensInBraces => write!( + f, + "{{{:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X}}}", + self.a, + self.b >> 16, + self.b & 0xFFFF, + self.c >> 16, + self.c & 0xFFFF, + self.d + ), + EGuidFormats::DigitsWithHyphensInParentheses => write!( + f, + "({:08X}-{:04X}-{:04X}-{:04X}-{:04X}{:08X})", + self.a, + self.b >> 16, + self.b & 0xFFFF, + self.c >> 16, + self.c & 0xFFFF, + self.d + ), + EGuidFormats::HexValuesInBraces => write!( + f, + "{{0x{:08X},0x{:04X},0x{:04X},{{0x{:02X},0x{:02X},0x{:02X},0x{:02X},0x{:02X},0x{:02X},0x{:02X},0x{:02X}}}}}", + self.a, + self.b >> 16, + self.b & 0xFFFF, + self.c >> 24, + (self.c >> 16) & 0xFF, + (self.c >> 8) & 0xFF, + self.c & 0xFF, + self.d >> 24, + (self.d >> 16) & 0xFF, + (self.d >> 8) & 0xFF, + self.d & 0xFF + ), + EGuidFormats::UniqueObjectGuid => write!( + f, + "{:08X}-{:08X}-{:08X}-{:08X}", + self.a, self.b, self.c, self.d + ), + // EGuidFormats::Short => write!(f,"{}", base64_url::encode(&self.to_u8())), + // EGuidFormats::Base36Encoded => todo!(), + EGuidFormats::DigitsLower => { + write!( + f, + "{:08x}{:08x}{:08x}{:08x}", + self.a, self.b, self.c, self.d + ) + } + EGuidFormats::Digits => { + write!( + f, + "{:08X}{:08X}{:08X}{:08X}", + self.a, self.b, self.c, self.d + ) + } + _ => todo!("{format:?}"), + } + } +} + +impl Display for FGuid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.format(f, EGuidFormats::DigitsWithHyphensLower) + } +} + +macro_rules! from { + ($src:ty, $dst:ty, $con:path) => { + impl From<$src> for $dst { + fn from(value: $src) -> Self { + $con(value) + } + } + }; +} + +from!(u128, FGuid, Self::from_u128); +from!([u32; 4], FGuid, Self::from_u32s); +from!([u8; 16], FGuid, Self::from_u8); + +from!(FGuid, u128, FGuid::to_u128); +from!(FGuid, [u32; 4], FGuid::to_u32); +from!(FGuid, [u8; 16], FGuid::to_u8); + +impl FromStr for FGuid { + type Err = ParseGuidError; + + fn from_str(s: &str) -> Result { + let s = s.replace('-', ""); + let s = s.trim(); + let s = s.strip_prefix('{').unwrap_or(s); + let s = s.strip_suffix('}').unwrap_or(s); + let s = s.strip_prefix('(').unwrap_or(s); + let s = s.strip_suffix(')').unwrap_or(s); + + if s == "0" { + return Ok(Self::default()); + } + + let length = s.len(); + if length != 32 { + return Err(ParseGuidError::InvalidLength(length)); + } + + let a = u32::from_str_radix(&s[0..8], 16)?; + let b_hi = u32::from_str_radix(&s[8..12], 16)?; + let b_lo = u32::from_str_radix(&s[12..16], 16)?; + let c_hi = u32::from_str_radix(&s[16..20], 16)?; + let c_lo = u32::from_str_radix(&s[20..24], 16)?; + let d = u32::from_str_radix(&s[24..32], 16)?; + + let b = (b_hi << 16) | b_lo; + let c = (c_hi << 16) | c_lo; + + Ok(Self::from_u32(a, b, c, d)) + } +} + +#[cfg(test)] +mod test { + use crate::error::Result; + + use super::*; + + #[test] + fn display() -> Result<()> { + let expected = "e4b068ed-f494-42e9-a231-da0b2e46bb41"; + for guid in [ + FGuid::from_u32(0xE4B068ED, 0xF49442E9, 0xA231DA0B, 0x2E46BB41), + FGuid::from_u128(0x2E46BB41A231DA0BF49442E9E4B068ED), + FGuid::from_str(expected)?, + ] { + assert_eq!(format!("{guid}"), expected); + } + Ok(()) + } +} diff --git a/src/types/f_map_property.rs b/src/types/f_map_property.rs new file mode 100644 index 0000000..7805a23 --- /dev/null +++ b/src/types/f_map_property.rs @@ -0,0 +1,70 @@ +use crate::{ + format::SerializationFormat, + types::{EPropertyTagFlags, FProperty, PropertyTag, TArray}, +}; +use binrw::binrw; + +#[binrw] +#[br(import(format: &SerializationFormat, t: &PropertyTag))] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FMapProperty { + #[br(pre_assert(!t.flags().is_some_and(EPropertyTagFlags::has_binary_or_native_serialize)))] + Known { + allocation_flags: u32, + + #[br(try_calc = t.map_key_type())] + #[bw(ignore)] + key_type: PropertyTag, + + #[br(try_calc = t.map_value_type())] + #[bw(ignore)] + value_type: PropertyTag, + + #[br(args(format, &key_type, &value_type))] + #[bw(args(format))] + properties: TArray, + }, + Unknown { + #[br(try_calc = t.map_key_type())] + #[bw(ignore)] + key_type: PropertyTag, + + #[br(try_calc = t.map_value_type())] + #[bw(ignore)] + value_type: PropertyTag, + + #[br(count = t.size())] + data: Vec, + }, +} + +impl FMapProperty { + pub fn key_type(&self) -> &PropertyTag { + match self { + Self::Known { key_type, .. } => key_type, + Self::Unknown { key_type, .. } => key_type, + } + } + + pub fn value_type(&self) -> &PropertyTag { + match self { + Self::Known { value_type, .. } => value_type, + Self::Unknown { value_type, .. } => value_type, + } + } +} + +#[binrw] +#[br(import(format: &SerializationFormat, key_type: &PropertyTag, value_type: &PropertyTag))] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct MapEntry { + #[br(args(format, key_type))] + #[bw(args(format))] + pub key: FProperty, + + #[br(args(format, value_type))] + #[bw(args(format))] + pub value: FProperty, +} diff --git a/src/types/f_package_file_version.rs b/src/types/f_package_file_version.rs new file mode 100644 index 0000000..3f70505 --- /dev/null +++ b/src/types/f_package_file_version.rs @@ -0,0 +1,40 @@ +use binrw::binrw; + +use crate::types::SaveGameFileVersion; + +const UE5_VERSION: u32 = SaveGameFileVersion::PackageFileSummaryVersionChange as u32; + +#[binrw] +#[br(import(save_game_file_version: u32))] +#[derive(Debug, PartialEq)] +pub enum FPackageFileVersion { + #[br(pre_assert(save_game_file_version < UE5_VERSION))] + UE4 { + file_version: u32, //EUnrealEngineObjectUE4Version, + }, + #[br(pre_assert(save_game_file_version >= UE5_VERSION))] + UE5 { + file_version_ue4: u32, //EUnrealEngineObjectUE4Version, + file_version_ue5: u32, //EUnrealEngineObjectUE5Version, + }, +} + +impl FPackageFileVersion { + pub const fn version_ue4(&self) -> u32 { + match self { + Self::UE4 { file_version } => *file_version, + Self::UE5 { + file_version_ue4, .. + } => *file_version_ue4, + } + } + + pub const fn version_ue5(&self) -> u32 { + match self { + Self::UE4 { .. } => 0, + Self::UE5 { + file_version_ue5, .. + } => *file_version_ue5, + } + } +} diff --git a/src/types/f_property.rs b/src/types/f_property.rs new file mode 100644 index 0000000..4cf2364 --- /dev/null +++ b/src/types/f_property.rs @@ -0,0 +1,412 @@ +use binrw::{BinRead, binwrite}; + +use crate::{ + error::{PropertyTagError, binrw_custom}, + format::SerializationFormat, + types::{ + CollectionProperties, EPropertyTagFlags, FArrayProperty, FBoolProperty, FByteProperty, + FDelegateProperty, FDoubleProperty, FEnumProperty, FFieldPathProperty, FFloatProperty, + FGuid, FInt8Property, FInt16Property, FInt64Property, FIntProperty, FMapProperty, + FMulticastInlineDelegateProperty, FMulticastSparseDelegateProperty, FNameProperty, + FObjectProperty, FPropertyTypeName, FSetProperty, FSoftObjectProperty, FStrProperty, + FString, FStructProperty, FTextProperty, FUInt16Property, FUInt32Property, FUInt64Property, + NAME_ARRAY_PROPERTY, NAME_BOOL_PROPERTY, NAME_BYTE_PROPERTY, NAME_DELEGATE_PROPERTY, + NAME_DOUBLE_PROPERTY, NAME_ENUM_PROPERTY, NAME_FIELD_PATH_PROPERTY, NAME_FLOAT_PROPERTY, + NAME_INT_PROPERTY, NAME_INT8_PROPERTY, NAME_INT16_PROPERTY, NAME_INT64_PROPERTY, + NAME_MAP_PROPERTY, NAME_MULTICAST_INLINE_DELGATE_PROPERTY, + NAME_MULTICAST_SPARSE_DELGATE_PROPERTY, NAME_NAME_PROPERTY, NAME_NONE, + NAME_OBJECT_PROPERTY, NAME_SET_PROPERTY, NAME_SOFT_OBJECT_PROPERTY, NAME_STR_PROPERTY, + NAME_STRUCT_PROPERTY, NAME_TEXT_PROPERTY, NAME_UINT16_PROPERTY, NAME_UINT32_PROPERTY, + NAME_UINT64_PROPERTY, PropertyTag, PropertyTagIncompleteGuid, + }, +}; + +#[binwrite] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FProperty { + Array(#[bw(args(format))] FArrayProperty), + Bool(#[bw(ignore)] FBoolProperty), + Byte(FByteProperty), + Delegate(FDelegateProperty), + Double(FDoubleProperty), + Enum(FEnumProperty), + FieldPath(FFieldPathProperty), + Float(FFloatProperty), + Int(FIntProperty), + Int16(FInt16Property), + Int64(FInt64Property), + Int8(FInt8Property), + Map(#[bw(args(format))] FMapProperty), + MulticastInlineDelegate(FMulticastInlineDelegateProperty), + MulticastSparseDelegate(FMulticastSparseDelegateProperty), + Name(FNameProperty), + Object(FObjectProperty), + Set(#[bw(args(format))] FSetProperty), + SoftObject(FSoftObjectProperty), + Str(FStrProperty), + Struct(#[bw(args(format))] FStructProperty), + Text(#[bw(args(format))] FTextProperty), + UInt16(FUInt16Property), + UInt32(FUInt32Property), + UInt64(FUInt64Property), + Unknown(#[bw(ignore)] PropertyTag, Vec), +} + +impl FProperty { + fn property_type_name(&self) -> &str { + match &self { + Self::Array(..) => NAME_ARRAY_PROPERTY, + Self::Bool(..) => NAME_BOOL_PROPERTY, + Self::Byte(..) => NAME_BYTE_PROPERTY, + Self::Delegate(..) => NAME_DELEGATE_PROPERTY, + Self::Double(..) => NAME_DOUBLE_PROPERTY, + Self::Enum(..) => NAME_ENUM_PROPERTY, + Self::FieldPath(..) => NAME_FIELD_PATH_PROPERTY, + Self::Float(..) => NAME_FLOAT_PROPERTY, + Self::Int(..) => NAME_INT_PROPERTY, + Self::Int16(..) => NAME_INT16_PROPERTY, + Self::Int64(..) => NAME_INT64_PROPERTY, + Self::Int8(..) => NAME_INT8_PROPERTY, + Self::Map(..) => NAME_MAP_PROPERTY, + Self::MulticastInlineDelegate(..) => NAME_MULTICAST_INLINE_DELGATE_PROPERTY, + Self::MulticastSparseDelegate(..) => NAME_MULTICAST_SPARSE_DELGATE_PROPERTY, + Self::Name(..) => NAME_NAME_PROPERTY, + Self::Object(..) => NAME_OBJECT_PROPERTY, + Self::Set(..) => NAME_SET_PROPERTY, + Self::SoftObject(..) => NAME_SOFT_OBJECT_PROPERTY, + Self::Str(..) => NAME_STR_PROPERTY, + Self::Struct(..) => NAME_STRUCT_PROPERTY, + Self::Text(..) => NAME_TEXT_PROPERTY, + Self::UInt16(..) => NAME_UINT16_PROPERTY, + Self::UInt32(..) => NAME_UINT32_PROPERTY, + Self::UInt64(..) => NAME_UINT64_PROPERTY, + Self::Unknown(t, ..) => match t { + PropertyTag::Incomplete { + property_type: FString(Some(name)), + .. + } => name, + PropertyTag::Complete { property_type, .. } => property_type.name(), + PropertyTag::Incomplete { .. } => todo!("{t:?}"), + }, + } + } + + pub(crate) fn generate_tag( + &self, + format: &SerializationFormat, + size: u32, + array_index: u32, + has_binary_or_native_serialize: bool, + has_property_extensions: bool, + property_guid: FGuid, + ) -> Result { + if let Self::Unknown(original_tag, _) = self { + return Ok(original_tag.clone()); + } + + if format.property_tag_complete_type_name() { + let mut flags = EPropertyTagFlags::new(); + flags.set_has_array_index(array_index != 0); + flags.set_has_property_guid(property_guid.is_valid()); + flags.set_has_binary_or_native_serialize(has_binary_or_native_serialize); + flags.set_has_property_extensions(has_property_extensions); + if let Self::Bool(FBoolProperty(value)) = self { + flags.set_bool_true(*value); + } + let property_type = self.generate_complete_property_type()?; + Ok(PropertyTag::Complete { + property_type, + size, + flags, + array_index, + property_guid, + }) + } else { + let property_type = FString::from(self.property_type_name()); + let extra = self.generate_incomplete_property_extra(); + let maybe_property_guid = PropertyTagIncompleteGuid::from(property_guid); + Ok(PropertyTag::Incomplete { + property_type, + size, + array_index, + extra, + maybe_property_guid, + }) + } + } + + fn generate_incomplete_property_extra(&self) -> CollectionProperties { + match &self { + Self::Array(p) => CollectionProperties::Array { + inner_type: p.element_property_type_name().into(), + }, + Self::Bool(FBoolProperty(value)) => CollectionProperties::Bool { + value: u8::from(*value), + }, + Self::Byte(FByteProperty::Byte(_)) => CollectionProperties::Byte { + enum_name: NAME_NONE.into(), + }, + Self::Byte(FByteProperty::Enum(FEnumProperty(enum_type, _))) + | Self::Enum(FEnumProperty(enum_type, _)) => CollectionProperties::Enum { + enum_name: enum_type.enum_class_name().clone(), + }, + Self::Map(map_property) => { + let (key_type, value_type) = match map_property { + FMapProperty::Known { + key_type, + value_type, + .. + } + | FMapProperty::Unknown { + key_type, + value_type, + .. + } => (key_type, value_type), + }; + + let PropertyTag::Incomplete { + property_type: key_type, + .. + } = key_type + else { + todo!() + }; + let PropertyTag::Incomplete { + property_type: value_type, + .. + } = value_type + else { + todo!() + }; + CollectionProperties::Map { + inner_type: key_type.clone(), + value_type: value_type.clone(), + } + } + // Property::Optional(p) => CollectionProperties::Optional { inner_type }, + Self::Set(p) => CollectionProperties::Set { + inner_type: p.element_property_type_name().into(), + }, + Self::Struct(p) => CollectionProperties::Struct { + type_name: p.struct_type(), + struct_guid: p.struct_guid(), + }, + Self::Delegate(..) + | Self::Double(..) + | Self::FieldPath(..) + | Self::Float(..) + | Self::Int(..) + | Self::Int16(..) + | Self::Int64(..) + | Self::Int8(..) + | Self::MulticastInlineDelegate(..) + | Self::MulticastSparseDelegate(..) + | Self::Name(..) + | Self::Object(..) + | Self::SoftObject(..) + | Self::Str(..) + | Self::Text(..) + | Self::UInt16(..) + | Self::UInt32(..) + | Self::UInt64(..) => CollectionProperties::None, + Self::Unknown(..) => unimplemented!(), + } + } + + fn generate_complete_property_type(&self) -> Result { + Ok(match self { + Self::Array(array_property) => { + let x = match array_property { + FArrayProperty::Bool(_) => FPropertyTypeName::Bool, + FArrayProperty::Byte(_) => FPropertyTypeName::Byte(None), + FArrayProperty::Enum(enum_type, _) => match enum_type { + FPropertyTypeName::Enum { .. } => enum_type.clone(), + _ => unimplemented!("{enum_type:?}"), + }, + FArrayProperty::Float(_) => FPropertyTypeName::Float, + FArrayProperty::Int(_) => FPropertyTypeName::Int, + FArrayProperty::Name(_) => FPropertyTypeName::Name, + FArrayProperty::Object(_) => FPropertyTypeName::Object, + FArrayProperty::SoftObject(_) => FPropertyTypeName::SoftObject, + FArrayProperty::Str(_) => FPropertyTypeName::Str, + FArrayProperty::Struct { + type_name, + class_name, + struct_guid, + values: _, + } => FPropertyTypeName::Struct { + type_name: type_name.as_ref().into(), + class_name: class_name.as_ref().into(), + struct_guid: *struct_guid, + }, + FArrayProperty::TaggedStruct { .. } => unimplemented!(), + FArrayProperty::Text(_) => FPropertyTypeName::Text, + }; + FPropertyTypeName::Array(Box::new(x)) + } + Self::Bool(_) => FPropertyTypeName::Bool, + // Property::Delegate(_) => todo!(), + Self::Double(_) => FPropertyTypeName::Double, + // Property::Enum(_) => todo!(), + // Property::Float(_) => todo!(), + Self::Int(_) => FPropertyTypeName::Int, + // Property::Int16(_) => todo!(), + // Property::Int64(_) => todo!(), + // Property::Int8(_) => todo!(), + Self::Map(map_property) => FPropertyTypeName::Map { + key: Box::new(map_property.key_type().property_type_name()?), + value: Box::new(map_property.value_type().property_type_name()?), + }, + // Property::MulticastInlineDelegate(_) => todo!(), + // Property::MulticastSparseDelegate(_) => todo!(), + Self::Name(_) => FPropertyTypeName::Name, + Self::Object(_) => FPropertyTypeName::Object, + Self::SoftObject(_) => FPropertyTypeName::SoftObject, + Self::Str(_) => FPropertyTypeName::Str, + Self::Struct(struct_property) => FPropertyTypeName::Struct { + type_name: struct_property.struct_type(), + class_name: struct_property.struct_class(), + struct_guid: struct_property.struct_guid(), + }, + Self::Text(_) => FPropertyTypeName::Text, + // Property::UInt16(_) => todo!(), + // Property::UInt32(_) => todo!(), + // Property::UInt64(_) => todo!(), + _ => todo!("{self:?}"), + }) + } +} + +impl BinRead for FProperty { + type Args<'a> = (&'a SerializationFormat, &'a PropertyTag); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + (format, t): Self::Args<'_>, + ) -> binrw::BinResult { + let size = t.size(); + let start = reader.stream_position()?; + let err_convert = &binrw_custom(start); + + let property_type = t.property_type_str().map_err(err_convert)?; + + #[rustfmt::skip] // Disable wrapping on this block + let result = match property_type { + NAME_ARRAY_PROPERTY => { + let inner_type = t.array_inner_type().map_err(err_convert)?; + let array_property = FArrayProperty::read_options(reader, endian, (format, t, inner_type))?; + Self::from(array_property) + }, + NAME_BOOL_PROPERTY => Self::from( FBoolProperty::read_options(reader, endian, (t,))?), + NAME_BYTE_PROPERTY => Self::from( FByteProperty::read_options(reader, endian, (t,))?), + NAME_DELEGATE_PROPERTY => Self::from(FDelegateProperty::read_options(reader, endian, ())?), + NAME_DOUBLE_PROPERTY => Self::from(FDoubleProperty::read_options(reader, endian, ())?), + NAME_ENUM_PROPERTY => Self::from( FEnumProperty::read_options(reader, endian, (t,))?), + NAME_FIELD_PATH_PROPERTY => Self::from(FFieldPathProperty::read_options(reader, endian, ())?), + NAME_FLOAT_PROPERTY => Self::from( FFloatProperty::read_options(reader, endian, ())?), + NAME_INT16_PROPERTY => Self::from( FInt16Property::read_options(reader, endian, ())?), + NAME_INT64_PROPERTY => Self::from( FInt64Property::read_options(reader, endian, ())?), + NAME_INT8_PROPERTY => Self::from( FInt8Property::read_options(reader, endian, ())?), + NAME_INT_PROPERTY => Self::from( FIntProperty::read_options(reader, endian, ())?), + NAME_MAP_PROPERTY => Self::from( FMapProperty::read_options(reader, endian, (format, t))?), + NAME_MULTICAST_INLINE_DELGATE_PROPERTY => Self::from(FMulticastInlineDelegateProperty::read_options(reader, endian, ())?), + NAME_MULTICAST_SPARSE_DELGATE_PROPERTY => Self::from(FMulticastSparseDelegateProperty::read_options(reader, endian, ())?), + NAME_NAME_PROPERTY => Self::from( FNameProperty::read_options(reader, endian, ())?), + NAME_OBJECT_PROPERTY => Self::from(FObjectProperty::read_options(reader, endian, ())?), + // NAME_OPTIONAL_PROPERTY => Property::Optional(OptionalProperty::read_options(reader, endian, ())?), + NAME_SET_PROPERTY => Self::from( FSetProperty::read_options(reader, endian, (format, t))?), + NAME_SOFT_OBJECT_PROPERTY => Self::from(FSoftObjectProperty::read_options(reader, endian, (format,))?), + NAME_STRUCT_PROPERTY => { + let type_name = t.struct_type_name().unwrap_or_default(); + let class_name = t.struct_class_name(); + let guid = t.struct_guid().map_err(binrw_custom(start))?; + Self::from(FStructProperty::read_options(reader, endian, (format, Some(t.size()), type_name, class_name, guid))?) + }, + NAME_STR_PROPERTY => Self::from( FStrProperty::read_options(reader, endian, ())?), + NAME_TEXT_PROPERTY => Self::from( FTextProperty::read_options(reader, endian, (format,))?), + NAME_UINT16_PROPERTY => Self::from(FUInt16Property::read_options(reader, endian, ())?), + NAME_UINT32_PROPERTY => Self::from(FUInt32Property::read_options(reader, endian, ())?), + NAME_UINT64_PROPERTY => Self::from(FUInt64Property::read_options(reader, endian, ())?), + _ => { + println!("Warning: Unrecognized property type {property_type}"); + let err_convert = &binrw_custom(start); + let size = usize::try_from(size).map_err(err_convert)?; + let mut buf = vec![0u8; size]; + reader.read_exact(&mut buf)?; + let result = Self::Unknown(t.clone(), buf); + return Ok(result); + } + }; + + // Check bytes read compared to size + let size = i64::from(size); + let err_convert = &binrw_custom(start); + let start = i64::try_from(start).map_err(err_convert)?; + let pos = reader.stream_position()?; + let pos = i64::try_from(pos).map_err(err_convert)?; + let bytes_read = pos - start; + if size != 0 && bytes_read != size { + let remaining = size - bytes_read; + reader.seek_relative(-bytes_read)?; + let kind = if remaining < 0 { + "overflow" + } else { + "underflow" + }; + let offset = remaining.abs(); + eprintln!( + "Warning: Reader position {kind}: Bytes read 0x{bytes_read:04X} does not match size 0x{size:04X} for {t:#?}: 0x{offset:04X}", + ); + let size = usize::try_from(size).map_err(err_convert)?; + let mut buf = vec![0u8; size]; + reader.read_exact(&mut buf)?; + let result = Self::Unknown(t.clone(), buf); + return Ok(result); + } + + Ok(result) + } +} + +macro_rules! from { + ($variant:ident, $inner_type:ident) => { + impl From<$inner_type> for FProperty { + fn from(value: $inner_type) -> Self { + Self::$variant(value) + } + } + }; +} + +from!(Array, FArrayProperty); +from!(Bool, FBoolProperty); +from!(Byte, FByteProperty); +from!(Delegate, FDelegateProperty); +from!(Double, FDoubleProperty); +from!(Enum, FEnumProperty); +from!(FieldPath, FFieldPathProperty); +from!(Float, FFloatProperty); +from!(Int, FIntProperty); +from!(Int16, FInt16Property); +from!(Int64, FInt64Property); +from!(Int8, FInt8Property); +from!(Map, FMapProperty); +from!(MulticastInlineDelegate, FMulticastInlineDelegateProperty); +from!(MulticastSparseDelegate, FMulticastSparseDelegateProperty); +from!(Name, FNameProperty); +from!(Object, FObjectProperty); +from!(Set, FSetProperty); +from!(SoftObject, FSoftObjectProperty); +from!(Str, FStrProperty); +from!(Struct, FStructProperty); +from!(Text, FTextProperty); +from!(UInt16, FUInt16Property); +from!(UInt32, FUInt32Property); +from!(UInt64, FUInt64Property); + +#[cfg(test)] +mod test { + // TODO: Write tests +} diff --git a/src/types/f_property_tag.rs b/src/types/f_property_tag.rs new file mode 100644 index 0000000..f240f18 --- /dev/null +++ b/src/types/f_property_tag.rs @@ -0,0 +1,718 @@ +use std::io::Cursor; + +use binrw::{BinRead, BinWrite, binrw}; + +use crate::error::{ParseGuidError, PropertyTagError, binrw_custom}; +use crate::format::SerializationFormat; +use crate::types::{ + EPropertyTagFlags, FGuid, FProperty, FPropertyTypeName, FString, NAME_ARRAY_PROPERTY, + NAME_BOOL_PROPERTY, NAME_BYTE_PROPERTY, NAME_ENUM_PROPERTY, NAME_MAP_PROPERTY, NAME_NONE, + NAME_OPTION_PROPERTY, NAME_SET_PROPERTY, NAME_STRUCT_PROPERTY, NAME_TEXT_PROPERTY, +}; + +#[derive(Debug, Eq, PartialEq)] +pub enum FPropertyTag { + None, + Some { + name: FString, + property_tag: PropertyTag, + }, +} + +impl BinRead for FPropertyTag { + type Args<'a> = (&'a SerializationFormat,); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + (format,): Self::Args<'_>, + ) -> binrw::BinResult { + let name = FString::read_options(reader, endian, ())?; + if name == NAME_NONE { + return Ok(Self::None); + } + let property_tag = PropertyTag::read_options(reader, endian, (format,))?; + Ok(Self::Some { name, property_tag }) + } +} + +impl BinWrite for FPropertyTag { + type Args<'a> = (&'a SerializationFormat,); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match self { + Self::None => Ok(writer.write_all(b"\x05\x00\x00\x00None\x00")?), + Self::Some { name, property_tag } => { + name.write_options(writer, endian, ())?; + property_tag.write_options(writer, endian, args) + } + } + } +} + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PropertyTag { + #[br(pre_assert(!format.property_tag_complete_type_name()))] + Incomplete { + property_type: FString, + size: u32, + array_index: u32, + #[br(args(format, property_type.as_deref().unwrap_or("")))] + extra: CollectionProperties, + #[brw(args(format, property_type.as_deref().unwrap_or("")))] + maybe_property_guid: PropertyTagIncompleteGuid, + }, + + #[br(pre_assert(format.property_tag_complete_type_name()))] + #[bw(assert(flags.has_array_index() == (*array_index != 0)))] + #[bw(assert(flags.has_property_guid() == property_guid.is_valid()))] + Complete { + property_type: FPropertyTypeName, + size: u32, + flags: EPropertyTagFlags, + #[brw(if(flags.has_array_index()))] + array_index: u32, + #[brw(if(flags.has_property_guid()))] + property_guid: FGuid, + }, +} + +impl PropertyTag { + #[inline] + fn synthetic_incomplete(property_type: FString, size: u32) -> Self { + Self::Incomplete { + property_type, + size, + array_index: 0, + extra: CollectionProperties::None, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + } + } + + #[inline] + fn synthetic_complete(property_type: FPropertyTypeName, size: u32) -> Self { + Self::Complete { + property_type, + size, + flags: EPropertyTagFlags::new(), + array_index: 0, + property_guid: FGuid::default(), + } + } + + #[inline] + pub fn array_index(&self) -> u32 { + match self { + Self::Incomplete { array_index, .. } | Self::Complete { array_index, .. } => { + *array_index + } + } + } + + #[inline] + pub fn enum_type(&self) -> Result { + let result: FPropertyTypeName = match self { + Self::Incomplete { + property_type, + extra, + .. + } => FPropertyTypeName::Enum { + enum_class: match extra { + CollectionProperties::Byte { enum_name } + | CollectionProperties::Enum { enum_name } => enum_name.clone(), + CollectionProperties::Array { .. } => FString(None), + CollectionProperties::None if property_type == NAME_ENUM_PROPERTY => { + FString(None) + } + _ => Err(PropertyTagError::Unsupported( + "enum_type".into(), + format!("{self:?}"), + ))?, + }, + class_path: None, + inner_type: None, + }, + Self::Complete { property_type, .. } => match property_type { + FPropertyTypeName::Array(inner) => *inner.clone(), + other => other.clone(), + }, + }; + Ok(result) + } + + #[inline] + pub fn flags(&self) -> Option<&EPropertyTagFlags> { + match self { + Self::Incomplete { .. } => None, + Self::Complete { flags, .. } => Some(flags), + } + } + + #[inline] + fn guid(&self) -> FGuid { + match self { + Self::Incomplete { + maybe_property_guid, + .. + } => maybe_property_guid.into(), + Self::Complete { property_guid, .. } => *property_guid, + } + } + + #[inline] + pub fn struct_guid(&self) -> Result { + let result = match self { + Self::Complete { property_type, .. } => property_type.struct_guid().unwrap_or_default(), + Self::Incomplete { + extra: CollectionProperties::Struct { struct_guid, .. }, + .. + } => *struct_guid, + Self::Incomplete { .. } => FGuid::default(), + }; + Ok(result) + } + + #[inline] + pub fn map_key_type(&self) -> Result { + let size = 0; + match self { + Self::Incomplete { + extra: CollectionProperties::Map { inner_type, .. }, + .. + } => Ok(Self::synthetic_incomplete(inner_type.clone(), size)), + Self::Complete { + property_type: FPropertyTypeName::Map { key, .. }, + .. + } => Ok(Self::synthetic_complete(*key.clone(), size)), + _ => Err(PropertyTagError::Unsupported( + "map_key_type".into(), + format!("{self:?}"), + )), + } + } + + #[inline] + pub fn map_value_type(&self) -> Result { + let size = 0; + match self { + Self::Incomplete { + extra: CollectionProperties::Map { value_type, .. }, + .. + } => Ok(Self::synthetic_incomplete(value_type.clone(), size)), + Self::Complete { + property_type: FPropertyTypeName::Map { value, .. }, + .. + } => Ok(Self::synthetic_complete(*value.clone(), size)), + _ => Err(PropertyTagError::Unsupported( + "map_value_type".into(), + format!("{self:?}"), + )), + } + } + + #[inline] + pub fn set_element_tag(&self) -> Result { + let size = 0; + match self { + Self::Incomplete { + extra: CollectionProperties::Set { inner_type }, + .. + } => Ok(Self::synthetic_incomplete(inner_type.clone(), size)), + Self::Complete { + property_type: FPropertyTypeName::Set(e), + .. + } => Ok(Self::synthetic_complete(*e.clone(), size)), + _ => Err(PropertyTagError::Unsupported( + "set_element_tag".into(), + format!("{self:?}"), + )), + } + } + + #[inline] + pub fn property_type_str(&self) -> Result<&str, PropertyTagError> { + match self { + Self::Incomplete { property_type, .. } => property_type.as_deref(), + Self::Complete { property_type, .. } => Some(property_type.name()), + } + .ok_or_else(|| { + PropertyTagError::Unsupported("property_type_str".into(), format!("{self:?}")) + }) + } + + #[inline] + pub fn property_type_name(&self) -> Result { + match self { + Self::Complete { property_type, .. } => Some(property_type.clone()), + Self::Incomplete { + property_type, + extra, + .. + } => FPropertyTypeName::from_incomplete(property_type, extra), + } + .ok_or_else(|| { + PropertyTagError::Unsupported("property_type_name".into(), format!("{self:?}")) + }) + } + + #[inline] + pub fn size(&self) -> u32 { + match self { + Self::Incomplete { size, .. } => *size, + Self::Complete { size, .. } => *size, + } + } + + #[inline] + pub fn array_inner_type(&self) -> Result<&str, PropertyTagError> { + match self { + Self::Incomplete { + extra: CollectionProperties::Array { inner_type, .. }, + .. + } => inner_type.as_deref(), + Self::Complete { + property_type: FPropertyTypeName::Array(inner_type), + .. + } => Some(inner_type.name()), + _ => None, + } + .ok_or_else(|| { + PropertyTagError::Unsupported("array_inner_type".into(), format!("{self:?}")) + }) + } + + #[inline] + pub fn array_struct_type(&self) -> Result<(&str, &str, FGuid), PropertyTagError> { + match &self { + Self::Complete { + property_type: FPropertyTypeName::Array(inner_type), + .. + } => Some(inner_type.as_ref()), + _ => None, + } + .and_then(|inner_type| match inner_type { + FPropertyTypeName::Struct { + type_name: FString(Some(type_name)), + class_name: FString(Some(class_name)), + struct_guid: guid, + } => Some((type_name.as_str(), class_name.as_str(), *guid)), + _ => None, + }) + .ok_or_else(|| { + PropertyTagError::Unsupported("array_struct_type".into(), format!("{self:?}")) + }) + } + + #[inline] + pub fn struct_type_name(&self) -> Option<&str> { + match self { + Self::Incomplete { + extra: CollectionProperties::Struct { type_name, .. }, + .. + } => type_name.as_deref(), + Self::Complete { + property_type: FPropertyTypeName::Struct { type_name, .. }, + .. + } => type_name.as_deref(), + _ => None, + } + } + + #[inline] + pub fn struct_class_name(&self) -> Option<&str> { + match self { + Self::Complete { + property_type: FPropertyTypeName::Struct { class_name, .. }, + .. + } => class_name.as_deref(), + _ => None, + } + } +} + +#[binrw] +#[derive(Clone, Debug, Eq, PartialEq)] +#[br(import(format: &SerializationFormat, property_type: &str))] +pub enum CollectionProperties { + #[br(pre_assert(matches!(property_type, NAME_ARRAY_PROPERTY)))] + Array { + inner_type: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_BOOL_PROPERTY)))] + Bool { + value: u8, + }, + + #[br(pre_assert(matches!(property_type, NAME_BYTE_PROPERTY)))] + Byte { + enum_name: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_ENUM_PROPERTY)))] + Enum { + enum_name: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_MAP_PROPERTY) && format.property_tag_set_map_support()))] + Map { + inner_type: FString, + value_type: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_OPTION_PROPERTY)))] + Option { + inner_type: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_SET_PROPERTY) && format.property_tag_set_map_support()))] + Set { + inner_type: FString, + }, + + #[br(pre_assert(matches!(property_type, NAME_STRUCT_PROPERTY)))] + Struct { + type_name: FString, + struct_guid: FGuid, + }, + + None, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct PropertyTagIncompleteGuid(pub Option); + +impl BinRead for PropertyTagIncompleteGuid { + type Args<'a> = (&'a SerializationFormat, &'a str); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + (format, property_type): Self::Args<'_>, + ) -> binrw::BinResult { + let has_property_guid = if format.property_guid_in_property_tag() { + match u8::read_options(reader, endian, ())? { + 0 => false, + 1 => true, + l => unimplemented!("unsupported length: {l}"), + } + } else { + property_type == NAME_TEXT_PROPERTY + }; + let maybe_guid = has_property_guid + .then(|| FGuid::read_options(reader, endian, ())) + .transpose()?; + Ok(Self(maybe_guid)) + } +} + +impl BinWrite for PropertyTagIncompleteGuid { + type Args<'a> = (&'a SerializationFormat, &'a str); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + (format, property_type): Self::Args<'_>, + ) -> binrw::BinResult<()> { + if format.property_guid_in_property_tag() { + match self.0 { + None => { + let has_property_guid = 0u8; + has_property_guid.write_options(writer, endian, ()) + } + Some(guid) => { + let has_property_guid = 1u8; + has_property_guid.write_options(writer, endian, ())?; + guid.write_options(writer, endian, ()) + } + } + } else { + let guid = FGuid::from(self); + if property_type == NAME_TEXT_PROPERTY { + guid.write_options(writer, endian, ()) + } else { + assert!(!guid.is_valid(), "illegal guid for this property"); + Ok(()) + } + } + } +} + +impl From for PropertyTagIncompleteGuid { + fn from(value: FGuid) -> Self { + Self(value.is_valid().then_some(value)) + } +} + +impl From<&PropertyTagIncompleteGuid> for FGuid { + fn from(value: &PropertyTagIncompleteGuid) -> Self { + value.0.unwrap_or_default() + } +} + +#[derive(Debug, PartialEq)] +pub struct TaggedProperty { + pub property_name: FString, + pub array_index: u32, + pub has_binary_or_native_serialize: bool, + pub has_property_extensions: bool, + pub property: FProperty, + pub property_guid: FGuid, +} + +impl TaggedProperty { + fn new(property_name: FString, property_tag: PropertyTag, property: FProperty) -> Self { + let array_index = property_tag.array_index(); + let property_guid = property_tag.guid(); + let flags = property_tag.flags(); + let (has_binary_or_native_serialize, has_property_extensions) = ( + flags.is_some_and(EPropertyTagFlags::has_binary_or_native_serialize), + flags.is_some_and(EPropertyTagFlags::has_property_extensions), + ); + Self { + property_name, + array_index, + has_binary_or_native_serialize, + has_property_extensions, + property, + property_guid, + } + } +} + +#[derive(PartialEq)] +pub struct TaggedProperties(pub Vec); + +impl BinRead for TaggedProperties { + type Args<'a> = (&'a SerializationFormat,); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + (format,): Self::Args<'_>, + ) -> binrw::BinResult { + let mut properties = Vec::new(); + loop { + match FPropertyTag::read_options(reader, endian, (format,))? { + FPropertyTag::None => break, + FPropertyTag::Some { name, property_tag } => { + let property = + FProperty::read_options(reader, endian, (format, &property_tag))?; + // println!("Read {property:?}"); + let property = TaggedProperty::new(name, property_tag, property); + properties.push(property); + } + } + } + Ok(Self(properties)) + } +} + +impl BinWrite for TaggedProperties { + type Args<'a> = (&'a SerializationFormat,); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + (format,): Self::Args<'_>, + ) -> binrw::BinResult<()> { + for tagged_property in self.iter() { + let TaggedProperty { + property_name, + array_index, + has_binary_or_native_serialize, + has_property_extensions, + property, + property_guid, + } = tagged_property; + // Write to temp buffer + let mut buf = Cursor::new(Vec::new()); + property.write_options(&mut buf, endian, (format,))?; + let property_buf = buf.into_inner(); + let pos = writer.stream_position()?; + let len = property_buf.len(); + let len = u32::try_from(len).map_err(binrw_custom(pos))?; + + // Generate property tag + let property_tag = property + .generate_tag( + format, + len, + *array_index, + *has_binary_or_native_serialize, + *has_property_extensions, + *property_guid, + ) + .map_err(binrw_custom(pos))?; + + // Write tagged property to writer + property_name.write_options(writer, endian, ())?; + property_tag.write_options(writer, endian, (format,))?; + property_buf.write_options(writer, endian, ())?; + } + // Write the sentinel value "None" to terminate the list + FPropertyTag::None.write_options(writer, endian, (format,))?; + Ok(()) + } +} + +impl std::fmt::Debug for TaggedProperties { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("TaggedProperties::from") + .field(&self.0) + .finish() + } +} + +impl std::ops::Deref for TaggedProperties { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for TaggedProperties { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From<[TaggedProperty; N]> for TaggedProperties { + fn from(value: [TaggedProperty; N]) -> Self { + Self(Vec::from(value)) + } +} + +impl From> for TaggedProperties { + fn from(value: Vec) -> Self { + Self(value) + } +} + +#[cfg(test)] +mod test { + + use crate::{ + error::Result, + types::{ + EEditorObjectVersion, EUE5ReleaseStreamObjectVersion, EUnrealEngineObjectUE4Version, + EUnrealEngineObjectUE5Version, + }, + }; + + use super::*; + + const FORMAT_INCOMPLETE: &SerializationFormat = &SerializationFormat::from_enums( + EUnrealEngineObjectUE4Version::OldestLoadablePackage, + None, + EUE5ReleaseStreamObjectVersion::BeforeCustomVersionWasAdded, + EEditorObjectVersion::BeforeCustomVersionWasAdded, + ); + + const FORMAT_COMPLETE: &SerializationFormat = &SerializationFormat::from_enums( + EUnrealEngineObjectUE4Version::AutomaticVersionPlusOne, + Some(EUnrealEngineObjectUE5Version::AutomaticVersionPlusOne), + EUE5ReleaseStreamObjectVersion::AutomaticVersionPlusOne, + EEditorObjectVersion::AutomaticVersionPlusOne, + ); + + fn test_fpropertytag( + tag: FPropertyTag, + expected: &[u8], + format: &SerializationFormat, + ) -> Result<()> { + // Write + let mut buf = Cursor::new(vec![]); + tag.write_le_args(&mut buf, (format,))?; + let buf = buf.into_inner(); + assert_eq!(buf, expected); + + // Read + let mut cursor = Cursor::new(Vec::from(expected)); + let read = FPropertyTag::read_le_args(&mut cursor, (format,))?; + assert_eq!(tag, read); + + Ok(()) + } + + #[test] + fn structproperty_incomplete() -> Result<()> { + test_fpropertytag( + FPropertyTag::Some { + name: FString::from("test"), + property_tag: PropertyTag::Incomplete { + property_type: FString::from(NAME_STRUCT_PROPERTY), + size: 0, + array_index: 0, + extra: CollectionProperties::Struct { + type_name: FString::from("TestClass"), + struct_guid: FGuid::default(), + }, + maybe_property_guid: PropertyTagIncompleteGuid::default(), + }, + }, + &[ + 5, 0, 0, 0, b't', b'e', b's', b't', 0, // name + 15, 0, 0, 0, b'S', b't', b'r', b'u', b'c', b't', b'P', b'r', b'o', b'p', b'e', + b'r', b't', b'y', 0, // type + 0, 0, 0, 0, // size + 0, 0, 0, 0, // array_index + 10, 0, 0, 0, b'T', b'e', b's', b't', b'C', b'l', b'a', b's', b's', + 0, // type_name + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // struct_guid + ], + FORMAT_INCOMPLETE, + ) + } + + #[test] + fn structproperty_complete() -> Result<()> { + test_fpropertytag( + FPropertyTag::Some { + name: FString::from("test"), + property_tag: PropertyTag::Complete { + property_type: FPropertyTypeName::Struct { + type_name: "TestClass".into(), + class_name: "/path".into(), + struct_guid: FGuid::from_u32( + 0xFF000088, 0xEE111199, 0xDD2222AA, 0xCC3333BB, + ), + }, + size: 0, + flags: EPropertyTagFlags::new(), + array_index: 0, + property_guid: FGuid::default(), + }, + }, + &[ + 5, 0, 0, 0, b't', b'e', b's', b't', 0, // name + 15, 0, 0, 0, b'S', b't', b'r', b'u', b'c', b't', b'P', b'r', b'o', b'p', b'e', + b'r', b't', b'y', 0, // type + 2, 0, 0, 0, // root child count + 10, 0, 0, 0, b'T', b'e', b's', b't', b'C', b'l', b'a', b's', b's', + 0, // child 1 name + 1, 0, 0, 0, // child 1 child count + 6, 0, 0, 0, b'/', b'p', b'a', b't', b'h', 0, // child 1.1 name + 0, 0, 0, 0, // child 1.1 child count + 37, 0, 0, 0, b'f', b'f', b'0', b'0', b'0', b'0', b'8', b'8', b'-', b'e', b'e', + b'1', b'1', b'-', b'1', b'1', b'9', b'9', b'-', b'd', b'd', b'2', b'2', b'-', b'2', + b'2', b'a', b'a', b'c', b'c', b'3', b'3', b'3', b'3', b'b', b'b', + 0, // child 2 name + 0, 0, 0, 0, // child 2 child count + 0, 0, 0, 0, // size + 0, // flags + ], + FORMAT_COMPLETE, + ) + } +} diff --git a/src/types/f_property_type_name.rs b/src/types/f_property_type_name.rs new file mode 100644 index 0000000..7e76fd7 --- /dev/null +++ b/src/types/f_property_type_name.rs @@ -0,0 +1,415 @@ +use binrw::{BinRead, BinWrite, binrw}; + +use crate::types::{ + CollectionProperties, FGuid, FString, NAME_ARRAY_PROPERTY, NAME_BOOL_PROPERTY, + NAME_BYTE_PROPERTY, NAME_DELEGATE_PROPERTY, NAME_DOUBLE_PROPERTY, NAME_ENUM_PROPERTY, + NAME_FLOAT_PROPERTY, NAME_INT_PROPERTY, NAME_INT8_PROPERTY, NAME_INT16_PROPERTY, + NAME_INT64_PROPERTY, NAME_MAP_PROPERTY, NAME_MULTICAST_INLINE_DELGATE_PROPERTY, + NAME_MULTICAST_SPARSE_DELGATE_PROPERTY, NAME_NAME_PROPERTY, NAME_OBJECT_PROPERTY, + NAME_OPTION_PROPERTY, NAME_SET_PROPERTY, NAME_SOFT_OBJECT_PROPERTY, NAME_STR_PROPERTY, + NAME_STRUCT_PROPERTY, NAME_TEXT_PROPERTY, NAME_UINT16_PROPERTY, NAME_UINT32_PROPERTY, + NAME_UINT64_PROPERTY, TArray, +}; + +#[binrw] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RawPropertyTypeName { + name: FString, + children: TArray, +} + +impl RawPropertyTypeName { + #[inline] + fn from_name(name: impl Into) -> Self { + Self { + name: name.into(), + children: TArray::empty(), + } + } + + #[inline] + fn with_children(name: impl Into, children: impl IntoIterator) -> Self { + Self { + name: name.into(), + children: TArray(children.into_iter().collect()), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum FPropertyTypeName { + Array(Box), + Bool, + Byte(Option), + Delegate, + Double, + Enum { + enum_class: FString, + class_path: Option, + inner_type: Option>, + }, + Float, + Int, + Int8, + Int16, + Int64, + Map { + key: Box, + value: Box, + }, + MulticastInlineDelegate, + MulticastSparseDelegate, + Name, + Object, + Set(Box), + SoftObject, + Str, + Struct { + type_name: FString, + class_name: FString, + struct_guid: FGuid, + }, + Text, + UInt16, + UInt32, + UInt64, + Unknown(RawPropertyTypeName), +} + +impl FPropertyTypeName { + pub fn from_incomplete(name: &FString, extra: &CollectionProperties) -> Option { + let result = match extra { + CollectionProperties::Array { inner_type } => { + let inner_type = Self::from_name(inner_type.clone())?; + Self::Array(Box::new(inner_type)) + } + CollectionProperties::Byte { enum_name } => Self::Byte(match enum_name.is_none() { + true => None, + false => Some(enum_name.clone()), + }), + CollectionProperties::Enum { enum_name } => Self::Enum { + enum_class: enum_name.clone(), + class_path: None, + inner_type: None, + }, + CollectionProperties::Map { + inner_type, + value_type, + } => Self::Map { + key: Box::new(Self::from_name(inner_type.clone())?), + value: Box::new(Self::from_name(value_type.clone())?), + }, + CollectionProperties::Option { inner_type } => { + Self::Unknown(RawPropertyTypeName::with_children( + NAME_OPTION_PROPERTY, + [RawPropertyTypeName::from_name(inner_type.clone())], + )) + } + CollectionProperties::Set { inner_type } => { + Self::Set(Box::new(Self::from_name(inner_type.clone())?)) + } + CollectionProperties::Struct { + type_name, + struct_guid, + } => Self::Struct { + type_name: type_name.clone(), + class_name: FString(None), + struct_guid: *struct_guid, + }, + CollectionProperties::Bool { .. } | CollectionProperties::None => { + Self::from_name(name.clone())? + } + }; + Some(result) + } + + pub fn from_name(name: impl Into) -> Option { + Self::from_raw(RawPropertyTypeName::from_name(name)) + } + + pub fn with_children( + name: impl Into, + children: impl IntoIterator, + ) -> Option { + let children = children.into_iter().map(Self::into_raw); + Self::from_raw(RawPropertyTypeName::with_children(name, children)) + } + + fn from_raw(raw: RawPropertyTypeName) -> Option { + let RawPropertyTypeName { name, children } = raw; + match (name.as_deref()?, children.len()) { + (NAME_ARRAY_PROPERTY, 1) => { + let [inner] = children.0.try_into().ok()?; + let inner = Self::from_raw(inner)?; + Some(Self::Array(Box::new(inner))) + } + (NAME_BOOL_PROPERTY, 0) => Some(Self::Bool), + (NAME_BYTE_PROPERTY, 0) => Some(Self::Byte(None)), + (NAME_BYTE_PROPERTY, 1) => { + let [enum_node] = children.0.try_into().ok()?; + enum_node + .children + .is_empty() + .then_some(Self::Byte(Some(enum_node.name))) + } + (NAME_DELEGATE_PROPERTY, 0) => Some(Self::Delegate), + (NAME_DOUBLE_PROPERTY, 0) => Some(Self::Double), + (NAME_ENUM_PROPERTY, 1 | 2) => Self::from_raw_enum(children), + (NAME_FLOAT_PROPERTY, 0) => Some(Self::Float), + (NAME_INT_PROPERTY, 0) => Some(Self::Int), + (NAME_INT8_PROPERTY, 0) => Some(Self::Int8), + (NAME_INT16_PROPERTY, 0) => Some(Self::Int16), + (NAME_INT64_PROPERTY, 0) => Some(Self::Int64), + (NAME_MAP_PROPERTY, 2) => { + let [key, value] = children.0.try_into().ok()?; + let key = Box::new(Self::from_raw(key)?); + let value = Box::new(Self::from_raw(value)?); + Some(Self::Map { key, value }) + } + (NAME_MULTICAST_INLINE_DELGATE_PROPERTY, 0) => Some(Self::MulticastInlineDelegate), + (NAME_MULTICAST_SPARSE_DELGATE_PROPERTY, 0) => Some(Self::MulticastSparseDelegate), + (NAME_NAME_PROPERTY, 0) => Some(Self::Name), + (NAME_OBJECT_PROPERTY, 0) => Some(Self::Object), + (NAME_SET_PROPERTY, 1) => { + let [inner] = children.0.try_into().ok()?; + let inner = Self::from_raw(inner)?; + Some(Self::Set(Box::new(inner))) + } + (NAME_SOFT_OBJECT_PROPERTY, 0) => Some(Self::SoftObject), + (NAME_STR_PROPERTY, 0) => Some(Self::Str), + (NAME_STRUCT_PROPERTY, 1) => { + let [type_node] = children.0.try_into().ok()?; + Self::from_raw_struct(type_node, None) + } + (NAME_STRUCT_PROPERTY, 2) => { + let [type_node, guid_node] = children.0.try_into().ok()?; + Self::from_raw_struct(type_node, Some(guid_node)) + } + (NAME_TEXT_PROPERTY, 0) => Some(Self::Text), + (NAME_UINT16_PROPERTY, 0) => Some(Self::UInt16), + (NAME_UINT32_PROPERTY, 0) => Some(Self::UInt32), + (NAME_UINT64_PROPERTY, 0) => Some(Self::UInt64), + _ => Some(Self::Unknown(RawPropertyTypeName { name, children })), + } + } + + fn from_raw_enum(children: impl IntoIterator) -> Option { + let mut children = children.into_iter(); + let enum_node = children.next()?; + let inner_type = children.next().and_then(Self::from_raw).map(Box::new); + if children.next().is_some() { + return None; + } + + let mut enum_children = enum_node.children.into_iter(); + let class_path = match enum_children.next() { + None => None, + Some(class_path) if class_path.children.is_empty() => Some(class_path.name), + Some(_) => return None, + }; + + if enum_children.next().is_some() { + return None; + } + + Some(Self::Enum { + enum_class: enum_node.name, + class_path, + inner_type, + }) + } + + fn from_raw_struct( + type_node: RawPropertyTypeName, + guid_node: Option, + ) -> Option { + /// Returns `Some(())` if `node.children.is_empty()` and `None` otherwise. + fn no_children(node: &RawPropertyTypeName) -> Option<()> { + node.children.is_empty().then_some(()) + } + + let [class_node] = type_node.children.0.try_into().ok()?; + no_children(&class_node)?; + + let struct_guid = if let Some(guid_node) = guid_node { + no_children(&guid_node)?; + guid_node.name.as_deref()?.parse().ok()? + } else { + FGuid::default() + }; + + Some(Self::Struct { + type_name: type_node.name, + class_name: class_node.name, + struct_guid, + }) + } + + fn into_raw(self) -> RawPropertyTypeName { + // zero children + fn zero(name: impl Into) -> RawPropertyTypeName { + RawPropertyTypeName::from_name(name) + } + // one child + fn one( + name: impl Into, + child: impl Into, + ) -> RawPropertyTypeName { + RawPropertyTypeName::with_children(name, [child.into()]) + } + // two children + fn two( + name: impl Into, + one: impl Into, + two: impl Into, + ) -> RawPropertyTypeName { + RawPropertyTypeName::with_children(name, [one.into(), two.into()]) + } + + match self { + Self::Array(i) => one(NAME_ARRAY_PROPERTY, i.into_raw()), + Self::Bool => zero(NAME_BOOL_PROPERTY), + Self::Byte(None) => zero(NAME_BYTE_PROPERTY), + Self::Byte(Some(i)) => one(NAME_BYTE_PROPERTY, zero(i)), + Self::Delegate => zero(NAME_DELEGATE_PROPERTY), + Self::Double => zero(NAME_DOUBLE_PROPERTY), + Self::Enum { + enum_class, + class_path, + inner_type, + } => { + let child1 = RawPropertyTypeName::with_children(enum_class, class_path.map(zero)); + let maybe_child2 = inner_type.map(|boxed| (*boxed).into_raw()); + let children = std::iter::once(child1).chain(maybe_child2); + RawPropertyTypeName::with_children(NAME_ENUM_PROPERTY, children) + } + Self::Float => zero(NAME_FLOAT_PROPERTY), + Self::Int => zero(NAME_INT_PROPERTY), + Self::Int8 => zero(NAME_INT8_PROPERTY), + Self::Int16 => zero(NAME_INT16_PROPERTY), + Self::Int64 => zero(NAME_INT64_PROPERTY), + Self::Map { key, value } => two(NAME_MAP_PROPERTY, key.into_raw(), value.into_raw()), + Self::MulticastInlineDelegate => zero(NAME_MULTICAST_INLINE_DELGATE_PROPERTY), + Self::MulticastSparseDelegate => zero(NAME_MULTICAST_SPARSE_DELGATE_PROPERTY), + Self::Name => zero(NAME_NAME_PROPERTY), + Self::Object => zero(NAME_OBJECT_PROPERTY), + Self::Set(i) => one(NAME_SET_PROPERTY, i.into_raw()), + Self::SoftObject => zero(NAME_SOFT_OBJECT_PROPERTY), + Self::Str => zero(NAME_STR_PROPERTY), + Self::Struct { + type_name, + class_name, + struct_guid, + } => { + let type_node = RawPropertyTypeName { + name: type_name, + children: TArray(vec![RawPropertyTypeName { + name: class_name, + children: TArray::empty(), + }]), + }; + + if struct_guid.is_valid() { + two( + NAME_STRUCT_PROPERTY, + type_node, + zero(struct_guid.to_string()), + ) + } else { + one(NAME_STRUCT_PROPERTY, type_node) + } + } + Self::Text => zero(NAME_TEXT_PROPERTY), + Self::UInt16 => zero(NAME_UINT16_PROPERTY), + Self::UInt32 => zero(NAME_UINT32_PROPERTY), + Self::UInt64 => zero(NAME_UINT64_PROPERTY), + Self::Unknown(raw) => raw, + } + } + + pub fn enum_class_name(&self) -> &FString { + match self { + Self::Byte(Some(enum_class)) | Self::Enum { enum_class, .. } => enum_class, + _ => todo!("FPropertyTypeName::enum_class_name({self:?})"), + } + } + + pub fn name(&self) -> &str { + match self { + Self::Array(_) => NAME_ARRAY_PROPERTY, + Self::Bool => NAME_BOOL_PROPERTY, + Self::Byte(_) => NAME_BYTE_PROPERTY, + Self::Delegate => NAME_DELEGATE_PROPERTY, + Self::Double => NAME_DOUBLE_PROPERTY, + Self::Enum { .. } => NAME_ENUM_PROPERTY, + Self::Float => NAME_FLOAT_PROPERTY, + Self::Int => NAME_INT_PROPERTY, + Self::Int8 => NAME_INT8_PROPERTY, + Self::Int16 => NAME_INT16_PROPERTY, + Self::Int64 => NAME_INT64_PROPERTY, + Self::Map { .. } => NAME_MAP_PROPERTY, + Self::MulticastInlineDelegate => NAME_MULTICAST_INLINE_DELGATE_PROPERTY, + Self::MulticastSparseDelegate => NAME_MULTICAST_SPARSE_DELGATE_PROPERTY, + Self::Name => NAME_NAME_PROPERTY, + Self::Object => NAME_OBJECT_PROPERTY, + Self::Set(_) => NAME_SET_PROPERTY, + Self::SoftObject => NAME_SOFT_OBJECT_PROPERTY, + Self::Str => NAME_STR_PROPERTY, + Self::Struct { .. } => NAME_STRUCT_PROPERTY, + Self::Text => NAME_TEXT_PROPERTY, + Self::UInt16 => NAME_UINT16_PROPERTY, + Self::UInt32 => NAME_UINT32_PROPERTY, + Self::UInt64 => NAME_UINT64_PROPERTY, + Self::Unknown(RawPropertyTypeName { + name: FString(Some(name)), + .. + }) => name, + Self::Unknown(RawPropertyTypeName { .. }) => todo!("{self:?}"), + } + } + + pub fn struct_guid(&self) -> Option { + match self { + Self::Struct { struct_guid, .. } => Some(*struct_guid), + _ => None, + } + } + + pub fn enum_type(&self) -> Option<&Self> { + match self { + Self::Array(inner) | Self::Set(inner) => inner.enum_type(), + Self::Byte(Some(_)) | Self::Enum { .. } => Some(self), + _ => todo!("{self:?}"), + } + } +} + +impl BinRead for FPropertyTypeName { + type Args<'a> = (); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult { + let pos = reader.stream_position()?; + let raw = RawPropertyTypeName::read_options(reader, endian, args)?; + Self::from_raw(raw).ok_or_else(|| binrw::Error::AssertFail { + pos, + message: "invalid FPropertyTypeName".to_string(), + }) + } +} + +impl BinWrite for FPropertyTypeName { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + self.clone().into_raw().write_options(writer, endian, args) + } +} diff --git a/src/types/f_save_game_header.rs b/src/types/f_save_game_header.rs new file mode 100644 index 0000000..84f48c0 --- /dev/null +++ b/src/types/f_save_game_header.rs @@ -0,0 +1,86 @@ +use binrw::binrw; + +use crate::types::{FCustomVersionContainer, FEngineVersion, FPackageFileVersion, FString}; + +#[binrw] +#[brw(repr = u32)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum SaveGameFileVersion { + InitialVersion = 1, + // serializing custom versions into the savegame data to handle that type of versioning + AddedCustomVersions = 2, + // added a new UE5 version number to FPackageFileSummary + PackageFileSummaryVersionChange = 3, +} + +#[binrw] +#[brw(little, magic = b"GVAS")] +#[derive(Debug, PartialEq)] +pub struct FSaveGameHeader { + #[br(temp)] + #[bw(calc = self.save_game_file_version())] + pub save_game_file_version: u32, //SaveGameFileVersion, + + #[br(args(save_game_file_version))] + pub package_file_version: FPackageFileVersion, + + pub engine_version: FEngineVersion, + + #[brw(if(save_game_file_version >= SaveGameFileVersion::AddedCustomVersions as u32))] + pub custom_versions: Option, + + pub save_game_class_name: FString, +} + +impl FSaveGameHeader { + fn save_game_file_version(&self) -> u32 { + if let FPackageFileVersion::UE5 { .. } = self.package_file_version { + SaveGameFileVersion::PackageFileSummaryVersionChange as u32 + } else if self.custom_versions.is_some() { + SaveGameFileVersion::AddedCustomVersions as u32 + } else { + SaveGameFileVersion::InitialVersion as u32 + } + } +} + +#[cfg(test)] +mod test { + use std::io::{Read, Seek}; + use std::{fs::File, io::Cursor}; + + use binrw::{BinRead, BinWrite}; + + use crate::{error::Result, test::common::ALL_TEST_PATHS, types::FSaveGameHeader}; + + #[test] + fn test_save_game_header() -> Result<()> { + for path in ALL_TEST_PATHS { + // Open + let mut file = File::open(path)?; + + // Read + let mut buf = Vec::new(); + let _len = file.read_to_end(&mut buf)?; + + // Parse + let mut cursor = Cursor::new(buf); + let result = FSaveGameHeader::read(&mut cursor)?; + + // Write + let len = cursor.stream_position()?; + let len = usize::try_from(len)?; + let buf2 = vec![0u8; len]; + let mut cursor2 = Cursor::new(buf2); + FSaveGameHeader::write(&result, &mut cursor2)?; + + // Compare + let buf = cursor.into_inner(); + let buf2 = cursor2.into_inner(); + assert_eq!(&buf[..buf2.len()], &buf2[..]); + } + + // Success + Ok(()) + } +} diff --git a/src/types/f_set_property.rs b/src/types/f_set_property.rs new file mode 100644 index 0000000..ae15435 --- /dev/null +++ b/src/types/f_set_property.rs @@ -0,0 +1,28 @@ +use binrw::binrw; + +use crate::{ + format::SerializationFormat, + types::{FProperty, PropertyTag, TArray}, +}; + +#[binrw] +#[br(import(format: &SerializationFormat, t: &PropertyTag))] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct FSetProperty { + allocation_flags: u32, + #[br(try_calc = t.set_element_tag())] + #[bw(ignore)] + element_type: PropertyTag, + #[br(args(format, &element_type))] + #[bw(args(format))] + properties: TArray, +} + +impl FSetProperty { + pub(crate) fn element_property_type_name(&self) -> &str { + self.element_type + .property_type_str() + .unwrap_or_else(|_| todo!("{:?}", self.element_type)) + } +} diff --git a/src/types/f_soft_object_property.rs b/src/types/f_soft_object_property.rs new file mode 100644 index 0000000..c99dc08 --- /dev/null +++ b/src/types/f_soft_object_property.rs @@ -0,0 +1,13 @@ +use binrw::binrw; + +use crate::{format::SerializationFormat, types::FString}; + +#[binrw] +#[br(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FSoftObjectProperty { + #[br(pre_assert(!format.fsoftobjectpath_remove_asset_path_fnames()))] + Old(FString, FString), + #[br(pre_assert(format.fsoftobjectpath_remove_asset_path_fnames()))] + New(FString, FString, FString), +} diff --git a/src/types/f_string.rs b/src/types/f_string.rs new file mode 100644 index 0000000..0c38581 --- /dev/null +++ b/src/types/f_string.rs @@ -0,0 +1,263 @@ +use binrw::{BinRead, BinWrite}; + +use crate::error::binrw_custom; + +#[derive(Clone, Eq, Hash, PartialEq)] +pub struct FString(pub Option); + +impl std::fmt::Debug for FString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + None => f.write_str("FString(None)"), + Some(s) => write!(f, "FString::from({s:?})"), + } + } +} + +impl std::fmt::Display for FString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.as_deref() { + None => f.write_str("null"), + Some(s) => std::fmt::Display::fmt(s, f), + } + } +} + +impl BinRead for FString { + type Args<'a> = (); + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + _args: Self::Args<'_>, + ) -> binrw::BinResult { + let pos = reader.stream_position()?; + let length = i32::read_options(reader, endian, ())?; + if !(-0x20000..0x20000).contains(&length) { + Err(binrw::Error::AssertFail { + pos, + message: format!("Invalid FString length 0x{length:x}"), + })?; + } + let value = if length == 0 { + None + } else if length > 0 { + let count = usize::try_from(length - 1).map_err(binrw_custom(pos))?; + let mut buf = vec![0u8; count]; + reader.read_exact(&mut buf)?; + + let terminator = u8::read_options(reader, endian, ())?; + if terminator != 0 { + Err(binrw::Error::AssertFail { + pos, + message: format!("Invalid terminator value for string length {count}"), + })?; + } + + let str = String::from_utf8(buf).map_err(binrw_custom(pos))?; + Some(str) + } else { + let count = usize::try_from(-length - 1).map_err(binrw_custom(pos))?; + let mut bytes = vec![0u8; count * 2]; + reader.read_exact(&mut bytes)?; + + let terminator = u16::read_options(reader, endian, ())?; + if terminator != 0 { + Err(binrw::Error::AssertFail { + pos, + message: format!("Invalid terminator value for string length {count}"), + })?; + } + + let buf: Vec = bytes + .as_chunks::<2>() + .0 + .iter() + .copied() + .map(match endian { + binrw::Endian::Big => u16::from_be_bytes, + binrw::Endian::Little => u16::from_le_bytes, + }) + .collect(); + let str = String::from_utf16(&buf).map_err(binrw_custom(pos))?; + Some(str) + }; + Ok(Self(value)) + } +} + +impl BinWrite for FString { + type Args<'a> = (); + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + _args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match self.as_deref() { + None => u32::write_options(&0, writer, endian, ())?, + Some(str) => { + let pos = writer.stream_position()?; + let err_convert = binrw_custom(pos); + if str.is_ascii() { + // ASCII strings do not require encoding + let len = i32::try_from(str.len() + 1).map_err(err_convert)?; + i32::write_options(&len, writer, endian, ())?; + let _ = writer.write(str.as_bytes())?; + let _ = writer.write(&[0u8; 1])?; + } else { + // Perform UTF-16 encoding when non-ASCII characters are detected + let words: Vec = str.encode_utf16().collect(); + let len = -i32::try_from(words.len() + 1).map_err(err_convert)?; + i32::write_options(&len, writer, endian, ())?; + for word in words { + u16::write_options(&word, writer, endian, ())?; + } + u16::write_options(&0, writer, endian, ())?; + } + } + } + Ok(()) + } +} + +impl std::ops::Deref for FString { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for FString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for FString { + #[inline] + fn from(value: String) -> Self { + Self(Some(value)) + } +} + +impl From> for FString { + #[inline] + fn from(value: Option) -> Self { + Self(value) + } +} + +impl From<&str> for FString { + #[inline] + fn from(value: &str) -> Self { + Self(Some(value.to_owned())) + } +} + +impl From> for FString { + #[inline] + fn from(value: Option<&str>) -> Self { + Self(value.map(ToOwned::to_owned)) + } +} + +impl PartialEq for FString { + fn eq(&self, other: &str) -> bool { + self.as_deref() == Some(other) + } +} + +impl PartialEq<&str> for FString { + fn eq(&self, other: &&str) -> bool { + self == *other + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use crate::error::Result; + + use super::*; + + const BYTES_NULL: &[u8; 4] = b"\x00\x00\x00\x00"; + const BYTES_EMPTY: &[u8; 5] = b"\x01\x00\x00\x00\x00"; + const BYTES_PROPERTY: &[u8; 16] = b"\x0c\x00\x00\x00StrProperty\x00"; + const BYTES_UTF16: &[u8; 8] = b"\xfe\xff\xff\xff\xa7\x00\x00\x00"; + + const STR_EMPTY: &str = ""; + const STR_PROPERTY: &str = "StrProperty"; + const STR_UTF16: &str = "§"; + + #[test] + fn read_fstring_null() -> Result<()> { + let mut cursor = Cursor::new(BYTES_NULL); + let string = FString::read_le(&mut cursor)?; + assert_eq!(string, FString(None)); + Ok(()) + } + + #[test] + fn read_fstring_empty() -> Result<()> { + let mut cursor = Cursor::new(BYTES_EMPTY); + let string = FString::read_le(&mut cursor)?; + assert_eq!(string, FString::from(STR_EMPTY)); + Ok(()) + } + + #[test] + fn read_fstring_ascii() -> Result<()> { + let mut cursor = Cursor::new(BYTES_PROPERTY); + let string = FString::read_le(&mut cursor)?; + assert_eq!(string, FString::from(STR_PROPERTY)); + Ok(()) + } + + #[test] + fn read_fstring_utf16() -> Result<()> { + let mut cursor = Cursor::new(BYTES_UTF16); + let string = FString::read_le(&mut cursor)?; + assert_eq!(string, FString::from(STR_UTF16)); + Ok(()) + } + + #[test] + fn write_fstring_null() -> Result<()> { + let mut cursor = Cursor::new(vec![]); + let string = FString(None); + string.write_le(&mut cursor)?; + assert_eq!(cursor.into_inner().as_slice(), BYTES_NULL); + Ok(()) + } + + #[test] + fn write_fstring_empty() -> Result<()> { + let mut cursor = Cursor::new(vec![]); + let string = FString::from(STR_EMPTY); + string.write_le(&mut cursor)?; + assert_eq!(cursor.into_inner().as_slice(), BYTES_EMPTY); + Ok(()) + } + + #[test] + fn write_fstring_ascii() -> Result<()> { + let mut cursor = Cursor::new(vec![]); + let string = FString::from(STR_PROPERTY); + string.write_le(&mut cursor)?; + assert_eq!(cursor.into_inner().as_slice(), BYTES_PROPERTY); + Ok(()) + } + + #[test] + fn write_fstring_utf16() -> Result<()> { + let mut cursor = Cursor::new(vec![]); + let string = FString::from(STR_UTF16); + string.write_le(&mut cursor)?; + assert_eq!(cursor.into_inner().as_slice(), BYTES_UTF16); + Ok(()) + } +} diff --git a/src/types/f_struct_property.rs b/src/types/f_struct_property.rs new file mode 100644 index 0000000..508534a --- /dev/null +++ b/src/types/f_struct_property.rs @@ -0,0 +1,112 @@ +use binrw::binrw; + +use crate::{ + format::SerializationFormat, + types::{ + FDateTime, FGameplayTagContainer, FGuid, FIntPoint, FLinearColor, FQuat, FRotator, FString, + FTimespan, FVector, FVector2D, NAME_DATE_TIME, NAME_GAMEPLAY_TAG_CONTAINER, NAME_GUID, + NAME_INT_POINT, NAME_LINEAR_COLOR, NAME_QUAT, NAME_ROTATOR, NAME_TIMESPAN, NAME_VECTOR, + NAME_VECTOR2D, PATH__SCRIPT__CORE_U_OBJECT, TaggedProperties, + }, +}; + +#[binrw] +#[br(import(format: &SerializationFormat, size: Option, struct_type: &str, class_name: Option<&str>, guid: FGuid))] +#[bw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FStructProperty { + #[br(pre_assert(struct_type == NAME_DATE_TIME))] + DateTime(FDateTime), + #[br(pre_assert(struct_type == NAME_GAMEPLAY_TAG_CONTAINER))] + GameplayTagContainer(FGameplayTagContainer), + #[br(pre_assert(struct_type == NAME_GUID))] + Guid(FGuid), + #[br(pre_assert(struct_type == NAME_INT_POINT))] + IntPoint(FIntPoint), + #[br(pre_assert(struct_type == NAME_LINEAR_COLOR))] + LinearColor(FLinearColor), + #[br(pre_assert(struct_type == NAME_QUAT))] + Quat(#[br(args(format))] FQuat), + #[br(pre_assert(struct_type == NAME_ROTATOR))] + Rotator(#[br(args(format))] FRotator), + #[br(pre_assert(struct_type == NAME_TIMESPAN))] + Timespan(FTimespan), + #[br(pre_assert(struct_type == NAME_VECTOR))] + Vector(#[br(args(format))] FVector), + #[br(pre_assert(struct_type == NAME_VECTOR2D))] + Vector2D(FVector2D), + Custom { + #[br(calc = struct_type.into())] + #[bw(ignore)] + struct_type: FString, + #[br(calc = class_name.into())] + #[bw(ignore)] + class_name: FString, + #[br(calc = guid)] + #[bw(ignore)] + struct_guid: FGuid, + #[brw(args(format))] + properties: TaggedProperties, + }, + Unknown { + #[br(calc = struct_type.into())] + #[bw(ignore)] + struct_type: FString, + #[br(calc = class_name.into())] + #[bw(ignore)] + class_name: FString, + #[br(calc = guid)] + #[bw(ignore)] + struct_guid: FGuid, + #[br(count = size.unwrap_or(0))] + data: Vec, + }, +} + +impl FStructProperty { + #[inline] + pub fn struct_type(&self) -> FString { + FString::from(match self { + Self::DateTime(..) => Some(NAME_DATE_TIME), + Self::GameplayTagContainer(..) => Some(NAME_GAMEPLAY_TAG_CONTAINER), + Self::Guid(..) => Some(NAME_GUID), + Self::IntPoint(..) => Some(NAME_INT_POINT), + Self::LinearColor(..) => Some(NAME_LINEAR_COLOR), + Self::Quat(..) => Some(NAME_QUAT), + Self::Rotator(..) => Some(NAME_ROTATOR), + Self::Timespan(..) => Some(NAME_TIMESPAN), + Self::Vector(..) => Some(NAME_VECTOR), + Self::Vector2D(..) => Some(NAME_VECTOR2D), + Self::Unknown { struct_type, .. } | Self::Custom { struct_type, .. } => { + struct_type.as_deref() + } + }) + } + + #[inline] + pub fn struct_class(&self) -> FString { + match self { + Self::DateTime(..) + | Self::GameplayTagContainer(..) + | Self::Guid(..) + | Self::IntPoint(..) + | Self::LinearColor(..) + | Self::Quat(..) + | Self::Rotator(..) + | Self::Timespan(..) + | Self::Vector(..) + | Self::Vector2D(..) => FString::from(PATH__SCRIPT__CORE_U_OBJECT), + Self::Unknown { class_name, .. } | Self::Custom { class_name, .. } => { + class_name.clone() + } + } + } + + #[inline] + pub fn struct_guid(&self) -> FGuid { + match self { + Self::Unknown { struct_guid, .. } | Self::Custom { struct_guid, .. } => *struct_guid, + _ => FGuid::default(), + } + } +} diff --git a/src/types/f_text.rs b/src/types/f_text.rs new file mode 100644 index 0000000..761000e --- /dev/null +++ b/src/types/f_text.rs @@ -0,0 +1,126 @@ +use binrw::binrw; + +use crate::{ + format::SerializationFormat, + types::{FString, TArray, TOptional}, +}; + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct FText { + flags: u32, + // #[br(dbg)] + #[brw(args(format))] + history: FTextHistory, +} + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FTextHistory { + // #[brw(magic = -1i8)] + // #[br(pre_assert(!format.culture_invariant_stability))] + // Empty(), + #[brw(magic = -1i8)] + // #[br(pre_assert(format.culture_invariant_stability))] + None(#[br(args(format))] FTextHistoryNone), + #[brw(magic = 0i8)] + Base(FString, FString, FString), + // #[brw(magic = 1i8)] NamedFormat(Box, TArray<(FString, FormatArgumentValue)>), + // #[brw(magic = 2i8)] OrderedFormat(Box, TArray), + #[brw(magic = 3i8)] + ArgumentFormat( + #[brw(args(format))] Box, + #[brw(args(format))] TArray, + ), + #[brw(magic = 4i8)] + AsNumber( + #[brw(args(format))] Box, + #[brw(args(format))] TOptional, + FString, + ), + // #[brw(magic = 5i8)] AsPercent(FormatArgumentValue, TOptional, FString), + // #[brw(magic = 6i8)] AsCurrency(FString, FormatArgumentValue, TOptional, FString), + // #[brw(magic = 7i8)] AsDate(FDateTime, DateTimeStyle, #[brw(if(format.ftext_history_date_timezone))] FString, FString), + // #[brw(magic = 8i8)] AsTime(FDateTime, EDateTimeStyle, FString, FString), + // #[brw(magic = 9i8)] AsDateTime(FDateTime, DateTimeStyle, DateTimeStyle, FString, FString), + // #[brw(magic = 10i8)] Transform(Box, TransformType), + #[brw(magic = 11i8)] + StringTableEntry(FString, FString), + // #[brw(magic = 12i8)] TextGenerator(), + // #[brw(magic = 13i8)] RawText(), +} + +#[binrw] +#[br(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub enum FTextHistoryNone { + #[br(pre_assert(!format.culture_invariant_stability()))] + Old(), + #[br(pre_assert(format.culture_invariant_stability()))] + New(TOptional), +} + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct ArgumentFormatEntry(FString, #[brw(args(format))] FormatArgumentValue); + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +#[rustfmt::skip] +pub enum FormatArgumentValue { + #[brw(magic = 0i8)] Int(#[br(args(format))] FormatArgumentValueInt), + #[brw(magic = 1i8)] UInt(#[br(args(format))] FormatArgumentValueUInt), + #[brw(magic = 2i8)] Float(f32), + #[brw(magic = 3i8)] Double(f64), + #[brw(magic = 4i8)] Text(#[brw(args(format))] FText), + #[brw(magic = 5i8)] UI(i32), +} + +#[binrw] +#[br(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +#[rustfmt::skip] +pub enum FormatArgumentValueInt { + #[br(pre_assert(!format.text_64bit_support()))] Int32(i32), + #[br(pre_assert(format.text_64bit_support()))] Int64(i64), +} + +#[binrw] +#[br(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +#[rustfmt::skip] +pub enum FormatArgumentValueUInt { + #[br(pre_assert(!format.text_64bit_support()))] UInt32(u32), + #[br(pre_assert(format.text_64bit_support()))] UInt64(u64), +} + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct NumberFormattingOptions { + #[brw(if(format.include_always_sign()))] + always_sign: i32, + use_grouping: i32, + roudning_mode: RoundingMode, + minimum_integral_digits: i32, + maximum_integral_digits: i32, + minimum_fractional_digits: i32, + maximum_fractional_digits: i32, +} + +#[binrw] +#[brw(repr(i8))] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum RoundingMode { + HalfToEven, + HalfFromZero, + HalfToZero, + FromZero, + ToZero, + ToNegativeInfinity, + ToPositiveInfinity, +} diff --git a/src/types/f_text_property.rs b/src/types/f_text_property.rs new file mode 100644 index 0000000..cc91374 --- /dev/null +++ b/src/types/f_text_property.rs @@ -0,0 +1,8 @@ +use binrw::binrw; + +use crate::{format::SerializationFormat, types::FText}; + +#[binrw] +#[brw(import(format: &SerializationFormat))] +#[derive(Debug, PartialEq)] +pub struct FTextProperty(#[brw(args(format))] pub FText); diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..21e744e --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,174 @@ +//! Serializable Unreal Engine structs. + +mod e_editor_object_version; +mod e_property_tag_flags; +mod e_ue5_release_stream_object_version; +mod e_unreal_engine_object_ue4_version; +mod e_unreal_engine_object_ue5_version; +mod f_array_property; +mod f_bool_property; +mod f_byte_property; +mod f_custom_version_container; +mod f_date_time; +mod f_enum_property; +mod f_guid; +mod f_map_property; +mod f_package_file_version; +mod f_property; +mod f_property_tag; +mod f_property_type_name; +mod f_save_game_header; +mod f_set_property; +mod f_soft_object_property; +mod f_string; +mod f_struct_property; +mod f_text; +mod f_text_property; +mod t_array; +mod t_optional; +mod u_save_game; + +use binrw::binrw; + +use crate::format::SerializationFormat; +pub use crate::types::{ + e_editor_object_version::EEditorObjectVersion, + e_property_tag_flags::EPropertyTagFlags, + e_ue5_release_stream_object_version::EUE5ReleaseStreamObjectVersion, + e_unreal_engine_object_ue4_version::EUnrealEngineObjectUE4Version, + e_unreal_engine_object_ue5_version::EUnrealEngineObjectUE5Version, + f_array_property::FArrayProperty, + f_bool_property::FBoolProperty, + f_byte_property::FByteProperty, + f_custom_version_container::{CustomVersion, FCustomVersionArray, FCustomVersionContainer}, + f_enum_property::FEnumProperty, + f_guid::FGuid, + f_map_property::{FMapProperty, MapEntry}, + f_package_file_version::FPackageFileVersion, + f_property::FProperty, + f_property_tag::{ + CollectionProperties, FPropertyTag, PropertyTag, PropertyTagIncompleteGuid, + TaggedProperties, TaggedProperty, + }, + f_property_type_name::FPropertyTypeName, + f_save_game_header::{FSaveGameHeader, SaveGameFileVersion}, + f_set_property::FSetProperty, + f_soft_object_property::FSoftObjectProperty, + f_string::FString, + f_struct_property::FStructProperty, + f_text::FText, + f_text_property::FTextProperty, + t_array::TArray, + t_optional::TOptional, + u_save_game::USaveGame, +}; + +pub const NAME_NONE: &str = "None"; + +pub const NAME_ARRAY_PROPERTY: &str = "ArrayProperty"; +pub const NAME_BOOL_PROPERTY: &str = "BoolProperty"; +pub const NAME_BYTE_PROPERTY: &str = "ByteProperty"; +pub const NAME_DELEGATE_PROPERTY: &str = "DelegateProperty"; +pub const NAME_DOUBLE_PROPERTY: &str = "DoubleProperty"; +pub const NAME_ENUM_PROPERTY: &str = "EnumProperty"; +pub const NAME_FIELD_PATH_PROPERTY: &str = "FieldPathProperty"; +pub const NAME_FLOAT_PROPERTY: &str = "FloatProperty"; +pub const NAME_INT16_PROPERTY: &str = "Int16Property"; +pub const NAME_INT64_PROPERTY: &str = "Int64Property"; +pub const NAME_INT8_PROPERTY: &str = "Int8Property"; +pub const NAME_INT_PROPERTY: &str = "IntProperty"; +pub const NAME_MAP_PROPERTY: &str = "MapProperty"; +pub const NAME_MULTICAST_INLINE_DELGATE_PROPERTY: &str = "MulticastInlineDelegateProperty"; +pub const NAME_MULTICAST_SPARSE_DELGATE_PROPERTY: &str = "MulticastSparseDelegateProperty"; +pub const NAME_NAME_PROPERTY: &str = "NameProperty"; +pub const NAME_OBJECT_PROPERTY: &str = "ObjectProperty"; +pub const NAME_OPTION_PROPERTY: &str = "OptionProperty"; +pub const NAME_SET_PROPERTY: &str = "SetProperty"; +pub const NAME_SOFT_OBJECT_PROPERTY: &str = "SoftObjectProperty"; +pub const NAME_STRUCT_PROPERTY: &str = "StructProperty"; +pub const NAME_STR_PROPERTY: &str = "StrProperty"; +pub const NAME_TEXT_PROPERTY: &str = "TextProperty"; +pub const NAME_UINT16_PROPERTY: &str = "UInt16Property"; +pub const NAME_UINT32_PROPERTY: &str = "UInt32Property"; +pub const NAME_UINT64_PROPERTY: &str = "UInt64Property"; + +pub const PATH__SCRIPT__CORE_U_OBJECT: &str = "/Script/CoreUObject"; + +pub const NAME_DATE_TIME: &str = "DateTime"; +pub const NAME_GAMEPLAY_TAG_CONTAINER: &str = "GameplayTagContainer"; +pub const NAME_GUID: &str = "Guid"; +pub const NAME_INT_POINT: &str = "IntPoint"; +pub const NAME_LINEAR_COLOR: &str = "LinearColor"; +pub const NAME_QUAT: &str = "Quat"; +pub const NAME_ROTATOR: &str = "Rotator"; +pub const NAME_TIMESPAN: &str = "Timespan"; +pub const NAME_VECTOR: &str = "Vector"; +pub const NAME_VECTOR2D: &str = "Vector2D"; + +// #[binrw] #[derive(Debug)] pub struct OptionalProperty(...); + +macro_rules! ue_struct { + ($name:ident, LWC, $($field:ident),+) => { + #[binrw] + #[br(import(format: &SerializationFormat))] + #[derive(Debug, PartialEq)] + pub enum $name { + #[br(pre_assert(!format.large_world_coordinates()))] + F { + $($field: f32,)+ + }, + #[br(pre_assert( format.large_world_coordinates()))] + D { + $($field: f64,)+ + }, + } + }; + ($name:ident, $($ty:ty),+) => { + #[binrw] + #[derive(Debug, PartialEq)] + pub struct $name( + $(pub $ty),+ + ); + }; + ($name:ident, $($field_name:ident : $ty:ty),+) => { + #[binrw] + #[derive(Debug, PartialEq)] + pub struct $name { + $(pub $field_name: $ty),+ + } + }; +} + +// Engine structs +ue_struct!(FCustomVersion, key: FGuid, value: u32); +ue_struct!(FEngineVersion, major: u16, minor: u16, patch: u16, change_list: u32, branch: FString); + +// Properties +ue_struct!(FDelegateProperty, object: FString, function_name: FString); +ue_struct!(FDoubleProperty, f64); +ue_struct!(FFloatProperty, f32); +ue_struct!(FFieldPathProperty, path: TArray, resolved_owner: FString); +ue_struct!(FInt16Property, i16); +ue_struct!(FInt64Property, i64); +ue_struct!(FInt8Property, i8); +ue_struct!(FIntProperty, i32); +ue_struct!(FMulticastInlineDelegateProperty, TArray); +ue_struct!(FMulticastSparseDelegateProperty, TArray); +ue_struct!(FNameProperty, FString); +ue_struct!(FObjectProperty, FString); +ue_struct!(FStrProperty, FString); +ue_struct!(FUInt16Property, u16); +ue_struct!(FUInt32Property, u32); +ue_struct!(FUInt64Property, u64); + +// StructProperty structs +ue_struct!(FDateTime, ticks: i64); +ue_struct!(FGameplayTag, FString); +ue_struct!(FGameplayTagContainer, TArray); +ue_struct!(FIntPoint, x: i32, y: i32); +ue_struct!(FLinearColor, r: f32, g: f32, b: f32, a: f32); +ue_struct!(FQuat, LWC, x, y, z, w); +ue_struct!(FRotator, LWC, pitch, yaw, roll); +ue_struct!(FTimespan, ticks: i64); +ue_struct!(FVector, LWC, x, y, z); +ue_struct!(FVector2D, x: f64, y: f64); diff --git a/src/types/t_array.rs b/src/types/t_array.rs new file mode 100644 index 0000000..edd7652 --- /dev/null +++ b/src/types/t_array.rs @@ -0,0 +1,100 @@ +use binrw::{BinRead, BinWrite}; + +use crate::error::binrw_custom; + +#[derive(Clone, Eq, PartialEq)] +pub struct TArray(pub Vec); + +impl TArray { + pub fn empty() -> Self { + Self(Vec::new()) + } +} + +impl BinRead for TArray +where + for<'a> T::Args<'a>: Copy, +{ + type Args<'a> = T::Args<'a>; + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult { + let count = u32::read_options(reader, endian, ())?; + let mut items = Vec::with_capacity(count as usize); + for _ in 0..count { + items.push(T::read_options(reader, endian, args)?); + } + Ok(Self(items)) + } +} + +impl BinWrite for TArray +where + for<'a> T::Args<'a>: Copy, +{ + type Args<'a> = T::Args<'a>; + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + let pos = writer.stream_position()?; + let count = self.len(); + let count = u32::try_from(count).map_err(binrw_custom(pos))?; + count.write_options(writer, endian, ())?; + for item in self.iter() { + item.write_options(writer, endian, args)?; + } + Ok(()) + } +} + +impl std::fmt::Debug for TArray { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TArray::from({:#?})", self.0) + } +} + +impl std::ops::Deref for TArray { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for TArray { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From<[T; N]> for TArray { + #[inline] + fn from(value: [T; N]) -> Self { + Self(Vec::from(value)) + } +} + +impl From> for TArray { + #[inline] + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl IntoIterator for TArray { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/src/types/t_optional.rs b/src/types/t_optional.rs new file mode 100644 index 0000000..caeb256 --- /dev/null +++ b/src/types/t_optional.rs @@ -0,0 +1,68 @@ +use binrw::{BinRead, BinWrite}; + +#[derive(Debug, PartialEq)] +pub struct TOptional(pub Option); + +impl BinRead for TOptional +where + for<'a> T::Args<'a>: Copy, +{ + type Args<'a> = T::Args<'a>; + + fn read_options( + reader: &mut R, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult { + let pos = reader.stream_position()?; + let count = u32::read_options(reader, endian, ())?; + let value = match count { + 0 => None, + 1 => Some(T::read_options(reader, endian, args)?), + n => Err(binrw::Error::AssertFail { + pos, + message: format!("TOptional expected 0 or 1 elements, got {n}"), + })?, + }; + Ok(Self(value)) + } +} + +impl BinWrite for TOptional +where + for<'a> T::Args<'a>: Copy, +{ + type Args<'a> = T::Args<'a>; + + fn write_options( + &self, + writer: &mut W, + endian: binrw::Endian, + args: Self::Args<'_>, + ) -> binrw::BinResult<()> { + match self.as_ref() { + None => { + u32::write_options(&0, writer, endian, ())?; + } + Some(value) => { + u32::write_options(&1, writer, endian, ())?; + T::write_options(value, writer, endian, args)?; + } + } + Ok(()) + } +} + +impl std::ops::Deref for TOptional { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for TOptional { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/types/u_save_game.rs b/src/types/u_save_game.rs new file mode 100644 index 0000000..bb84f96 --- /dev/null +++ b/src/types/u_save_game.rs @@ -0,0 +1,83 @@ +use binrw::binrw; + +use crate::types::{FSaveGameHeader, TaggedProperties}; + +#[binrw] +#[brw(little)] +#[derive(Debug, PartialEq)] +pub struct USaveGame { + pub header: FSaveGameHeader, + + #[brw(if(header.serialization_format().property_tag_complete_type_name()))] + #[br(temp, assert(spacer == 0))] + #[bw(calc(0))] + spacer: u8, + + #[brw(args(&header.serialization_format()))] + pub properties: TaggedProperties, + + #[br(temp, assert(footer == 0))] + #[bw(calc(0))] + footer: u32, +} + +#[cfg(test)] +mod test { + use std::{ + fs::File, + io::{Cursor, Read, Seek}, + }; + + use binrw::{BinRead, BinWrite}; + + use crate::{ + error::Result, + test::common::{ + ALL_TEST_PATHS, DELEGATE_PATH, OPTIONS_PATH, SAVESLOT_03_PATH, SLOT1_PATH, + VECTOR2D_PATH, delegate, options, saveslot3, slot1, vector2d, + }, + types::USaveGame, + }; + + #[test] + fn test_save_games() -> Result<()> { + for path in ALL_TEST_PATHS { + // Open + let mut file = File::open(path)?; + + // Read + let mut buf = Vec::new(); + let len = file.read_to_end(&mut buf)?; + + // Parse + let mut cursor = Cursor::new(buf); + let result = USaveGame::read(&mut cursor)?; + assert_eq!(len as u64, cursor.stream_position()?); + + // Write + let buf2 = vec![0u8; len]; + let mut cursor2 = Cursor::new(buf2); + USaveGame::write(&result, &mut cursor2)?; + + // Compare + let buf = cursor.into_inner(); + let buf2 = cursor2.into_inner(); + assert!(buf == buf2); + + assert_eq!( + result, + match path { + DELEGATE_PATH => delegate::expected(), + OPTIONS_PATH => options::expected(), + SAVESLOT_03_PATH => saveslot3::expected(), + SLOT1_PATH => slot1::expected(), + VECTOR2D_PATH => vector2d::expected(), + _ => continue, + } + ); + } + + // Success + Ok(()) + } +} diff --git a/tests/common/delegate.rs b/tests/common/delegate.rs deleted file mode 100644 index cb05697..0000000 --- a/tests/common/delegate.rs +++ /dev/null @@ -1,244 +0,0 @@ -use gvas::{ - GvasFile, GvasHeader, - engine_version::FEngineVersion, - game_version::DeserializedGameVersion, - properties::{ - Property, - delegate_property::{ - Delegate, DelegateProperty, MulticastInlineDelegateProperty, MulticastScriptDelegate, - MulticastSparseDelegateProperty, - }, - }, - types::{Guid, map::HashableIndexMap}, -}; -use std::str::FromStr; - -const DELEGATE_STR: &str = - "/Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.Saver1_Blueprint_2"; - -pub(crate) fn expected() -> GvasFile { - GvasFile { - deserialized_game_version: DeserializedGameVersion::Default, - header: GvasHeader::Version2 { - package_file_version: 517, - engine_version: FEngineVersion { - major: 4, - minor: 23, - patch: 1, - change_list: 9631420, - branch: String::from("++UE4+Release-4.23"), - }, - custom_version_format: 3, - custom_versions: HashableIndexMap::from([ - ( - Guid::from_str("22D5549C-BE4F-26A8-4607-2194D082B461").unwrap(), - 23, - ), - ( - Guid::from_str("E432D8B0-0D4F-891F-B77E-CFACA24AFD36").unwrap(), - 10, - ), - ( - Guid::from_str("2843C6E1-534D-2CA2-868E-6CA38CBD1764").unwrap(), - 0, - ), - ( - Guid::from_str("3CC15E37-FB48-E406-F084-00B57E712A26").unwrap(), - 3, - ), - ( - Guid::from_str("ED68B0E4-E942-94F4-0BDA-31A241BB462E").unwrap(), - 34, - ), - ( - Guid::from_str("3F74FCCF-8044-B043-DF14-919373201D17").unwrap(), - 35, - ), - ( - Guid::from_str("B5492BB0-E944-20BB-B732-04A36003E452").unwrap(), - 2, - ), - ( - Guid::from_str("5C10E4A4-B549-A159-C440-C5A7EEDF7E54").unwrap(), - 0, - ), - ( - Guid::from_str("C931C839-DC47-E65A-179C-449A7C8E1C3E").unwrap(), - 0, - ), - ( - Guid::from_str("331BF078-984F-EAEB-EA84-B4B9A25AB9CC").unwrap(), - 0, - ), - ( - Guid::from_str("0F383166-E043-4D2D-27CF-09805AA95669").unwrap(), - 0, - ), - ( - Guid::from_str("9F8BF812-FC4A-7588-0CD9-7CA629BD3A38").unwrap(), - 31, - ), - ( - Guid::from_str("4CE75A7B-104C-70D2-9857-58A95A2A210B").unwrap(), - 11, - ), - ( - Guid::from_str("186929D7-DD4B-D61D-A864-E29D8438C13C").unwrap(), - 2, - ), - ( - Guid::from_str("7852A1C2-FE4A-E7BF-FF90-176C55F71D53").unwrap(), - 1, - ), - ( - Guid::from_str("D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209").unwrap(), - 3, - ), - ( - Guid::from_str("DD75E529-2746-A3E0-76D2-109DEADC2C23").unwrap(), - 17, - ), - ( - Guid::from_str("5DA643AF-4749-D37F-8E3E-739805BBC1D9").unwrap(), - 2, - ), - ( - Guid::from_str("EC6C266B-8F4B-C71E-D9E4-0BA307FC4209").unwrap(), - 1, - ), - ( - Guid::from_str("613DF70D-EA47-3FA2-E989-27B79A49410C").unwrap(), - 1, - ), - ( - Guid::from_str("86181D60-844F-64AC-DED3-16AAD6C7EA0D").unwrap(), - 27, - ), - ( - Guid::from_str("D6BCFF9D-5801-4F49-8212-21E288A8923C").unwrap(), - 6, - ), - ( - Guid::from_str("ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA").unwrap(), - 1, - ), - ( - Guid::from_str("0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0").unwrap(), - 9, - ), - ( - Guid::from_str("E79E7F71-3A49-B0E9-3291-B3880781381B").unwrap(), - 6, - ), - ( - Guid::from_str("B3DC7D8E-BB47-DA80-A246-D39FF64D9893").unwrap(), - 1, - ), - ( - Guid::from_str("CDB08ACB-DE4B-8CE7-9313-62A862EFE914").unwrap(), - 0, - ), - ( - Guid::from_str("F20A68FB-A34B-EF59-B519-A8BA3D44C873").unwrap(), - 1, - ), - ( - Guid::from_str("9186E0AF-5249-0D3A-3B67-73B61E2DF27C").unwrap(), - 2, - ), - ( - Guid::from_str("BDFDB52E-104D-AC01-8FF3-3681DAA59333").unwrap(), - 5, - ), - ( - Guid::from_str("4F359D50-2F49-E6F6-B285-49A71C633C07").unwrap(), - 0, - ), - ( - Guid::from_str("EAB762A4-3A4E-99F4-1FEC-C199B2E12482").unwrap(), - 2, - ), - ( - Guid::from_str("194D0C43-7049-5471-699B-6987E5B090DF").unwrap(), - 13, - ), - ( - Guid::from_str("BD32FEAA-144C-9553-255E-6AB6DDD13210").unwrap(), - 1, - ), - ( - Guid::from_str("8EE1AF23-584E-E14C-52C2-618DB7BE53B9").unwrap(), - 8, - ), - ( - Guid::from_str("40EB564A-DC11-F510-7E34-D392E76AC9B2").unwrap(), - 2, - ), - ( - Guid::from_str("004A8AD7-9746-58E8-B519-A8BAB4467D48").unwrap(), - 17, - ), - ( - Guid::from_str("86F87955-1F4C-3A93-7B08-BA832FB96163").unwrap(), - 1, - ), - ( - Guid::from_str("52BE2F61-0B40-53DA-914F-0D917C85B19F").unwrap(), - 1, - ), - ( - Guid::from_str("367A23A4-C941-EACA-F818-A28FF31B6858").unwrap(), - 4, - ), - ( - Guid::from_str("753F4E80-494B-8870-068C-D6A4DCB67E3C").unwrap(), - 5, - ), - ( - Guid::from_str("ED0A3111-614D-552E-A39A-67AF2C08A1C5").unwrap(), - 17, - ), - ( - Guid::from_str("965196AB-FC08-D845-8D22-D7B79E56AD78").unwrap(), - 1, - ), - ( - Guid::from_str("F37ABB24-834F-4656-C22D-2F1FFF96AD49").unwrap(), - 4, - ), - ( - Guid::from_str("12E426FB-4D4B-151F-0A55-7293702F1D96").unwrap(), - 3, - ), - ]), - save_game_class_name: String::from("/Script/SaveFileTest.TestSaveGame"), - }, - properties: HashableIndexMap::from([ - ( - String::from("DynamicDelegate"), - Property::from(DelegateProperty::new(Delegate::new( - String::from(DELEGATE_STR), - String::from("FirstBinding"), - ))), - ), - ( - String::from("MulticastDelegate"), - Property::from(MulticastInlineDelegateProperty::new( - MulticastScriptDelegate::new(vec![ - Delegate::new(String::from(DELEGATE_STR), String::from("FirstBinding")), - Delegate::new(String::from(DELEGATE_STR), String::from("SecondBinding")), - ]), - )), - ), - ( - String::from("MulticastSparseDelegate"), - Property::from(MulticastSparseDelegateProperty::new( - MulticastScriptDelegate::new(vec![Delegate::new( - String::from(DELEGATE_STR), - String::from("FirstBinding"), - )]), - )), - ), - ]), - } -} diff --git a/tests/common/options.rs b/tests/common/options.rs deleted file mode 100644 index 087b3f0..0000000 --- a/tests/common/options.rs +++ /dev/null @@ -1,238 +0,0 @@ -use gvas::{ - GvasFile, GvasHeader, - engine_version::FEngineVersion, - game_version::DeserializedGameVersion, - properties::{Property, int_property::FloatProperty}, - types::{Guid, map::HashableIndexMap}, -}; -use std::str::FromStr; - -pub(crate) fn expected() -> GvasFile { - GvasFile { - deserialized_game_version: DeserializedGameVersion::Default, - header: GvasHeader::Version2 { - package_file_version: 518, - engine_version: FEngineVersion { - major: 4, - minor: 25, - patch: 3, - change_list: 13942748, - branch: "++UE4+Release-4.25".into(), - }, - custom_version_format: 3, - custom_versions: HashableIndexMap::from([ - ( - Guid::from_str("ED0A3111-614D-552E-A39A-67AF2C08A1C5").unwrap(), - 17, - ), - ( - Guid::from_str("F37ABB24-834F-4656-C22D-2F1FFF96AD49").unwrap(), - 5, - ), - ( - Guid::from_str("2923A576-B545-2309-41D8-AE98D86A2FCF").unwrap(), - 2, - ), - ( - Guid::from_str("0769BC5F-AE40-C855-84F1-678E3FF1FF5E").unwrap(), - 1, - ), - ( - Guid::from_str("12E426FB-4D4B-151F-0A55-7293702F1D96").unwrap(), - 3, - ), - ( - Guid::from_str("FA7AF5FC-8342-7650-58E6-A9B9322DA0FF").unwrap(), - 61, - ), - ( - Guid::from_str("22D5549C-BE4F-26A8-4607-2194D082B461").unwrap(), - 30, - ), - ( - Guid::from_str("E432D8B0-0D4F-891F-B77E-CFACA24AFD36").unwrap(), - 10, - ), - ( - Guid::from_str("2843C6E1-534D-2CA2-868E-6CA38CBD1764").unwrap(), - 0, - ), - ( - Guid::from_str("3CC15E37-FB48-E406-F084-00B57E712A26").unwrap(), - 4, - ), - ( - Guid::from_str("ED68B0E4-E942-94F4-0BDA-31A241BB462E").unwrap(), - 38, - ), - ( - Guid::from_str("3F74FCCF-8044-B043-DF14-919373201D17").unwrap(), - 37, - ), - ( - Guid::from_str("B5492BB0-E944-20BB-B732-04A36003E452").unwrap(), - 2, - ), - ( - Guid::from_str("5C10E4A4-B549-A159-C440-C5A7EEDF7E54").unwrap(), - 0, - ), - ( - Guid::from_str("C931C839-DC47-E65A-179C-449A7C8E1C3E").unwrap(), - 0, - ), - ( - Guid::from_str("331BF078-984F-EAEB-EA84-B4B9A25AB9CC").unwrap(), - 4, - ), - ( - Guid::from_str("0F383166-E043-4D2D-27CF-09805AA95669").unwrap(), - 0, - ), - ( - Guid::from_str("9F8BF812-FC4A-7588-0CD9-7CA629BD3A38").unwrap(), - 43, - ), - ( - Guid::from_str("4CE75A7B-104C-70D2-9857-58A95A2A210B").unwrap(), - 12, - ), - ( - Guid::from_str("186929D7-DD4B-D61D-A864-E29D8438C13C").unwrap(), - 3, - ), - ( - Guid::from_str("7852A1C2-FE4A-E7BF-FF90-176C55F71D53").unwrap(), - 1, - ), - ( - Guid::from_str("D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209").unwrap(), - 3, - ), - ( - Guid::from_str("DD75E529-2746-A3E0-76D2-109DEADC2C23").unwrap(), - 17, - ), - ( - Guid::from_str("5DA643AF-4749-D37F-8E3E-739805BBC1D9").unwrap(), - 7, - ), - ( - Guid::from_str("EC6C266B-8F4B-C71E-D9E4-0BA307FC4209").unwrap(), - 1, - ), - ( - Guid::from_str("613DF70D-EA47-3FA2-E989-27B79A49410C").unwrap(), - 1, - ), - ( - Guid::from_str("86181D60-844F-64AC-DED3-16AAD6C7EA0D").unwrap(), - 31, - ), - ( - Guid::from_str("D6BCFF9D-5801-4F49-8212-21E288A8923C").unwrap(), - 10, - ), - ( - Guid::from_str("ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA").unwrap(), - 1, - ), - ( - Guid::from_str("0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0").unwrap(), - 10, - ), - ( - Guid::from_str("834AF935-6C40-58E2-F509-18A37C241096").unwrap(), - 37, - ), - ( - Guid::from_str("6EC18FB6-E242-1B8B-5C21-53B4FE448805").unwrap(), - 1, - ), - ( - Guid::from_str("0685E1B2-C2CF-7342-BBF4-4EA507BA8B75").unwrap(), - 1, - ), - ( - Guid::from_str("50326854-AF48-9980-9698-C88BB7F9ADFB").unwrap(), - 0, - ), - ( - Guid::from_str("194D0C43-7049-5471-699B-6987E5B090DF").unwrap(), - 14, - ), - ( - Guid::from_str("BD32FEAA-144C-9553-255E-6AB6DDD13210").unwrap(), - 1, - ), - ( - Guid::from_str("8EE1AF23-584E-E14C-52C2-618DB7BE53B9").unwrap(), - 11, - ), - ( - Guid::from_str("EAB762A4-3A4E-99F4-1FEC-C199B2E12482").unwrap(), - 2, - ), - ( - Guid::from_str("BDFDB52E-104D-AC01-8FF3-3681DAA59333").unwrap(), - 5, - ), - ( - Guid::from_str("4F359D50-2F49-E6F6-B285-49A71C633C07").unwrap(), - 0, - ), - ( - Guid::from_str("E79E7F71-3A49-B0E9-3291-B3880781381B").unwrap(), - 6, - ), - ( - Guid::from_str("40EB564A-DC11-F510-7E34-D392E76AC9B2").unwrap(), - 2, - ), - ( - Guid::from_str("004A8AD7-9746-58E8-B519-A8BAB4467D48").unwrap(), - 17, - ), - ( - Guid::from_str("86F87955-1F4C-3A93-7B08-BA832FB96163").unwrap(), - 1, - ), - ( - Guid::from_str("52BE2F61-0B40-53DA-914F-0D917C85B19F").unwrap(), - 1, - ), - ( - Guid::from_str("367A23A4-C941-EACA-F818-A28FF31B6858").unwrap(), - 4, - ), - ( - Guid::from_str("753F4E80-494B-8870-068C-D6A4DCB67E3C").unwrap(), - 5, - ), - ( - Guid::from_str("F20A68FB-A34B-EF59-B519-A8BA3D44C873").unwrap(), - 2, - ), - ( - Guid::from_str("0EB75099-174E-1AB4-0DFA-CCBBD67F8157").unwrap(), - 1, - ), - ( - Guid::from_str("965196AB-FC08-D845-8D22-D7B79E56AD78").unwrap(), - 1, - ), - ]), - save_game_class_name: "/Game/UI/BP_SaveOptions.BP_SaveOptions_C".into(), - }, - properties: HashableIndexMap::from([ - ( - "Slider1".into(), - Property::from(FloatProperty::new(0.16610672)), - ), - ( - "Slider2".into(), - Property::from(FloatProperty::new(0.28251615)), - ), - ]), - } -} diff --git a/tests/common/saveslot3.rs b/tests/common/saveslot3.rs deleted file mode 100644 index 3c2cece..0000000 --- a/tests/common/saveslot3.rs +++ /dev/null @@ -1,702 +0,0 @@ -use std::{collections::HashMap, str::FromStr as _}; - -use gvas::{ - GvasFile, GvasHeader, - engine_version::FEngineVersion, - properties::{ - Property, - field_path_property::{FieldPath, FieldPathProperty}, - int_property::{FloatProperty, IntProperty}, - map_property::MapProperty, - name_property::NameProperty, - object_property::ObjectProperty, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, - struct_types::DateTime, - }, - types::{Guid, map::HashableIndexMap}, -}; - -pub(crate) fn hints() -> HashMap { - HashMap::from([ - ( - "MinersManualKnownObjects.SetProperty.StructProperty".to_string(), - "Struct".to_string(), - ), - ( - "GameplayDatabase.MapProperty.Value.StructProperty".to_string(), - "Struct".to_string(), - ), - ( - "PlayerAttributes.MapProperty.Key.StructProperty".to_string(), - "Struct".to_string(), - ), - ]) -} - -pub(crate) fn expected() -> GvasFile { - GvasFile { - deserialized_game_version: gvas::game_version::DeserializedGameVersion::Default, - header: GvasHeader::Version2 { - package_file_version: 522, - engine_version: FEngineVersion { - major: 4, - minor: 27, - patch: 2, - change_list: 18319896, - branch: String::from("++UE4+Release-4.27"), - }, - custom_version_format: 3, - custom_versions: HashableIndexMap::from([ - ( - Guid::from_str("FA7AF5FC-8342-7650-58E6-A9B9322DA0FF").unwrap(), - 68, - ), - ( - Guid::from_str("FA7AF5FC-8342-7650-58E6-A9B9322DA0FF").unwrap(), - 68, - ), - ( - Guid::from_str("12E426FB-4D4B-151F-0A55-7293702F1D96").unwrap(), - 3, - ), - ( - Guid::from_str("FB0C82A7-5943-A720-142C-548C50CF2396").unwrap(), - 6, - ), - ( - Guid::from_str("4E7CE782-A543-2333-C513-6BB4F30D3197").unwrap(), - 0, - ), - ( - Guid::from_str("ED0A3111-614D-552E-A39A-67AF2C08A1C5").unwrap(), - 17, - ), - ( - Guid::from_str("F37ABB24-834F-4656-C22D-2F1FFF96AD49").unwrap(), - 5, - ), - ( - Guid::from_str("2923A576-B545-2309-41D8-AE98D86A2FCF").unwrap(), - 5, - ), - ( - Guid::from_str("0769BC5F-AE40-C855-84F1-678E3FF1FF5E").unwrap(), - 1, - ), - ( - Guid::from_str("22D5549C-BE4F-26A8-4607-2194D082B461").unwrap(), - 43, - ), - ( - Guid::from_str("E432D8B0-0D4F-891F-B77E-CFACA24AFD36").unwrap(), - 10, - ), - ( - Guid::from_str("2843C6E1-534D-2CA2-868E-6CA38CBD1764").unwrap(), - 0, - ), - ( - Guid::from_str("3CC15E37-FB48-E406-F084-00B57E712A26").unwrap(), - 4, - ), - ( - Guid::from_str("ED68B0E4-E942-94F4-0BDA-31A241BB462E").unwrap(), - 40, - ), - ( - Guid::from_str("3F74FCCF-8044-B043-DF14-919373201D17").unwrap(), - 37, - ), - ( - Guid::from_str("B5492BB0-E944-20BB-B732-04A36003E452").unwrap(), - 3, - ), - ( - Guid::from_str("5C10E4A4-B549-A159-C440-C5A7EEDF7E54").unwrap(), - 0, - ), - ( - Guid::from_str("C931C839-DC47-E65A-179C-449A7C8E1C3E").unwrap(), - 0, - ), - ( - Guid::from_str("331BF078-984F-EAEB-EA84-B4B9A25AB9CC").unwrap(), - 14, - ), - ( - Guid::from_str("0F383166-E043-4D2D-27CF-09805AA95669").unwrap(), - 0, - ), - ( - Guid::from_str("9F8BF812-FC4A-7588-0CD9-7CA629BD3A38").unwrap(), - 45, - ), - ( - Guid::from_str("4CE75A7B-104C-70D2-9857-58A95A2A210B").unwrap(), - 13, - ), - ( - Guid::from_str("186929D7-DD4B-D61D-A864-E29D8438C13C").unwrap(), - 3, - ), - ( - Guid::from_str("7852A1C2-FE4A-E7BF-FF90-176C55F71D53").unwrap(), - 1, - ), - ( - Guid::from_str("D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209").unwrap(), - 3, - ), - ( - Guid::from_str("DD75E529-2746-A3E0-76D2-109DEADC2C23").unwrap(), - 17, - ), - ( - Guid::from_str("5DA643AF-4749-D37F-8E3E-739805BBC1D9").unwrap(), - 15, - ), - ( - Guid::from_str("EC6C266B-8F4B-C71E-D9E4-0BA307FC4209").unwrap(), - 1, - ), - ( - Guid::from_str("613DF70D-EA47-3FA2-E989-27B79A49410C").unwrap(), - 1, - ), - ( - Guid::from_str("86181D60-844F-64AC-DED3-16AAD6C7EA0D").unwrap(), - 47, - ), - ( - Guid::from_str("686308E7-584C-236B-701B-3984915E2616").unwrap(), - 1, - ), - ( - Guid::from_str("D6BCFF9D-5801-4F49-8212-21E288A8923C").unwrap(), - 10, - ), - ( - Guid::from_str("ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA").unwrap(), - 1, - ), - ( - Guid::from_str("0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0").unwrap(), - 10, - ), - ( - Guid::from_str("834AF935-6C40-58E2-F509-18A37C241096").unwrap(), - 41, - ), - ( - Guid::from_str("6EC18FB6-E242-1B8B-5C21-53B4FE448805").unwrap(), - 1, - ), - ( - Guid::from_str("0685E1B2-C2CF-7342-BBF4-4EA507BA8B75").unwrap(), - 1, - ), - ( - Guid::from_str("3689F564-BA42-1BFD-8972-96BA4EFAD0D5").unwrap(), - 1, - ), - ( - Guid::from_str("27D80E6F-9548-09A6-8D99-919CA40E1890").unwrap(), - 2, - ), - ( - Guid::from_str("E79E7F71-3A49-B0E9-3291-B3880781381B").unwrap(), - 8, - ), - ( - Guid::from_str("50326854-AF48-9980-9698-C88BB7F9ADFB").unwrap(), - 0, - ), - ( - Guid::from_str("194D0C43-7049-5471-699B-6987E5B090DF").unwrap(), - 15, - ), - ( - Guid::from_str("BD32FEAA-144C-9553-255E-6AB6DDD13210").unwrap(), - 1, - ), - ( - Guid::from_str("8EE1AF23-584E-E14C-52C2-618DB7BE53B9").unwrap(), - 11, - ), - ( - Guid::from_str("EAB762A4-3A4E-99F4-1FEC-C199B2E12482").unwrap(), - 4, - ), - ( - Guid::from_str("BDFDB52E-104D-AC01-8FF3-3681DAA59333").unwrap(), - 5, - ), - ( - Guid::from_str("4F359D50-2F49-E6F6-B285-49A71C633C07").unwrap(), - 0, - ), - ( - Guid::from_str("40EB564A-DC11-F510-7E34-D392E76AC9B2").unwrap(), - 2, - ), - ( - Guid::from_str("004A8AD7-9746-58E8-B519-A8BAB4467D48").unwrap(), - 18, - ), - ( - Guid::from_str("86F87955-1F4C-3A93-7B08-BA832FB96163").unwrap(), - 2, - ), - ( - Guid::from_str("52BE2F61-0B40-53DA-914F-0D917C85B19F").unwrap(), - 1, - ), - ( - Guid::from_str("367A23A4-C941-EACA-F818-A28FF31B6858").unwrap(), - 4, - ), - ( - Guid::from_str("753F4E80-494B-8870-068C-D6A4DCB67E3C").unwrap(), - 5, - ), - ( - Guid::from_str("F20A68FB-A34B-EF59-B519-A8BA3D44C873").unwrap(), - 2, - ), - ( - Guid::from_str("0EB75099-174E-1AB4-0DFA-CCBBD67F8157").unwrap(), - 1, - ), - ]), - save_game_class_name: String::from("/Script/CD.CDSave_GameState"), - }, - properties: HashableIndexMap::from([ - ( - String::from("LastSaveTime"), - Property::from(StructProperty { - type_name: String::from("DateTime"), - guid: Guid::default(), - value: StructPropertyValue::from(DateTime { - ticks: 638160761644140000, - }), - }), - ), - ( - String::from("PlayerClass"), - Property::from(ObjectProperty::from( - "/Game/Character/Player/Blueprints/BP_Soldier.BP_Soldier_C", - )), - ), - (String::from("Version"), Property::from(IntProperty::new(3))), - ( - String::from("GameplayDatabase"), - Property::from(MapProperty::new( - String::from("NameProperty"), - String::from("StructProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(NameProperty::from("unlock.welcomescreen.seen")), - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AsFloat"), - vec![Property::from(FloatProperty::new(0f32))], - ), - ( - String::from("AsString"), - vec![Property::from(StrProperty::new(None))], - ), - ]), - )), - ), - ( - Property::from(NameProperty::from("game.tutorial.finished")), - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AsFloat"), - vec![Property::from(FloatProperty::new(1f32))], - ), - ( - String::from("AsString"), - vec![Property::from(StrProperty::new(None))], - ), - ]), - )), - ), - ( - Property::from(NameProperty::from("game.tutorial.skipped")), - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AsFloat"), - vec![Property::from(FloatProperty::new(1f32))], - ), - ( - String::from("AsString"), - vec![Property::from(StrProperty::new(None))], - ), - ]), - )), - ), - ( - Property::from(NameProperty::from("dialogs.messages.seen.Rumiko.0.50")), - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AsFloat"), - vec![Property::from(FloatProperty::new(1f32))], - ), - ( - String::from("AsString"), - vec![Property::from(StrProperty::new(None))], - ), - ]), - )), - ), - ( - Property::from(NameProperty::from("codex.Rumiko")), - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AsFloat"), - vec![Property::from(FloatProperty::new(1f32))], - ), - ( - String::from("AsString"), - vec![Property::from(StrProperty::new(None))], - ), - ]), - )), - ), - ]), - )), - ), - ( - String::from("PlayerAttributes"), - Property::from(MapProperty::Properties { - key_type: String::from("StructProperty"), - value_type: String::from("FloatProperty"), - allocation_flags: 0, - value: HashableIndexMap::from([ - ( - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AttributeName"), - vec![Property::from(StrProperty::from( - "Currency_Blueprints", - ))], - ), - ( - String::from("Attribute"), - vec![Property::from(FieldPathProperty::new( - FieldPath::new( - Vec::from([String::from("Currency_Blueprints")]), - String::from("/Script/CD.CDPlayerAttributeSet"), - ), - ))], - ), - ( - String::from("AttributeOwner"), - vec![Property::from(ObjectProperty::from("None"))], - ), - ]), - )), - Property::from(FloatProperty::new(0f32)), - ), - ( - Property::from(StructPropertyValue::CustomStruct( - HashableIndexMap::from([ - ( - String::from("AttributeName"), - vec![Property::from(StrProperty::from( - "Currency_Electrum", - ))], - ), - ( - String::from("Attribute"), - vec![Property::from(FieldPathProperty::new( - FieldPath::new( - Vec::from([String::from("Currency_Electrum")]), - String::from("/Script/CD.CDPlayerAttributeSet"), - ), - ))], - ), - ( - String::from("AttributeOwner"), - vec![Property::from(ObjectProperty::from("None"))], - ), - ]), - )), - Property::from(FloatProperty::new(0f32)), - ), - ]), - }), - ), - ( - String::from("SecondaryWeaponClass"), - Property::from(ObjectProperty::from( - "/Game/Weapons/RocketLauncher/Blueprints/BP_RocketLauncher.BP_RocketLauncher_C", - )), - ), - ]), - } -} - -pub(crate) const SAVESLOT_03_JSON: &str = r#"{ - "header": { - "type": "Version2", - "package_file_version": 522, - "engine_version": { - "major": 4, - "minor": 27, - "patch": 2, - "change_list": 18319896, - "branch": "++UE4+Release-4.27" - }, - "custom_version_format": 3, - "custom_versions": { - "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 68, - "12E426FB-4D4B-151F-0A55-7293702F1D96": 3, - "FB0C82A7-5943-A720-142C-548C50CF2396": 6, - "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, - "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, - "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, - "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, - "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, - "22D5549C-BE4F-26A8-4607-2194D082B461": 43, - "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, - "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, - "3CC15E37-FB48-E406-F084-00B57E712A26": 4, - "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, - "3F74FCCF-8044-B043-DF14-919373201D17": 37, - "B5492BB0-E944-20BB-B732-04A36003E452": 3, - "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, - "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, - "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 14, - "0F383166-E043-4D2D-27CF-09805AA95669": 0, - "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 45, - "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, - "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, - "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, - "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, - "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, - "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, - "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, - "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, - "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 47, - "686308E7-584C-236B-701B-3984915E2616": 1, - "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, - "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, - "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, - "834AF935-6C40-58E2-F509-18A37C241096": 41, - "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, - "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, - "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, - "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, - "E79E7F71-3A49-B0E9-3291-B3880781381B": 8, - "50326854-AF48-9980-9698-C88BB7F9ADFB": 0, - "194D0C43-7049-5471-699B-6987E5B090DF": 15, - "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, - "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, - "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, - "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, - "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, - "40EB564A-DC11-F510-7E34-D392E76AC9B2": 2, - "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, - "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, - "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, - "367A23A4-C941-EACA-F818-A28FF31B6858": 4, - "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, - "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, - "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1 - }, - "save_game_class_name": "/Script/CD.CDSave_GameState" - }, - "properties": { - "LastSaveTime": { - "type": "StructProperty", - "type_name": "DateTime", - "DateTime": { - "ticks": 638160761644140000 - } - }, - "PlayerClass": { - "type": "ObjectProperty", - "value": "/Game/Character/Player/Blueprints/BP_Soldier.BP_Soldier_C" - }, - "Version": { - "type": "IntProperty", - "value": 3 - }, - "GameplayDatabase": { - "type": "MapProperty", - "value_type": "StructProperty", - "name_props": { - "unlock.welcomescreen.seen": { - "type": "StructPropertyValue", - "CustomStruct": { - "AsFloat": [ - { - "type": "FloatProperty", - "value": 0.0 - } - ], - "AsString": [ - { - "type": "StrProperty" - } - ] - } - }, - "game.tutorial.finished": { - "type": "StructPropertyValue", - "CustomStruct": { - "AsFloat": [ - { - "type": "FloatProperty", - "value": 1.0 - } - ], - "AsString": [ - { - "type": "StrProperty" - } - ] - } - }, - "game.tutorial.skipped": { - "type": "StructPropertyValue", - "CustomStruct": { - "AsFloat": [ - { - "type": "FloatProperty", - "value": 1.0 - } - ], - "AsString": [ - { - "type": "StrProperty" - } - ] - } - }, - "dialogs.messages.seen.Rumiko.0.50": { - "type": "StructPropertyValue", - "CustomStruct": { - "AsFloat": [ - { - "type": "FloatProperty", - "value": 1.0 - } - ], - "AsString": [ - { - "type": "StrProperty" - } - ] - } - }, - "codex.Rumiko": { - "type": "StructPropertyValue", - "CustomStruct": { - "AsFloat": [ - { - "type": "FloatProperty", - "value": 1.0 - } - ], - "AsString": [ - { - "type": "StrProperty" - } - ] - } - } - } - }, - "PlayerAttributes": { - "type": "MapProperty", - "key_type": "StructProperty", - "value_type": "FloatProperty", - "allocation_flags": 0, - "value": [ - [ - { - "type": "StructPropertyValue", - "CustomStruct": { - "AttributeName": [ - { - "type": "StrProperty", - "value": "Currency_Blueprints" - } - ], - "Attribute": [ - { - "type": "FieldPathProperty", - "value": { - "path": [ - "Currency_Blueprints" - ], - "resolved_owner": "/Script/CD.CDPlayerAttributeSet" - } - } - ], - "AttributeOwner": [ - { - "type": "ObjectProperty", - "value": "None" - } - ] - } - }, - { - "type": "FloatProperty", - "value": 0.0 - } - ], - [ - { - "type": "StructPropertyValue", - "CustomStruct": { - "AttributeName": [ - { - "type": "StrProperty", - "value": "Currency_Electrum" - } - ], - "Attribute": [ - { - "type": "FieldPathProperty", - "value": { - "path": [ - "Currency_Electrum" - ], - "resolved_owner": "/Script/CD.CDPlayerAttributeSet" - } - } - ], - "AttributeOwner": [ - { - "type": "ObjectProperty", - "value": "None" - } - ] - } - }, - { - "type": "FloatProperty", - "value": 0.0 - } - ] - ] - }, - "SecondaryWeaponClass": { - "type": "ObjectProperty", - "value": "/Game/Weapons/RocketLauncher/Blueprints/BP_RocketLauncher.BP_RocketLauncher_C" - } - } -}"#; diff --git a/tests/common/slot1.rs b/tests/common/slot1.rs deleted file mode 100644 index 0625d95..0000000 --- a/tests/common/slot1.rs +++ /dev/null @@ -1,558 +0,0 @@ -use gvas::{ - GvasFile, GvasHeader, - engine_version::FEngineVersion, - game_version::DeserializedGameVersion, - properties::{ - Property, - array_property::ArrayProperty, - int_property::{ - ByteProperty, BytePropertyValue, DoubleProperty, FloatProperty, Int8Property, - Int16Property, Int64Property, IntProperty, UInt16Property, UInt32Property, - UInt64Property, - }, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, - struct_types::DateTime, - }, - types::{Guid, map::HashableIndexMap}, -}; -use std::str::FromStr; - -#[allow(clippy::approx_constant)] -pub(crate) fn expected() -> GvasFile { - GvasFile { - deserialized_game_version: DeserializedGameVersion::Default, - header: GvasHeader::Version2 { - package_file_version: 522, - engine_version: FEngineVersion { - major: 4, - minor: 27, - patch: 2, - change_list: 18319896, - branch: String::from("++UE4+Release-4.27"), - }, - custom_version_format: 3, - custom_versions: HashableIndexMap::from([ - ( - Guid::from_str("22D5549C-BE4F-26A8-4607-2194D082B461").unwrap(), - 43, - ), - ( - Guid::from_str("E432D8B0-0D4F-891F-B77E-CFACA24AFD36").unwrap(), - 10, - ), - ( - Guid::from_str("2843C6E1-534D-2CA2-868E-6CA38CBD1764").unwrap(), - 0, - ), - ( - Guid::from_str("3CC15E37-FB48-E406-F084-00B57E712A26").unwrap(), - 4, - ), - ( - Guid::from_str("ED68B0E4-E942-94F4-0BDA-31A241BB462E").unwrap(), - 40, - ), - ( - Guid::from_str("3F74FCCF-8044-B043-DF14-919373201D17").unwrap(), - 37, - ), - ( - Guid::from_str("B5492BB0-E944-20BB-B732-04A36003E452").unwrap(), - 3, - ), - ( - Guid::from_str("5C10E4A4-B549-A159-C440-C5A7EEDF7E54").unwrap(), - 0, - ), - ( - Guid::from_str("C931C839-DC47-E65A-179C-449A7C8E1C3E").unwrap(), - 0, - ), - ( - Guid::from_str("331BF078-984F-EAEB-EA84-B4B9A25AB9CC").unwrap(), - 14, - ), - ( - Guid::from_str("0F383166-E043-4D2D-27CF-09805AA95669").unwrap(), - 0, - ), - ( - Guid::from_str("9F8BF812-FC4A-7588-0CD9-7CA629BD3A38").unwrap(), - 45, - ), - ( - Guid::from_str("4CE75A7B-104C-70D2-9857-58A95A2A210B").unwrap(), - 13, - ), - ( - Guid::from_str("186929D7-DD4B-D61D-A864-E29D8438C13C").unwrap(), - 3, - ), - ( - Guid::from_str("7852A1C2-FE4A-E7BF-FF90-176C55F71D53").unwrap(), - 1, - ), - ( - Guid::from_str("D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209").unwrap(), - 3, - ), - ( - Guid::from_str("DD75E529-2746-A3E0-76D2-109DEADC2C23").unwrap(), - 17, - ), - ( - Guid::from_str("5DA643AF-4749-D37F-8E3E-739805BBC1D9").unwrap(), - 15, - ), - ( - Guid::from_str("EC6C266B-8F4B-C71E-D9E4-0BA307FC4209").unwrap(), - 1, - ), - ( - Guid::from_str("613DF70D-EA47-3FA2-E989-27B79A49410C").unwrap(), - 1, - ), - ( - Guid::from_str("86181D60-844F-64AC-DED3-16AAD6C7EA0D").unwrap(), - 47, - ), - ( - Guid::from_str("686308E7-584C-236B-701B-3984915E2616").unwrap(), - 1, - ), - ( - Guid::from_str("D6BCFF9D-5801-4F49-8212-21E288A8923C").unwrap(), - 10, - ), - ( - Guid::from_str("ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA").unwrap(), - 1, - ), - ( - Guid::from_str("0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0").unwrap(), - 10, - ), - ( - Guid::from_str("834AF935-6C40-58E2-F509-18A37C241096").unwrap(), - 41, - ), - ( - Guid::from_str("6EC18FB6-E242-1B8B-5C21-53B4FE448805").unwrap(), - 1, - ), - ( - Guid::from_str("0685E1B2-C2CF-7342-BBF4-4EA507BA8B75").unwrap(), - 1, - ), - ( - Guid::from_str("3689F564-BA42-1BFD-8972-96BA4EFAD0D5").unwrap(), - 1, - ), - ( - Guid::from_str("27D80E6F-9548-09A6-8D99-919CA40E1890").unwrap(), - 2, - ), - ( - Guid::from_str("E79E7F71-3A49-B0E9-3291-B3880781381B").unwrap(), - 8, - ), - ( - Guid::from_str("50326854-AF48-9980-9698-C88BB7F9ADFB").unwrap(), - 0, - ), - ( - Guid::from_str("B3DC7D8E-BB47-DA80-A246-D39FF64D9893").unwrap(), - 1, - ), - ( - Guid::from_str("CDB08ACB-DE4B-8CE7-9313-62A862EFE914").unwrap(), - 0, - ), - ( - Guid::from_str("965196AB-FC08-D845-8D22-D7B79E56AD78").unwrap(), - 1, - ), - ( - Guid::from_str("0EB75099-174E-1AB4-0DFA-CCBBD67F8157").unwrap(), - 1, - ), - ( - Guid::from_str("F20A68FB-A34B-EF59-B519-A8BA3D44C873").unwrap(), - 2, - ), - ( - Guid::from_str("9186E0AF-5249-0D3A-3B67-73B61E2DF27C").unwrap(), - 2, - ), - ( - Guid::from_str("BDFDB52E-104D-AC01-8FF3-3681DAA59333").unwrap(), - 5, - ), - ( - Guid::from_str("4F359D50-2F49-E6F6-B285-49A71C633C07").unwrap(), - 0, - ), - ( - Guid::from_str("EAB762A4-3A4E-99F4-1FEC-C199B2E12482").unwrap(), - 4, - ), - ( - Guid::from_str("194D0C43-7049-5471-699B-6987E5B090DF").unwrap(), - 15, - ), - ( - Guid::from_str("BD32FEAA-144C-9553-255E-6AB6DDD13210").unwrap(), - 1, - ), - ( - Guid::from_str("8EE1AF23-584E-E14C-52C2-618DB7BE53B9").unwrap(), - 11, - ), - ( - Guid::from_str("40EB564A-DC11-F510-7E34-D392E76AC9B2").unwrap(), - 2, - ), - ( - Guid::from_str("004A8AD7-9746-58E8-B519-A8BAB4467D48").unwrap(), - 18, - ), - ( - Guid::from_str("86F87955-1F4C-3A93-7B08-BA832FB96163").unwrap(), - 2, - ), - ( - Guid::from_str("52BE2F61-0B40-53DA-914F-0D917C85B19F").unwrap(), - 1, - ), - ( - Guid::from_str("367A23A4-C941-EACA-F818-A28FF31B6858").unwrap(), - 4, - ), - ( - Guid::from_str("753F4E80-494B-8870-068C-D6A4DCB67E3C").unwrap(), - 5, - ), - ( - Guid::from_str("2923A576-B545-2309-41D8-AE98D86A2FCF").unwrap(), - 5, - ), - ( - Guid::from_str("0769BC5F-AE40-C855-84F1-678E3FF1FF5E").unwrap(), - 1, - ), - ( - Guid::from_str("FA7AF5FC-8342-7650-58E6-A9B9322DA0FF").unwrap(), - 68, - ), - ( - Guid::from_str("F37ABB24-834F-4656-C22D-2F1FFF96AD49").unwrap(), - 5, - ), - ( - Guid::from_str("ED0A3111-614D-552E-A39A-67AF2C08A1C5").unwrap(), - 17, - ), - ( - Guid::from_str("4E7CE782-A543-2333-C513-6BB4F30D3197").unwrap(), - 0, - ), - ( - Guid::from_str("12E426FB-4D4B-151F-0A55-7293702F1D96").unwrap(), - 3, - ), - ]), - save_game_class_name: String::from("/Script/UE4SaveFile.TestSaveGame"), - }, - properties: HashableIndexMap::from([ - ( - String::from("u8_test"), - Property::from(ByteProperty { - name: Some(String::from("None")), - value: BytePropertyValue::Byte(129), - }), - ), - ( - String::from("i8_test"), - Property::from(Int8Property::new(-123i8)), - ), - ( - String::from("ushort_test"), - Property::from(UInt16Property::new(65530u16)), - ), - ( - String::from("short_test"), - Property::from(Int16Property::new(-32764i16)), - ), - ( - String::from("uint32_test"), - Property::from(UInt32Property::new(4294967294u32)), - ), - ( - String::from("int32_test"), - Property::from(IntProperty::new(-2147483647i32)), - ), - ( - String::from("ulong_test"), - Property::from(UInt64Property::new(18446744073709551614u64)), - ), - ( - String::from("long_test"), - Property::from(Int64Property::new(-9223372036854775807i64)), - ), - ( - String::from("f_property"), - Property::from(FloatProperty::new(3.14159f32)), - ), - ( - String::from("d_property"), - Property::from(DoubleProperty::new(3.14159265358979f64)), - ), - ( - String::from("str_property"), - Property::from(StrProperty::from("Hello world")), - ), - ( - String::from("struct_property"), - Property::from(StructProperty { - type_name: String::from("CustomStruct"), - guid: Guid::default(), - value: StructPropertyValue::CustomStruct(HashableIndexMap::from([( - String::from("test_field"), - vec![Property::from(UInt64Property::new(12345u64))], - )])), - }), - ), - ( - String::from("date_time_property"), - Property::from(StructProperty { - type_name: String::from("DateTime"), - guid: Guid::default(), - value: StructPropertyValue::from(DateTime { - ticks: 637864237380020000, - }), - }), - ), - ( - String::from("array_of_structs"), - Property::from(ArrayProperty::Structs { - field_name: String::from("array_of_structs"), - type_name: String::from("CustomStruct"), - guid: Guid::default(), - structs: vec![ - StructPropertyValue::CustomStruct(HashableIndexMap::from([( - String::from("test_field"), - vec![Property::from(UInt64Property::new(10u64))], - )])), - StructPropertyValue::CustomStruct(HashableIndexMap::from([( - String::from("test_field"), - vec![Property::from(UInt64Property::new(10u64))], - )])), - ], - }), - ), - ( - String::from("array_of_ints"), - Property::from(ArrayProperty::Ints { - ints: vec![12, 12, 12, 12, 12], - }), - ), - ( - String::from("array_of_strings"), - Property::from(ArrayProperty::Strings { - strings: vec![ - Some(String::from("Hello world from array")), - Some(String::from("Hello world from array")), - Some(String::from("Hello world from array")), - ], - }), - ), - ]), - } -} - -pub const SLOT1_JSON: &str = r#"{ - "header": { - "type": "Version2", - "package_file_version": 522, - "engine_version": { - "major": 4, - "minor": 27, - "patch": 2, - "change_list": 18319896, - "branch": "++UE4+Release-4.27" - }, - "custom_version_format": 3, - "custom_versions": { - "22D5549C-BE4F-26A8-4607-2194D082B461": 43, - "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, - "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, - "3CC15E37-FB48-E406-F084-00B57E712A26": 4, - "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, - "3F74FCCF-8044-B043-DF14-919373201D17": 37, - "B5492BB0-E944-20BB-B732-04A36003E452": 3, - "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, - "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, - "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 14, - "0F383166-E043-4D2D-27CF-09805AA95669": 0, - "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 45, - "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, - "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, - "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, - "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, - "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, - "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, - "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, - "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, - "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 47, - "686308E7-584C-236B-701B-3984915E2616": 1, - "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, - "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, - "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, - "834AF935-6C40-58E2-F509-18A37C241096": 41, - "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, - "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, - "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, - "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, - "E79E7F71-3A49-B0E9-3291-B3880781381B": 8, - "50326854-AF48-9980-9698-C88BB7F9ADFB": 0, - "B3DC7D8E-BB47-DA80-A246-D39FF64D9893": 1, - "CDB08ACB-DE4B-8CE7-9313-62A862EFE914": 0, - "965196AB-FC08-D845-8D22-D7B79E56AD78": 1, - "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1, - "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, - "9186E0AF-5249-0D3A-3B67-73B61E2DF27C": 2, - "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, - "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, - "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, - "194D0C43-7049-5471-699B-6987E5B090DF": 15, - "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, - "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, - "40EB564A-DC11-F510-7E34-D392E76AC9B2": 2, - "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, - "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, - "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, - "367A23A4-C941-EACA-F818-A28FF31B6858": 4, - "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, - "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, - "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, - "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 68, - "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, - "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, - "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, - "12E426FB-4D4B-151F-0A55-7293702F1D96": 3 - }, - "save_game_class_name": "/Script/UE4SaveFile.TestSaveGame" - }, - "properties": { - "u8_test": { - "type": "ByteProperty", - "name": "None", - "Byte": 129 - }, - "i8_test": { - "type": "Int8Property", - "value": -123 - }, - "ushort_test": { - "type": "UInt16Property", - "value": 65530 - }, - "short_test": { - "type": "Int16Property", - "value": -32764 - }, - "uint32_test": { - "type": "UInt32Property", - "value": 4294967294 - }, - "int32_test": { - "type": "IntProperty", - "value": -2147483647 - }, - "ulong_test": { - "type": "UInt64Property", - "value": 18446744073709551614 - }, - "long_test": { - "type": "Int64Property", - "value": -9223372036854775807 - }, - "f_property": { - "type": "FloatProperty", - "value": 3.14159 - }, - "d_property": { - "type": "DoubleProperty", - "value": 3.14159265358979 - }, - "str_property": { - "type": "StrProperty", - "value": "Hello world" - }, - "struct_property": { - "type": "StructProperty", - "type_name": "CustomStruct", - "CustomStruct": { - "test_field": [ - { - "type": "UInt64Property", - "value": 12345 - } - ] - } - }, - "date_time_property": { - "type": "StructProperty", - "type_name": "DateTime", - "DateTime": { - "ticks": 637864237380020000 - } - }, - "array_of_structs": { - "type": "ArrayProperty", - "field_name": "array_of_structs", - "type_name": "CustomStruct", - "structs": [ - { - "CustomStruct": { - "test_field": [ - { - "type": "UInt64Property", - "value": 10 - } - ] - } - }, - { - "CustomStruct": { - "test_field": [ - { - "type": "UInt64Property", - "value": 10 - } - ] - } - } - ] - }, - "array_of_ints": { - "type": "ArrayProperty", - "ints": [ - 12, - 12, - 12, - 12, - 12 - ] - }, - "array_of_strings": { - "type": "ArrayProperty", - "strings": [ - "Hello world from array", - "Hello world from array", - "Hello world from array" - ] - } - } -}"#; diff --git a/tests/common/vector2d.rs b/tests/common/vector2d.rs deleted file mode 100644 index 98ebf7c..0000000 --- a/tests/common/vector2d.rs +++ /dev/null @@ -1,1304 +0,0 @@ -use gvas::{ - GvasFile, GvasHeader, - engine_version::FEngineVersion, - game_version::DeserializedGameVersion, - properties::{ - Property, - delegate_property::{Delegate, MulticastInlineDelegateProperty, MulticastScriptDelegate}, - int_property::{BoolProperty, FloatProperty, IntProperty}, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, - struct_types::Vector2D, - }, - types::{Guid, map::HashableIndexMap}, -}; -use ordered_float::OrderedFloat; -use std::str::FromStr; - -const DELEGATE_PREFIX: &str = "/Game/DefaultMap.DefaultMap:PersistentLevel."; - -pub(crate) fn expected() -> GvasFile { - GvasFile { - deserialized_game_version: DeserializedGameVersion::Default, - header: GvasHeader::Version3 { - package_file_version: 522, - package_file_version_ue5: 1009, - engine_version: FEngineVersion { - major: 5, - minor: 3, - patch: 2, - change_list: 29314046, - branch: String::from("++UE5+Release-5.3"), - }, - custom_version_format: 3, - custom_versions: HashableIndexMap::from([ - ( - Guid::from_str("22D5549C-BE4F-26A8-4607-2194D082B461").unwrap(), - 44, - ), - ( - Guid::from_str("A35C9162-F74B-8E1C-C712-0EA3F79D21C8").unwrap(), - 32, - ), - ( - Guid::from_str("240D40CC-7B4E-E9E0-83A2-F99B27C0C0DC").unwrap(), - 0, - ), - ( - Guid::from_str("E432D8B0-0D4F-891F-B77E-CFACA24AFD36").unwrap(), - 10, - ), - ( - Guid::from_str("2843C6E1-534D-2CA2-868E-6CA38CBD1764").unwrap(), - 0, - ), - ( - Guid::from_str("3CC15E37-FB48-E406-F084-00B57E712A26").unwrap(), - 4, - ), - ( - Guid::from_str("ED68B0E4-E942-94F4-0BDA-31A241BB462E").unwrap(), - 40, - ), - ( - Guid::from_str("3F74FCCF-8044-B043-DF14-919373201D17").unwrap(), - 37, - ), - ( - Guid::from_str("B5492BB0-E944-20BB-B732-04A36003E452").unwrap(), - 3, - ), - ( - Guid::from_str("5C10E4A4-B549-A159-C440-C5A7EEDF7E54").unwrap(), - 0, - ), - ( - Guid::from_str("C931C839-DC47-E65A-179C-449A7C8E1C3E").unwrap(), - 0, - ), - ( - Guid::from_str("331BF078-984F-EAEB-EA84-B4B9A25AB9CC").unwrap(), - 20, - ), - ( - Guid::from_str("0F383166-E043-4D2D-27CF-09805AA95669").unwrap(), - 0, - ), - ( - Guid::from_str("9F8BF812-FC4A-7588-0CD9-7CA629BD3A38").unwrap(), - 47, - ), - ( - Guid::from_str("4CE75A7B-104C-70D2-9857-58A95A2A210B").unwrap(), - 13, - ), - ( - Guid::from_str("186929D7-DD4B-D61D-A864-E29D8438C13C").unwrap(), - 3, - ), - ( - Guid::from_str("7852A1C2-FE4A-E7BF-FF90-176C55F71D53").unwrap(), - 1, - ), - ( - Guid::from_str("D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209").unwrap(), - 3, - ), - ( - Guid::from_str("DD75E529-2746-A3E0-76D2-109DEADC2C23").unwrap(), - 17, - ), - ( - Guid::from_str("5DA643AF-4749-D37F-8E3E-739805BBC1D9").unwrap(), - 15, - ), - ( - Guid::from_str("EC6C266B-8F4B-C71E-D9E4-0BA307FC4209").unwrap(), - 1, - ), - ( - Guid::from_str("613DF70D-EA47-3FA2-E989-27B79A49410C").unwrap(), - 1, - ), - ( - Guid::from_str("86181D60-844F-64AC-DED3-16AAD6C7EA0D").unwrap(), - 111, - ), - ( - Guid::from_str("5B2CBC8D-E043-A754-BBFC-68A76090A27D").unwrap(), - 2, - ), - ( - Guid::from_str("B7064C5B-F84A-6324-70BF-5B80DDD0F5CD").unwrap(), - 10, - ), - ( - Guid::from_str("686308E7-584C-236B-701B-3984915E2616").unwrap(), - 11, - ), - ( - Guid::from_str("D6BCFF9D-5801-4F49-8212-21E288A8923C").unwrap(), - 10, - ), - ( - Guid::from_str("ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA").unwrap(), - 1, - ), - ( - Guid::from_str("0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0").unwrap(), - 10, - ), - ( - Guid::from_str("834AF935-6C40-58E2-F509-18A37C241096").unwrap(), - 41, - ), - ( - Guid::from_str("6EC18FB6-E242-1B8B-5C21-53B4FE448805").unwrap(), - 1, - ), - ( - Guid::from_str("0685E1B2-C2CF-7342-BBF4-4EA507BA8B75").unwrap(), - 1, - ), - ( - Guid::from_str("3689F564-BA42-1BFD-8972-96BA4EFAD0D5").unwrap(), - 1, - ), - ( - Guid::from_str("81D57D69-AB41-4FE6-EC51-4AAA28B6B7BE").unwrap(), - 118, - ), - ( - Guid::from_str("425E9BD8-464D-BD24-A8AC-1284791764DF").unwrap(), - 47, - ), - ( - Guid::from_str("525DDA59-4849-3212-7859-78B88BE9B870").unwrap(), - 8, - ), - ( - Guid::from_str("325A0726-0847-0F73-328C-E988059D59F1").unwrap(), - 0, - ), - ( - Guid::from_str("27D80E6F-9548-09A6-8D99-919CA40E1890").unwrap(), - 2, - ), - ( - Guid::from_str("E38BD530-8242-EA95-59B1-E3A66AB0EBD8").unwrap(), - 1, - ), - ( - Guid::from_str("E79E7F71-3A49-B0E9-3291-B3880781381B").unwrap(), - 17, - ), - ( - Guid::from_str("FC09C468-8649-9570-D2AC-6389835186C4").unwrap(), - 3, - ), - ( - Guid::from_str("194D0C43-7049-5471-699B-6987E5B090DF").unwrap(), - 15, - ), - ( - Guid::from_str("BD32FEAA-144C-9553-255E-6AB6DDD13210").unwrap(), - 1, - ), - ( - Guid::from_str("8EE1AF23-584E-E14C-52C2-618DB7BE53B9").unwrap(), - 11, - ), - ( - Guid::from_str("EAB762A4-3A4E-99F4-1FEC-C199B2E12482").unwrap(), - 4, - ), - ( - Guid::from_str("BDFDB52E-104D-AC01-8FF3-3681DAA59333").unwrap(), - 5, - ), - ( - Guid::from_str("4F359D50-2F49-E6F6-B285-49A71C633C07").unwrap(), - 0, - ), - ( - Guid::from_str("3EF0A495-E449-0B7E-56D3-43BAD987FF94").unwrap(), - 7, - ), - ( - Guid::from_str("1C1BE3B6-EC11-9FD2-859F-7E85E270996F").unwrap(), - 1, - ), - ( - Guid::from_str("40EB564A-DC11-F510-7E34-D392E76AC9B2").unwrap(), - 3, - ), - ( - Guid::from_str("8A991784-EC43-C0BB-19D1-B38122272D07").unwrap(), - 19, - ), - ( - Guid::from_str("004A8AD7-9746-58E8-B519-A8BAB4467D48").unwrap(), - 18, - ), - ( - Guid::from_str("86F87955-1F4C-3A93-7B08-BA832FB96163").unwrap(), - 2, - ), - ( - Guid::from_str("52BE2F61-0B40-53DA-914F-0D917C85B19F").unwrap(), - 1, - ), - ( - Guid::from_str("367A23A4-C941-EACA-F818-A28FF31B6858").unwrap(), - 5, - ), - ( - Guid::from_str("753F4E80-494B-8870-068C-D6A4DCB67E3C").unwrap(), - 5, - ), - ( - Guid::from_str("F448D01E-684C-2E2F-A453-D0892D108FF1").unwrap(), - 1, - ), - ( - Guid::from_str("F20A68FB-A34B-EF59-B519-A8BA3D44C873").unwrap(), - 2, - ), - ( - Guid::from_str("0EB75099-174E-1AB4-0DFA-CCBBD67F8157").unwrap(), - 1, - ), - ( - Guid::from_str("CD14175E-5129-4E48-A789-7A7078AB0293").unwrap(), - 3, - ), - ( - Guid::from_str("7B472509-0140-3D76-73D6-919D11B4750B").unwrap(), - 1, - ), - ( - Guid::from_str("1B218842-C616-4845-B267-761A002A7A50").unwrap(), - 1, - ), - ( - Guid::from_str("9B9549DC-E74D-C053-88EA-5691395D7C5E").unwrap(), - 2, - ), - ( - Guid::from_str("FB0C82A7-5943-A720-142C-548C50CF2396").unwrap(), - 27, - ), - ( - Guid::from_str("4E7CE782-A543-2333-C513-6BB4F30D3197").unwrap(), - 0, - ), - ( - Guid::from_str("AA1C1EE2-5E42-47AF-D46A-BF89BBA8444C").unwrap(), - 0, - ), - ( - Guid::from_str("7E154A13-A349-E2D5-3C84-4E8D319EFE98").unwrap(), - 2, - ), - ( - Guid::from_str("FA7AF5FC-8342-7650-58E6-A9B9322DA0FF").unwrap(), - 79, - ), - ( - Guid::from_str("ED0A3111-614D-552E-A39A-67AF2C08A1C5").unwrap(), - 17, - ), - ( - Guid::from_str("78BBDFF6-E4A0-50BB-4DB8-184023AFCB60").unwrap(), - 2, - ), - ( - Guid::from_str("F37ABB24-834F-4656-C22D-2F1FFF96AD49").unwrap(), - 5, - ), - ( - Guid::from_str("2923A576-B545-2309-41D8-AE98D86A2FCF").unwrap(), - 5, - ), - ( - Guid::from_str("0769BC5F-AE40-C855-84F1-678E3FF1FF5E").unwrap(), - 1, - ), - ( - Guid::from_str("438C7392-9C4D-8829-BE9B-3D9AC09FFF6E").unwrap(), - 1, - ), - ]), - save_game_class_name: String::from( - "/Game/_Blueprints/BP_SettingsSave.BP_SettingsSave_C", - ), - }, - properties: HashableIndexMap::from([ - ( - String::from("SettingsChanged"), - Property::from(MulticastInlineDelegateProperty { - value: MulticastScriptDelegate { - delegates: vec![ - Delegate::new( - format!("{}BP_ActionTool_WaterGauge_C_2147482315", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plow_C_2147482312", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Plow_Row_Single_C_2147482309", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plow_Row_3_C_2147482305", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plow_5Row_C_2147482301", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plow_Row_5_C_2147482297", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plant_C_2147482293", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plant_Row_C_2147482286", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plant_Row3_C_2147482280", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Plant_Row5_C_2147482274", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Cultivate_C_2147482268", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Cultivate_Row_C_2147482265", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Cultivate_Row3_C_2147482261", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Cultivate_Row5_C_2147482257", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_PlasticRow_C_2147482253", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Purchase_C_2147482249", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Purchase_1x10_C_2147482242", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Purchase_3Row_C_2147482235", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Purchase_5Row_C_2147482228", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Purchase_10x10_C_2147482221", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Modify_C_2147482214", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Row_C_2147482198", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Row3_C_2147482181", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Harvest_C_2147482164", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Harvest_Row_C_2147482161", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Harvest_Row_3_C_2147482157", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Harvest_Row_5_C_2147482153", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_Harvest_Row_C_2147482149", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_AutomatedActionControl_C_2147482145", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_RemovePlaceable_C_2147482142", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_SeedSilo_C_2147482139", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_TractorBarn_C_2147482132", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Sell_C_2147482125", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_FuelStorageTank_C_2147482118", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_ChickenRun_C_2147482115", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_MovePlaceable_C_2147482112", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Beehive_C_2147482109", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_SetPHTool_Row_C_2147482106", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_BiodieselRefinery_C_2147482089", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_OilPress_C_2147482086", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_FlourMill_C_2147482083", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_LargeChickenCoop_C_2147482080", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_CropSign_C_2147482077", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Mulch_C_2147482070", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Mulch_Row_C_2147482054", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Mulch_Row3_C_2147482037", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Warehouse_C_2147482020", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_HarvestSilo_C_2147482013", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_ActionTool_Stockpile_C_2147482008", DELEGATE_PREFIX), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!( - "{}BP_ActionTool_CompostStation_C_2147482001", - DELEGATE_PREFIX - ), - String::from("SettingsChanged_Event"), - ), - Delegate::new( - format!("{}BP_Renders_C_1", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_PlayerPawn_C_2147482331", DELEGATE_PREFIX), - String::from("UpdatedSavedSettings"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478921", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478905", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478890", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478875", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478860", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478303", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478288", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478273", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478258", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478243", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478228", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478141", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478126", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478111", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147478096", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477750", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477735", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477720", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477705", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477690", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477675", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477660", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477645", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477189", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - Delegate::new( - format!("{}BP_AutomatedTool_C_2147477162", DELEGATE_PREFIX), - String::from("SettingsChanged"), - ), - ], - }, - }), - ), - ( - String::from("AudioSettings"), - Property::StructProperty(StructProperty { - guid: Guid::default(), - type_name: String::from("GameAudioSettings"), - value: StructPropertyValue::CustomStruct(HashableIndexMap::from([ - ( - String::from("MasterLevel"), - vec![Property::FloatProperty(FloatProperty { - value: OrderedFloat::from(0.20348908), - })], - ), - ( - String::from("MusicLevel"), - vec![Property::FloatProperty(FloatProperty { - value: OrderedFloat::from(0.1511635), - })], - ), - ( - String::from("SFXLevel"), - vec![Property::FloatProperty(FloatProperty { - value: OrderedFloat::from(0.5436054), - })], - ), - ])), - }), - ), - ( - String::from("GameSettings"), - Property::StructProperty(StructProperty { - guid: Guid::default(), - type_name: String::from("GameSettings"), - value: StructPropertyValue::CustomStruct(HashableIndexMap::from([ - ( - String::from("CurrentSaveSlot"), - vec![Property::StrProperty(StrProperty::from("SAVE2"))], - ), - ( - String::from("LoadTutorial"), - vec![Property::BoolProperty(BoolProperty::new(false))], - ), - ( - String::from("DisplayNewOrders"), - vec![Property::BoolProperty(BoolProperty::new(false))], - ), - ( - String::from("EscapeExitsTool"), - vec![Property::BoolProperty(BoolProperty::new(false))], - ), - ( - String::from("UseDarkMode"), - vec![Property::BoolProperty(BoolProperty::new(true))], - ), - ( - String::from("AnimateDayCycle"), - vec![Property::BoolProperty(BoolProperty::new(false))], - ), - ( - String::from("EnableTractorCollision"), - vec![Property::BoolProperty(BoolProperty::new(false))], - ), - ( - String::from("ShowInventory"), - vec![Property::BoolProperty(BoolProperty::new(true))], - ), - ( - String::from("CameraAngle"), - vec![Property::from(StructProperty { - type_name: String::from("Vector2D"), - guid: Guid::default(), - value: StructPropertyValue::Vector2D(Vector2D { - x: OrderedFloat::from(30.574748247861862), - y: OrderedFloat::from(60.42525175213814), - }), - })], - ), - ])), - }), - ), - ( - String::from("HighScore"), - Property::IntProperty(IntProperty { value: 2649 }), - ), - ]), - } -} - -pub const VECTOR2D_JSON: &str = r#"{ - "header": { - "type": "Version3", - "package_file_version": 522, - "package_file_version_ue5": 1009, - "engine_version": { - "major": 5, - "minor": 3, - "patch": 2, - "change_list": 29314046, - "branch": "++UE5+Release-5.3" - }, - "custom_version_format": 3, - "custom_versions": { - "22D5549C-BE4F-26A8-4607-2194D082B461": 44, - "A35C9162-F74B-8E1C-C712-0EA3F79D21C8": 32, - "240D40CC-7B4E-E9E0-83A2-F99B27C0C0DC": 0, - "E432D8B0-0D4F-891F-B77E-CFACA24AFD36": 10, - "2843C6E1-534D-2CA2-868E-6CA38CBD1764": 0, - "3CC15E37-FB48-E406-F084-00B57E712A26": 4, - "ED68B0E4-E942-94F4-0BDA-31A241BB462E": 40, - "3F74FCCF-8044-B043-DF14-919373201D17": 37, - "B5492BB0-E944-20BB-B732-04A36003E452": 3, - "5C10E4A4-B549-A159-C440-C5A7EEDF7E54": 0, - "C931C839-DC47-E65A-179C-449A7C8E1C3E": 0, - "331BF078-984F-EAEB-EA84-B4B9A25AB9CC": 20, - "0F383166-E043-4D2D-27CF-09805AA95669": 0, - "9F8BF812-FC4A-7588-0CD9-7CA629BD3A38": 47, - "4CE75A7B-104C-70D2-9857-58A95A2A210B": 13, - "186929D7-DD4B-D61D-A864-E29D8438C13C": 3, - "7852A1C2-FE4A-E7BF-FF90-176C55F71D53": 1, - "D4A3AC6E-C14C-EC40-ED8B-86B7C58F4209": 3, - "DD75E529-2746-A3E0-76D2-109DEADC2C23": 17, - "5DA643AF-4749-D37F-8E3E-739805BBC1D9": 15, - "EC6C266B-8F4B-C71E-D9E4-0BA307FC4209": 1, - "613DF70D-EA47-3FA2-E989-27B79A49410C": 1, - "86181D60-844F-64AC-DED3-16AAD6C7EA0D": 111, - "5B2CBC8D-E043-A754-BBFC-68A76090A27D": 2, - "B7064C5B-F84A-6324-70BF-5B80DDD0F5CD": 10, - "686308E7-584C-236B-701B-3984915E2616": 11, - "D6BCFF9D-5801-4F49-8212-21E288A8923C": 10, - "ACD0AEF2-6F41-FE9A-7FAA-6486FCD626FA": 1, - "0B1F4F17-A545-C6B4-E82E-3FB17D91FBD0": 10, - "834AF935-6C40-58E2-F509-18A37C241096": 41, - "6EC18FB6-E242-1B8B-5C21-53B4FE448805": 1, - "0685E1B2-C2CF-7342-BBF4-4EA507BA8B75": 1, - "3689F564-BA42-1BFD-8972-96BA4EFAD0D5": 1, - "81D57D69-AB41-4FE6-EC51-4AAA28B6B7BE": 118, - "425E9BD8-464D-BD24-A8AC-1284791764DF": 47, - "525DDA59-4849-3212-7859-78B88BE9B870": 8, - "325A0726-0847-0F73-328C-E988059D59F1": 0, - "27D80E6F-9548-09A6-8D99-919CA40E1890": 2, - "E38BD530-8242-EA95-59B1-E3A66AB0EBD8": 1, - "E79E7F71-3A49-B0E9-3291-B3880781381B": 17, - "FC09C468-8649-9570-D2AC-6389835186C4": 3, - "194D0C43-7049-5471-699B-6987E5B090DF": 15, - "BD32FEAA-144C-9553-255E-6AB6DDD13210": 1, - "8EE1AF23-584E-E14C-52C2-618DB7BE53B9": 11, - "EAB762A4-3A4E-99F4-1FEC-C199B2E12482": 4, - "BDFDB52E-104D-AC01-8FF3-3681DAA59333": 5, - "4F359D50-2F49-E6F6-B285-49A71C633C07": 0, - "3EF0A495-E449-0B7E-56D3-43BAD987FF94": 7, - "1C1BE3B6-EC11-9FD2-859F-7E85E270996F": 1, - "40EB564A-DC11-F510-7E34-D392E76AC9B2": 3, - "8A991784-EC43-C0BB-19D1-B38122272D07": 19, - "004A8AD7-9746-58E8-B519-A8BAB4467D48": 18, - "86F87955-1F4C-3A93-7B08-BA832FB96163": 2, - "52BE2F61-0B40-53DA-914F-0D917C85B19F": 1, - "367A23A4-C941-EACA-F818-A28FF31B6858": 5, - "753F4E80-494B-8870-068C-D6A4DCB67E3C": 5, - "F448D01E-684C-2E2F-A453-D0892D108FF1": 1, - "F20A68FB-A34B-EF59-B519-A8BA3D44C873": 2, - "0EB75099-174E-1AB4-0DFA-CCBBD67F8157": 1, - "CD14175E-5129-4E48-A789-7A7078AB0293": 3, - "7B472509-0140-3D76-73D6-919D11B4750B": 1, - "1B218842-C616-4845-B267-761A002A7A50": 1, - "9B9549DC-E74D-C053-88EA-5691395D7C5E": 2, - "FB0C82A7-5943-A720-142C-548C50CF2396": 27, - "4E7CE782-A543-2333-C513-6BB4F30D3197": 0, - "AA1C1EE2-5E42-47AF-D46A-BF89BBA8444C": 0, - "7E154A13-A349-E2D5-3C84-4E8D319EFE98": 2, - "FA7AF5FC-8342-7650-58E6-A9B9322DA0FF": 79, - "ED0A3111-614D-552E-A39A-67AF2C08A1C5": 17, - "78BBDFF6-E4A0-50BB-4DB8-184023AFCB60": 2, - "F37ABB24-834F-4656-C22D-2F1FFF96AD49": 5, - "2923A576-B545-2309-41D8-AE98D86A2FCF": 5, - "0769BC5F-AE40-C855-84F1-678E3FF1FF5E": 1, - "438C7392-9C4D-8829-BE9B-3D9AC09FFF6E": 1 - }, - "save_game_class_name": "/Game/_Blueprints/BP_SettingsSave.BP_SettingsSave_C" - }, - "properties": { - "SettingsChanged": { - "type": "MulticastInlineDelegateProperty", - "value": { - "delegates": [ - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_WaterGauge_C_2147482315", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_C_2147482312", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_Single_C_2147482309", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_3_C_2147482305", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_5Row_C_2147482301", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plow_Row_5_C_2147482297", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_C_2147482293", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row_C_2147482286", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row3_C_2147482280", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Plant_Row5_C_2147482274", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_C_2147482268", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row_C_2147482265", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row3_C_2147482261", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Cultivate_Row5_C_2147482257", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_PlasticRow_C_2147482253", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_C_2147482249", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_1x10_C_2147482242", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_3Row_C_2147482235", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_5Row_C_2147482228", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Purchase_10x10_C_2147482221", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Modify_C_2147482214", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Row_C_2147482198", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Row3_C_2147482181", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_C_2147482164", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_C_2147482161", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_3_C_2147482157", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_5_C_2147482153", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Harvest_Row_C_2147482149", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_AutomatedActionControl_C_2147482145", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_RemovePlaceable_C_2147482142", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_SeedSilo_C_2147482139", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_TractorBarn_C_2147482132", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Sell_C_2147482125", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_FuelStorageTank_C_2147482118", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_ChickenRun_C_2147482115", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_MovePlaceable_C_2147482112", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Beehive_C_2147482109", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_SetPHTool_Row_C_2147482106", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_BiodieselRefinery_C_2147482089", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_OilPress_C_2147482086", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_FlourMill_C_2147482083", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_LargeChickenCoop_C_2147482080", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_CropSign_C_2147482077", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_C_2147482070", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_Row_C_2147482054", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Mulch_Row3_C_2147482037", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Warehouse_C_2147482020", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_HarvestSilo_C_2147482013", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_Stockpile_C_2147482008", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_ActionTool_CompostStation_C_2147482001", - "function_name": "SettingsChanged_Event" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_Renders_C_1", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_PlayerPawn_C_2147482331", - "function_name": "UpdatedSavedSettings" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478921", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478905", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478890", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478875", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478860", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478303", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478288", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478273", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478258", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478243", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478228", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478141", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478126", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478111", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147478096", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477750", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477735", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477720", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477705", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477690", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477675", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477660", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477645", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477189", - "function_name": "SettingsChanged" - }, - { - "object": "/Game/DefaultMap.DefaultMap:PersistentLevel.BP_AutomatedTool_C_2147477162", - "function_name": "SettingsChanged" - } - ] - } - }, - "AudioSettings": { - "type": "StructProperty", - "type_name": "GameAudioSettings", - "CustomStruct": { - "MasterLevel": [ - { - "type": "FloatProperty", - "value": 0.20348908 - } - ], - "MusicLevel": [ - { - "type": "FloatProperty", - "value": 0.1511635 - } - ], - "SFXLevel": [ - { - "type": "FloatProperty", - "value": 0.5436054 - } - ] - } - }, - "GameSettings": { - "type": "StructProperty", - "type_name": "GameSettings", - "CustomStruct": { - "CurrentSaveSlot": [ - { - "type": "StrProperty", - "value": "SAVE2" - } - ], - "LoadTutorial": [ - { - "type": "BoolProperty", - "value": false - } - ], - "DisplayNewOrders": [ - { - "type": "BoolProperty", - "value": false - } - ], - "EscapeExitsTool": [ - { - "type": "BoolProperty", - "value": false - } - ], - "UseDarkMode": [ - { - "type": "BoolProperty", - "value": true - } - ], - "AnimateDayCycle": [ - { - "type": "BoolProperty", - "value": false - } - ], - "EnableTractorCollision": [ - { - "type": "BoolProperty", - "value": false - } - ], - "ShowInventory": [ - { - "type": "BoolProperty", - "value": true - } - ], - "CameraAngle": [ - { - "type": "StructProperty", - "type_name": "Vector2D", - "Vector2D": { - "x": 30.574748247861862, - "y": 60.42525175213814 - } - } - ] - } - }, - "HighScore": { - "type": "IntProperty", - "value": 2649 - } - } -}"#; diff --git a/tests/gvas.rs b/tests/gvas.rs deleted file mode 100644 index f87ba2b..0000000 --- a/tests/gvas.rs +++ /dev/null @@ -1,163 +0,0 @@ -mod common; -use common::*; -mod gvas_tests; -use gvas::{ - GvasFile, - game_version::{DeserializedGameVersion, GameVersion, PalworldCompressionType}, -}; -use std::{collections::HashMap, fs, io::Cursor, path::Path}; - -fn test_gvas_file(path: &str) -> GvasFile { - test_gvas_file_(path, GameVersion::Default, &HashMap::new()) -} - -fn test_gvas_file_( - path: &str, - game_version: GameVersion, - hints: &HashMap, -) -> GvasFile { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(path); - - // Read the file in to a Vec - let data = fs::read(path).expect("Read test asset"); - - // Convert the Vec to a GvasFile - let mut cursor = Cursor::new(data); - let file = GvasFile::read_with_hints(&mut cursor, game_version, hints).expect("Read GvasFile"); - - // Convert the GvasFile back to a Vec - let mut writer = Cursor::new(Vec::new()); - file.write(&mut writer).expect("Write GvasFile"); - - // Compare the two Vecs - if match file.deserialized_game_version { - DeserializedGameVersion::Default => true, - DeserializedGameVersion::Palworld(PalworldCompressionType::Zlib) => false, - DeserializedGameVersion::Palworld(PalworldCompressionType::ZlibTwice) => false, - _ => unimplemented!(), - } { - assert_eq!(cursor.get_ref(), writer.get_ref()); - } - - // Read the file back in again - let mut reader = Cursor::new(writer.into_inner()); - let file2 = GvasFile::read_with_hints(&mut reader, game_version, hints).expect("Read GvasFile"); - - // Compare the two GvasFiles - assert_eq!(file, file2); - - // Pass the file back for optional verification - file -} - -#[test] -fn assert_failed() { - test_gvas_file(ASSERT_FAILED_PATH); -} - -#[test] -fn component8() { - test_gvas_file(COMPONENT8_PATH); -} - -#[test] -fn delegate() { - assert_eq!(test_gvas_file(DELEGATE_PATH), delegate::expected()); -} - -#[test] -fn enum_array() { - test_gvas_file(ENUM_ARRAY_PATH); -} - -#[test] -fn features_01() { - test_gvas_file_(FEATURES_01_PATH, GameVersion::Default, &features::hints()); -} - -#[test] -fn options() { - assert_eq!(test_gvas_file(OPTIONS_PATH), options::expected()); -} - -#[test] -fn package_version_524() { - test_gvas_file(PACKAGE_VERSION_524_PATH); -} - -#[test] -fn palworld_zlib() { - test_gvas_file_(PALWORLD_ZLIB_PATH, GameVersion::Palworld, &HashMap::new()); -} - -#[test] -fn palworld_zlib_twice() { - test_gvas_file_( - PALWORLD_ZLIB_TWICE_PATH, - GameVersion::Palworld, - &palworld::hints(), - ); -} - -#[test] -fn profile_0() { - test_gvas_file_(PROFILE_0_PATH, GameVersion::Default, &profile0::hints()); -} - -#[test] -fn regression_01() { - test_gvas_file(REGRESSION_01_PATH); -} - -#[test] -fn ro_64bit_fav() { - test_gvas_file(RO_64BIT_FAV_PATH); -} - -#[test] -fn saveslot03() { - assert_eq!( - test_gvas_file_(SAVESLOT_03_PATH, GameVersion::Default, &saveslot3::hints()), - saveslot3::expected() - ); -} - -#[test] -fn slot1() { - assert_eq!(test_gvas_file(SLOT1_PATH), slot1::expected()); -} - -#[test] -fn slot2() { - test_gvas_file(SLOT2_PATH); -} - -#[test] -fn slot3() { - test_gvas_file(SLOT3_PATH); -} - -#[test] -fn string_table_entry() { - test_gvas_file(STRING_TABLE_ENTRY); -} - -#[test] -fn tagcontainer() { - test_gvas_file(TAGCONTAINER_PATH); -} - -#[test] -fn text_property_noarray() { - test_gvas_file(TEXT_PROPERTY_NOARRAY); -} - -#[test] -fn transform() { - test_gvas_file(TRANSFORM_PATH); -} - -#[test] -fn vector2d() { - assert_eq!(test_gvas_file(VECTOR2D_PATH), vector2d::expected()); -} diff --git a/tests/gvas_tests/errors.rs b/tests/gvas_tests/errors.rs deleted file mode 100644 index 04c39de..0000000 --- a/tests/gvas_tests/errors.rs +++ /dev/null @@ -1,304 +0,0 @@ -use gvas::{ - GvasFile, - error::{DeserializeError, Error}, - game_version::GameVersion, - properties::{ - PropertyOptions, array_property::ArrayProperty, enum_property::EnumProperty, - map_property::MapProperty, set_property::SetProperty, str_property::StrProperty, - }, - types::map::HashableIndexMap, -}; -use std::{collections::HashMap, io::Cursor}; - -const UNEXPECTED_EOF: [u8; 0] = []; - -#[test] -fn test_unexpected_eof() { - let mut reader = Cursor::new(UNEXPECTED_EOF); - let result = GvasFile::read(&mut reader, GameVersion::Default); - match result { - Err(Error::Io(e)) => { - assert_eq!(e.to_string(), "failed to fill whole buffer"); - } - _ => panic!("Unexpected result {result:?}"), - } -} - -const INVALID_HEADER: [u8; 4] = *b"GVAZ"; - -#[test] -fn test_invalid_header() { - let mut reader = Cursor::new(INVALID_HEADER); - let result = GvasFile::read(&mut reader, GameVersion::Default); - match result { - Err(Error::Deserialize(DeserializeError::InvalidHeader(reason))) => { - assert_eq!(reason.into_string(), "File type 1514231367 not recognized"); - } - _ => panic!("Unexpected result {result:?}"), - } -} - -const INVALID_ARRAY_INDEX: [u8; 8] = [ - 0, 0, 0, 0, // length - 1, 0, 0, 0, // array_index -]; - -#[test] -fn test_invalid_array_index() { - // StrProperty - let mut reader = Cursor::new(INVALID_ARRAY_INDEX); - let result = StrProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 4); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // EnumProperty - let mut reader = Cursor::new(INVALID_ARRAY_INDEX); - let result = EnumProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 4); - } - _ => panic!("Unexpected result {result:?}"), - }; - - let mut options = PropertyOptions { - hints: &HashMap::new(), - properties_stack: &mut Vec::new(), - custom_versions: &HashableIndexMap::new(), - }; - - // ArrayProperty - let mut reader = Cursor::new(INVALID_ARRAY_INDEX); - let result = ArrayProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 4); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // SetProperty - let mut reader = Cursor::new(INVALID_ARRAY_INDEX); - let result = SetProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 4); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // MapProperty - let mut reader = Cursor::new(INVALID_ARRAY_INDEX); - let result = MapProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidArrayIndex(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 4); - } - _ => panic!("Unexpected result {result:?}"), - }; -} - -const INVALID_TERMINATOR: [u8; 9] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, // terminator -]; - -const INVALID_TERMINATOR_ENUM: [u8; 14] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // enum_type - 1, // terminator -]; - -const INVALID_TERMINATOR_MAP: [u8; 19] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // key_type - 1, 0, 0, 0, 0, // value_type - 1, // terminator -]; - -#[test] -fn test_invalid_terminator() { - // StrProperty - let mut reader = Cursor::new(INVALID_TERMINATOR); - let result = StrProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 8); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // EnumProperty - let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); - let result = EnumProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 13); - } - _ => panic!("Unexpected result {result:?}"), - }; - - let mut options = PropertyOptions { - hints: &HashMap::new(), - properties_stack: &mut Vec::new(), - custom_versions: &HashableIndexMap::new(), - }; - - // ArrayProperty - let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); - let result = ArrayProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 13); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // SetProperty - let mut reader = Cursor::new(INVALID_TERMINATOR_ENUM); - let result = SetProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 13); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // MapProperty - let mut reader = Cursor::new(INVALID_TERMINATOR_MAP); - let result = MapProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidTerminator(value, position))) => { - assert_eq!(value, 1); - assert_eq!(position, 18); - } - _ => panic!("Unexpected result {result:?}"), - }; -} - -const INVALID_LENGTH_STR: [u8; 13] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 0, // terminator - 0, 0, 0, 0, // string -]; - -const INVALID_LENGTH_ENUM: [u8; 19] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // enum_type - 0, // terminator - 1, 0, 0, 0, 0, // string -]; - -const INVALID_LENGTH_ARRAY: [u8; 18] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // property_type - 0, // terminator - 0, 0, 0, 0, // element_count -]; - -const INVALID_LENGTH_SET: [u8; 22] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // property_type - 0, // terminator - 0, 0, 0, 0, // allocation_flags - 0, 0, 0, 0, // element_count -]; - -const INVALID_LENGTH_MAP: [u8; 27] = [ - 0, 0, 0, 0, // length - 0, 0, 0, 0, // array_index - 1, 0, 0, 0, 0, // key_type - 1, 0, 0, 0, 0, // value_type - 0, // terminator - 0, 0, 0, 0, // allocation_flags - 0, 0, 0, 0, // element_count -]; - -#[test] -fn test_invalid_length() { - // StrProperty - let mut reader = Cursor::new(INVALID_LENGTH_STR); - let result = StrProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { - assert_eq!(expected, 0); - assert_eq!(read, 4); - assert_eq!(position, 9); - } - _ => panic!("Unexpected result {result:?}"), - } - - // EnumProperty - let mut reader = Cursor::new(INVALID_LENGTH_ENUM); - let result = EnumProperty::read_header(&mut reader); - match result { - Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { - assert_eq!(expected, 0); - assert_eq!(read, 5); - assert_eq!(position, 14); - } - _ => panic!("Unexpected result {result:?}"), - } - - let mut options = PropertyOptions { - hints: &HashMap::new(), - properties_stack: &mut Vec::new(), - custom_versions: &HashableIndexMap::new(), - }; - - // ArrayProperty - let mut reader = Cursor::new(INVALID_LENGTH_ARRAY); - let result = ArrayProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { - assert_eq!(expected, 0); - assert_eq!(read, 4); - assert_eq!(position, 14); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // SetProperty - let mut reader = Cursor::new(INVALID_LENGTH_SET); - let result = SetProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { - assert_eq!(expected, 0); - assert_eq!(read, 8); - assert_eq!(position, 14); - } - _ => panic!("Unexpected result {result:?}"), - }; - - // MapProperty - let mut reader = Cursor::new(INVALID_LENGTH_MAP); - let result = MapProperty::read_header(&mut reader, &mut options); - match result { - Err(Error::Deserialize(DeserializeError::InvalidValueSize(expected, read, position))) => { - assert_eq!(expected, 0); - assert_eq!(read, 8); - assert_eq!(position, 19); - } - _ => panic!("Unexpected result {result:?}"), - }; -} diff --git a/tests/gvas_tests/mod.rs b/tests/gvas_tests/mod.rs deleted file mode 100644 index d3197ad..0000000 --- a/tests/gvas_tests/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod errors; -mod name_arrayindex; -mod package_version_524; -mod package_version_525; -mod regression_01; -mod test_cursor; -mod test_file; -mod test_guid; -mod test_property; diff --git a/tests/gvas_tests/name_arrayindex.rs b/tests/gvas_tests/name_arrayindex.rs deleted file mode 100644 index a2f0b6d..0000000 --- a/tests/gvas_tests/name_arrayindex.rs +++ /dev/null @@ -1,43 +0,0 @@ -use gvas::cursor_ext::ReadExt; -use gvas::properties::{PropertyOptions, PropertyTrait, name_property::NameProperty}; -use gvas::types::map::HashableIndexMap; -use std::collections::HashMap; -use std::io::Cursor; - -#[test] -fn name_property_with_array_index() { - let data = vec![ - 0x0d, 0x00, 0x00, 0x00, 0x4e, 0x61, 0x6d, 0x65, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, - 0x79, 0x00, 0x1d, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, - 0x51, 0x55, 0x39, 0x31, 0x5f, 0x49, 0x6e, 0x76, 0x65, 0x73, 0x74, 0x69, 0x67, 0x61, 0x74, - 0x65, 0x54, 0x6f, 0x77, 0x65, 0x72, 0x5f, 0x42, 0x32, 0x00, - ]; - - // Convert the Vec to a NameProperty - let mut cursor = Cursor::new(data); - let property_type = cursor.read_fstring().expect("Failed to read property type"); - assert_eq!(Some(String::from("NameProperty")), property_type); - let prop = NameProperty::read(&mut cursor, true).expect("Failed to read NameProperty"); - - // Compare the parsed value to its expected value - assert_eq!( - NameProperty { - array_index: 1, - value: Some("QU91_InvestigateTower_B2".into()), - }, - prop - ); - - // Convert the NameProperty back to a Vec - let mut options = PropertyOptions { - hints: &HashMap::new(), - properties_stack: &mut Vec::new(), - custom_versions: &HashableIndexMap::new(), - }; - let mut writer = Cursor::new(Vec::new()); - prop.write(&mut writer, true, &mut options) - .expect("Failed to serialize gvas file"); - - // Compare the two Vecs - assert_eq!(cursor.get_ref(), writer.get_ref()); -} diff --git a/tests/gvas_tests/package_version_524.rs b/tests/gvas_tests/package_version_524.rs deleted file mode 100644 index 214dc58..0000000 --- a/tests/gvas_tests/package_version_524.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::common::PACKAGE_VERSION_524_PATH; -use gvas::GvasFile; -use gvas::game_version::GameVersion; -use std::{ - fs::File, - io::{Cursor, Read}, - path::Path, -}; - -#[test] -fn write_slot3() { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(PACKAGE_VERSION_524_PATH); - let mut file = File::open(path).expect("Failed to open test asset"); - - // Read the file in to a Vec - let mut data = Vec::new(); - file.read_to_end(&mut data) - .expect("Failed to read test asset"); - - // Convert the Vec to a GvasFile - let mut cursor = Cursor::new(data); - let file = - GvasFile::read(&mut cursor, GameVersion::Default).expect("Failed to parse gvas file"); - - // Convert the GvasFile back to a Vec - let mut writer = Cursor::new(Vec::new()); - file.write(&mut writer) - .expect("Failed to serialize gvas file"); - - // Compare the two Vecs - assert_eq!(cursor.get_ref(), writer.get_ref()); - - // Read the file back in again - let mut reader = Cursor::new(writer.get_ref().to_owned()); - let read_back = GvasFile::read(&mut reader, GameVersion::Default) - .expect("Failed to parse serialized save file"); - - // Compare the two GvasFiles - assert_eq!(file, read_back); -} diff --git a/tests/gvas_tests/package_version_525.rs b/tests/gvas_tests/package_version_525.rs deleted file mode 100644 index 18266a8..0000000 --- a/tests/gvas_tests/package_version_525.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::common::PACKAGE_VERSION_525_PATH; -use gvas::GvasFile; -use gvas::game_version::GameVersion; -use std::fs::File; -use std::io::{Cursor, Read}; -use std::path::Path; - -#[test] -fn package_version_525() { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(PACKAGE_VERSION_525_PATH); - let mut file = File::open(path).expect("Failed to open test asset"); - - // Read the file in to a Vec - let mut data = Vec::new(); - file.read_to_end(&mut data) - .expect("Failed to read test asset"); - - // Convert the Vec to a GvasFile - let mut cursor = Cursor::new(data); - let file = - GvasFile::read(&mut cursor, GameVersion::Default).expect("Failed to parse gvas file"); - - // Convert the GvasFile back to a Vec - let mut writer = Cursor::new(Vec::new()); - file.write(&mut writer) - .expect("Failed to serialize gvas file"); - - // Compare the two Vecs - assert_eq!(cursor.get_ref(), writer.get_ref()); - - // Read the file back in again - let mut reader = Cursor::new(writer.get_ref().to_owned()); - let read_back = GvasFile::read(&mut reader, GameVersion::Default) - .expect("Failed to parse serialized save file"); - - // Compare the two GvasFiles - assert_eq!(file, read_back); -} diff --git a/tests/gvas_tests/regression_01.rs b/tests/gvas_tests/regression_01.rs deleted file mode 100644 index dfc6037..0000000 --- a/tests/gvas_tests/regression_01.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gvas::game_version::GameVersion; -use std::{ - fs::File, - io::{Cursor, Read}, - path::Path, -}; - -use gvas::GvasFile; - -#[test] -fn regression_01_guid() { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/test/regression_01.bin"); - let mut file = File::open(path).expect("Failed to open test asset"); - - // Read the file in to a Vec - let mut data = Vec::new(); - file.read_to_end(&mut data) - .expect("Failed to read test asset"); - - // Convert the Vec to a GvasFile - let mut cursor = Cursor::new(data); - let file = - GvasFile::read(&mut cursor, GameVersion::Default).expect("Failed to parse gvas file"); - - // Convert the GvasFile back to a Vec - let mut writer = Cursor::new(Vec::new()); - file.write(&mut writer).expect("Failed to write test asset"); - - // Compare the two Vecs - assert_eq!(cursor.get_ref(), writer.get_ref()); - - // Read the file back in again - let mut cursor = Cursor::new(writer.get_ref().to_owned()); - let read_back = - GvasFile::read(&mut cursor, GameVersion::Default).expect("Failed to read written asset"); - - // Compare the two GvasFiles - assert_eq!(file, read_back); - - let original_guid = file - .properties - .get("Thing") - .expect("Failed to get test property in original asset") - .get_struct() - .expect("Failed to cast property from original asset to the correct type") - .value - .get_guid() - .expect("Failed to get property from original asset as Guid"); - - let written_guid = read_back - .properties - .get("Thing") - .expect("Failed to get test property in written asset") - .get_struct() - .expect("Failed to cast property from written asset to the correct type") - .value - .get_guid() - .expect("Failed to get property from written asset as Guid"); - - assert_eq!(original_guid, written_guid); -} diff --git a/tests/gvas_tests/test_cursor.rs b/tests/gvas_tests/test_cursor.rs deleted file mode 100644 index 1125ab1..0000000 --- a/tests/gvas_tests/test_cursor.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::io::Cursor; - -use gvas::{ - cursor_ext::{ReadExt, WriteExt}, - error::Error, -}; - -#[test] -fn test_write_string() -> Result<(), Error> { - // ASCII - let mut cursor = Cursor::new(Vec::new()); - cursor.write_string("test")?; - assert_eq!( - cursor.get_ref(), - &[5u8, 0u8, 0u8, 0u8, b't', b'e', b's', b't', 0u8], - ); - - // Non-ASCII - let mut cursor = Cursor::new(Vec::new()); - cursor.write_string("\u{A7}")?; - assert_eq!( - cursor.get_ref(), - &[0xfeu8, 0xffu8, 0xffu8, 0xffu8, 0xa7u8, 0u8, 0u8, 0u8], - ); - - Ok(()) -} - -#[test] -fn test_write_fstring() -> Result<(), Error> { - // ASCII - let mut cursor = Cursor::new(Vec::new()); - cursor.write_fstring(Some("test"))?; - assert_eq!( - cursor.get_ref(), - &[5u8, 0u8, 0u8, 0u8, b't', b'e', b's', b't', 0u8], - ); - - // Non-ASCII - let mut cursor = Cursor::new(Vec::new()); - cursor.write_fstring(Some("\u{A7}"))?; - assert_eq!( - cursor.get_ref(), - &[0xfeu8, 0xffu8, 0xffu8, 0xffu8, 0xa7u8, 0u8, 0u8, 0u8], - ); - - // Null - let mut cursor = Cursor::new(Vec::new()); - cursor.write_fstring(None)?; - assert_eq!(cursor.get_ref(), &[0u8; 4],); - - Ok(()) -} - -#[test] -fn test_read_string() -> Result<(), Error> { - // ASCII - let mut cursor = Cursor::new(vec![5u8, 0u8, 0u8, 0u8, b't', b'e', b's', b't', 0u8]); - let string = cursor.read_string()?; - assert_eq!(string, "test"); - - // Non-ASCII - let mut cursor = Cursor::new(vec![0xfeu8, 0xffu8, 0xffu8, 0xffu8, 0xa7u8, 0u8, 0u8, 0u8]); - let string = cursor.read_string()?; - assert_eq!(string, "\u{A7}"); - - // Null - let mut cursor = Cursor::new(vec![0u8; 4]); - let string = cursor.read_string().expect_err("Expected err").to_string(); - assert_eq!(string, "Invalid string size 0 at position 0x0"); - - // Missing null terminator - let mut cursor = Cursor::new(vec![1u8, 0u8, 0u8, 0u8, b't']); - let string = cursor.read_string().expect_err("Expected err").to_string(); - assert_eq!(string, "Invalid string terminator 116 at position 0x5"); - - // Missing null terminator, UTF-16 - let mut cursor = Cursor::new(vec![0xffu8, 0xffu8, 0xffu8, 0xffu8, b't', b'e']); - let string = cursor.read_string().expect_err("Expected err").to_string(); - assert_eq!(string, "Invalid string terminator 25972 at position 0x6"); - - Ok(()) -} - -#[test] -fn test_read_fstring() -> Result<(), Error> { - // ASCII - let mut cursor = Cursor::new(vec![5u8, 0u8, 0u8, 0u8, b't', b'e', b's', b't', 0u8]); - let string = cursor.read_fstring()?.expect("Expected Some"); - assert_eq!(string, "test"); - - // Non-ASCII - let mut cursor = Cursor::new(vec![0xfeu8, 0xffu8, 0xffu8, 0xffu8, 0xa7u8, 0u8, 0u8, 0u8]); - let string = cursor.read_fstring()?.expect("Expected Some"); - assert_eq!(string, "\u{A7}"); - - // Null - let mut cursor = Cursor::new(vec![0u8; 4]); - let string = cursor.read_fstring()?; - assert_eq!(string, None); - - // Missing null terminator - let mut cursor = Cursor::new(vec![1u8, 0u8, 0u8, 0u8, b't']); - let string = cursor.read_fstring().expect_err("Expected err").to_string(); - assert_eq!(string, "Invalid string terminator 116 at position 0x5"); - - // Missing null terminator, UTF-16 - let mut cursor = Cursor::new(vec![0xffu8, 0xffu8, 0xffu8, 0xffu8, b't', b'e']); - let string = cursor.read_fstring().expect_err("Expected err").to_string(); - assert_eq!(string, "Invalid string terminator 25972 at position 0x6"); - - Ok(()) -} diff --git a/tests/gvas_tests/test_file.rs b/tests/gvas_tests/test_file.rs deleted file mode 100644 index c17d425..0000000 --- a/tests/gvas_tests/test_file.rs +++ /dev/null @@ -1,58 +0,0 @@ -use std::io::Cursor; - -use byteorder::{LittleEndian, WriteBytesExt}; - -use gvas::game_version::GameVersion; -use gvas::{FILE_TYPE_GVAS, GvasFile, GvasHeader, error::Error}; - -#[test] -fn test_file_err() { - let buf = [0; 4]; - - // Read GvasFile from Vec - let mut reader = Cursor::new(buf); - let err = - GvasFile::read(&mut reader, GameVersion::Default).expect_err("Expected file type error"); - assert_eq!( - err.to_string(), - "Invalid header: File type 0 not recognized" - ); - - // Read GvasHeader from Vec - let mut reader = Cursor::new(buf); - let err = GvasHeader::read(&mut reader).expect_err("Expected file type error"); - assert_eq!( - err.to_string(), - "Invalid header: File type 0 not recognized" - ); -} - -#[test] -fn test_version_err() -> Result<(), Error> { - let buf = { - let mut cursor = Cursor::new(Vec::new()); - cursor.write_u32::(FILE_TYPE_GVAS)?; - cursor.write_u32::(0x7DC2293F)?; - cursor.into_inner() - }; - let buf = buf.as_slice(); - - // Read GvasFile from &[u8] - let mut reader = Cursor::new(buf); - let err = - GvasFile::read(&mut reader, GameVersion::Default).expect_err("Expected file type error"); - assert_eq!( - err.to_string(), - "Invalid header: GVAS version 2109876543 not supported" - ); - - // Read GvasHeader from &[u8] - let mut reader = Cursor::new(buf); - let err = GvasHeader::read(&mut reader).expect_err("Expected file type error"); - assert_eq!( - err.to_string(), - "Invalid header: GVAS version 2109876543 not supported" - ); - - Ok(()) -} diff --git a/tests/gvas_tests/test_property.rs b/tests/gvas_tests/test_property.rs deleted file mode 100644 index 4ff236d..0000000 --- a/tests/gvas_tests/test_property.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::{collections::HashMap, io::Cursor}; - -use gvas::{ - cursor_ext::ReadExt, - properties::{ - Property, PropertyOptions, PropertyTrait, - array_property::ArrayProperty, - enum_property::EnumProperty, - int_property::{ - BoolProperty, ByteProperty, BytePropertyValue, DoubleProperty, FloatProperty, - Int8Property, Int16Property, Int64Property, IntProperty, UInt16Property, - UInt32Property, UInt64Property, - }, - map_property::MapProperty, - set_property::SetProperty, - str_property::StrProperty, - struct_property::{StructProperty, StructPropertyValue}, - struct_types::VectorF, - text_property::TextProperty, - }, - types::{Guid, map::HashableIndexMap}, -}; - -use gvas::properties::text_property::FText; - -macro_rules! test_property { - ($function_name:ident, $type:ident, $property_value:expr) => { - #[test] - fn $function_name() { - let property: $type = $property_value; - - let mut options = PropertyOptions { - hints: &HashMap::new(), - properties_stack: &mut Vec::new(), - custom_versions: &HashableIndexMap::new(), - }; - - // Export the property to a byte array - let mut writer = Cursor::new(Vec::new()); - property - .write(&mut writer, true, &mut options) - .expect(concat!("Failed to serialize {}", stringify!($ty))); - - // Import the property from a byte array - let mut reader = Cursor::new(writer.get_ref().to_owned()); - let property_type = reader - .read_string() - .expect(&format!("Read {}", stringify!(property))); - assert_eq!(property_type, stringify!($type)); - let imported = Property::new(&mut reader, &property_type, true, &mut options, None) - .expect(&format!("Reading {} from {:?}", property_type, reader)); - - assert_eq!(writer, reader); - assert_eq!(Property::$type(property), imported); - } - }; -} - -test_property!(test_int8, Int8Property, Int8Property::new(i8::MAX)); -test_property!( - test_byte, - ByteProperty, - ByteProperty::new( - Some(String::from("Test ByteProperty")), - BytePropertyValue::Byte(2) - ) -); -test_property!( - test_byte_namespaced, - ByteProperty, - ByteProperty::new( - Some(String::from("Test NamespacedByteproperty")), - BytePropertyValue::Namespaced(String::from("TestEnum::Value0")) - ) -); -test_property!(test_int16, Int16Property, Int16Property::new(i16::MAX)); -test_property!(test_uint16, UInt16Property, UInt16Property::new(u16::MAX)); -test_property!(test_int, IntProperty, IntProperty::new(i32::MAX)); -test_property!(test_uint32, UInt32Property, UInt32Property::new(u32::MAX)); -test_property!(test_int64, Int64Property, Int64Property::new(i64::MAX)); -test_property!(test_uint64, UInt64Property, UInt64Property::new(u64::MAX)); -test_property!(test_float, FloatProperty, FloatProperty::new(1234f32)); -test_property!(test_double, DoubleProperty, DoubleProperty::new(1234f64)); -test_property!(test_bool, BoolProperty, BoolProperty::new(true)); -test_property!(test_str, StrProperty, StrProperty::from("test string")); - -// EnumProperty -test_property!( - test_enum, - EnumProperty, - EnumProperty::new(Some(String::from("type")), String::from("value")) -); - -// StructProperty -test_property!( - test_struct, - StructProperty, - StructProperty::new( - Guid::default(), - "Vector".to_string(), - StructPropertyValue::from(VectorF::new(0f32, 1f32, 2f32)) - ) -); - -// ArrayProperty -test_property!( - test_array_empty, - ArrayProperty, - ArrayProperty::new(String::from("FloatProperty"), None, vec![]).expect("ArrayProperty::new") -); - -test_property!( - test_array_float, - ArrayProperty, - ArrayProperty::new( - String::from("FloatProperty"), - None, - vec![ - Property::from(FloatProperty::new(0f32)), - Property::from(FloatProperty::new(1f32)), - ], - ) - .expect("ArrayProperty::new") -); - -test_property!( - test_array_vector, - ArrayProperty, - ArrayProperty::new( - String::from("StructProperty"), - Some(( - "FieldName".to_string(), - String::from("Vector"), - Guid::from(0u128) - )), - vec![ - Property::from(StructPropertyValue::from(VectorF::new(0f32, 1f32, 2f32))), - Property::from(StructPropertyValue::from(VectorF::new(3f32, 4f32, 5f32))), - ], - ) - .expect("ArrayProperty::new") -); - -// TextProperty -test_property!( - test_array_text, - ArrayProperty, - ArrayProperty::new( - String::from("TextProperty"), - None, - vec![ - Property::from(TextProperty::new(FText::new_none(0, None))), - Property::from(TextProperty::new(FText::new_base( - 0, - Some(String::from("identifier")), - Some(String::from("{0}
{1}")), - Some(String::from("test
test")) - ))), - ] - ) - .expect("ArrayProperty::new") -); - -// SetProperty -test_property!( - test_set, - SetProperty, - SetProperty::new( - String::from("FloatProperty"), - 0, - vec![Property::from(FloatProperty::new(4321f32))] - ) -); - -// MapProperty -test_property!( - test_map, - MapProperty, - MapProperty::new( - String::from("StrProperty"), - String::from("FloatProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(StrProperty::from("key1")), - Property::from(FloatProperty::new(-1f32)), - ), - ( - Property::from(StrProperty::from("key2")), - Property::from(FloatProperty::new(0.5f32)), - ), - ]), - ) -); diff --git a/tests/serde.rs b/tests/serde.rs deleted file mode 100644 index 8fab273..0000000 --- a/tests/serde.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod common; -mod serde_tests; diff --git a/tests/serde_tests/mod.rs b/tests/serde_tests/mod.rs deleted file mode 100644 index 6fec22c..0000000 --- a/tests/serde_tests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod serde_json_round_trip; -mod serde_json_template; diff --git a/tests/serde_tests/serde_json_round_trip.rs b/tests/serde_tests/serde_json_round_trip.rs deleted file mode 100644 index e6e9fd3..0000000 --- a/tests/serde_tests/serde_json_round_trip.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::common::*; -use gvas::{GvasFile, game_version::GameVersion}; -use std::{collections::HashMap, fs::File, path::Path}; - -fn test_file_with_hints(path: &str, hints: &HashMap) { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(path); - let mut file = File::open(path).expect("Open test asset"); - let file = - GvasFile::read_with_hints(&mut file, GameVersion::Default, hints).expect("Parse gvas file"); - let value = serde_json::to_string(&file).expect("Deserialize"); - let from_value = serde_json::from_str::(value.as_str()).expect("Serialize"); - assert_eq!(file, from_value); -} - -fn test_file(path: &str) { - test_file_with_hints(path, &HashMap::new()); -} - -#[test] -fn serde_assert_failed() { - test_file(ASSERT_FAILED_PATH); -} - -#[ignore] // This test takes ~5 seconds to run -#[test] -fn serde_component8() { - test_file(COMPONENT8_PATH); -} - -#[test] -fn serde_delgate() { - test_file(DELEGATE_PATH); -} - -#[ignore] // This test takes ~5 seconds to run -#[test] -fn serde_enum_array() { - test_file(ENUM_ARRAY_PATH); -} - -#[test] -fn serde_features_01() { - test_file_with_hints(FEATURES_01_PATH, &features::hints()); -} - -#[test] -fn serde_options() { - test_file(OPTIONS_PATH); -} - -#[ignore] // This test takes ~5 seconds to run -#[test] -fn serde_package_version_524() { - test_file(PACKAGE_VERSION_524_PATH); -} - -#[test] -fn serde_profile_0() { - test_file_with_hints(PROFILE_0_PATH, &profile0::hints()); -} - -#[test] -fn serde_regression_01() { - test_file(REGRESSION_01_PATH); -} - -#[test] -fn serde_ro_64bit() { - test_file(RO_64BIT_FAV_PATH); -} - -#[test] -fn serde_saveslot_03() { - test_file_with_hints(SAVESLOT_03_PATH, &saveslot3::hints()); -} - -#[test] -fn serde_slot1() { - test_file(SLOT1_PATH); -} - -#[test] -fn serde_slot2() { - test_file(SLOT2_PATH); -} - -#[ignore] // This test takes ~5 seconds to run -#[test] -fn serde_slot3() { - test_file(SLOT3_PATH); -} - -#[test] -fn serde_tagcontainer() { - test_file(TAGCONTAINER_PATH); -} - -#[ignore] // This test takes ~5 seconds to run -#[test] -fn serde_transform() { - test_file(TRANSFORM_PATH); -} - -#[test] -fn serde_vector2d() { - test_file(VECTOR2D_PATH); -} diff --git a/tests/serde_tests/serde_json_template.rs b/tests/serde_tests/serde_json_template.rs deleted file mode 100644 index 4a00d80..0000000 --- a/tests/serde_tests/serde_json_template.rs +++ /dev/null @@ -1,1983 +0,0 @@ -use crate::common::*; -use gvas::{ - GvasFile, - game_version::GameVersion, - properties::{ - Property, - array_property::ArrayProperty, - delegate_property::{Delegate, DelegateProperty}, - enum_property::EnumProperty, - field_path_property::{FieldPath, FieldPathProperty}, - int_property::{ - BoolProperty, ByteProperty, BytePropertyValue, DoubleProperty, FloatProperty, - Int8Property, Int16Property, Int64Property, IntProperty, UInt16Property, - UInt32Property, UInt64Property, - }, - map_property::MapProperty, - name_property::NameProperty, - object_property::ObjectProperty, - set_property::SetProperty, - str_property::StrProperty, - struct_property::StructPropertyValue, - struct_types::{ - DateTime, IntPoint, LinearColor, QuatD, QuatF, RotatorD, RotatorF, Timespan, VectorD, - VectorF, - }, - text_property::{ - DateTimeStyle, FText, FTextHistory, FormatArgumentValue, NumberFormattingOptions, - RoundingMode, TextProperty, TransformType, - }, - unknown_property::UnknownProperty, - }, - types::{Guid, map::HashableIndexMap}, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - fmt::Debug, - fs::File, - io::{Cursor, Read}, - path::Path, -}; - -fn serde_json(value: &T, json: &str) -where - T: Debug + for<'a> Deserialize<'a> + PartialEq + Serialize, -{ - assert_eq!( - serde_json::to_string_pretty(value) - .expect("serde_json::to_string") - .as_str(), - json - ); - assert_eq!( - &serde_json::from_str::(json).expect("serde_json::from_str"), - value - ); -} - -fn file>(path: P, json: &str) { - file_with_hints(path, &HashMap::new(), json) -} - -fn file_with_hints>(path: P, hints: &HashMap, json: &str) { - let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(path); - let mut file = File::open(path).expect("Failed to open test asset"); - - // Read the file in to a Vec - let mut data = Vec::new(); - file.read_to_end(&mut data) - .expect("Failed to read test asset"); - - // Convert the Vec to a GvasFile - let mut cursor = Cursor::new(data); - let file = GvasFile::read_with_hints(&mut cursor, GameVersion::Default, hints) - .expect("Failed to parse gvas file"); - - // Compare the GvasFile to its expected JSON representation - serde_json(&file, json); -} - -#[test] -fn file_profile_0() { - file_with_hints(PROFILE_0_PATH, &profile0::hints(), profile0::PROFILE_0_JSON); -} - -#[test] -fn file_regression_01() { - file(REGRESSION_01_PATH, regression::REGRESSION_01_JSON); -} - -#[test] -fn file_slot1() { - file(SLOT1_PATH, slot1::SLOT1_JSON); -} - -#[test] -fn file_saveslot_03() { - file_with_hints( - SAVESLOT_03_PATH, - &saveslot3::hints(), - saveslot3::SAVESLOT_03_JSON, - ) -} - -#[test] -fn file_tagcontainer() { - file(TAGCONTAINER_PATH, tagcontainer::TAGCONTAINER_JSON) -} - -#[test] -fn file_vector2d() { - file(VECTOR2D_PATH, vector2d::VECTOR2D_JSON) -} - -#[test] -fn array_int8() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("Int8Property"), - None, - vec![ - Property::Int8Property(Int8Property { value: 0 }), - Property::Int8Property(Int8Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "Int8Property", - "properties": [ - { - "type": "Int8Property", - "value": 0 - }, - { - "type": "Int8Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_int16() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("Int16Property"), - None, - vec![ - Property::Int16Property(Int16Property { value: 0 }), - Property::Int16Property(Int16Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "Int16Property", - "properties": [ - { - "type": "Int16Property", - "value": 0 - }, - { - "type": "Int16Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_int32() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("IntProperty"), - None, - vec![ - Property::IntProperty(IntProperty { value: 0 }), - Property::IntProperty(IntProperty { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "ints": [ - 0, - 1 - ] -}"#, - ) -} - -#[test] -fn array_int64() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("Int64Property"), - None, - vec![ - Property::Int64Property(Int64Property { value: 0 }), - Property::Int64Property(Int64Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "Int64Property", - "properties": [ - { - "type": "Int64Property", - "value": 0 - }, - { - "type": "Int64Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_uint8() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("ByteProperty"), - None, - vec![ - Property::ByteProperty(ByteProperty::new_byte(None, 0)), - Property::ByteProperty(ByteProperty::new_byte(None, 1)), - Property::ByteProperty(ByteProperty::new_byte(None, 0xab)), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "bytes": "0001ab" -}"#, - ) -} - -#[test] -fn array_uint16() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("UInt16Property"), - None, - vec![ - Property::UInt16Property(UInt16Property { value: 0 }), - Property::UInt16Property(UInt16Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "UInt16Property", - "properties": [ - { - "type": "UInt16Property", - "value": 0 - }, - { - "type": "UInt16Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_uint32() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("UInt32Property"), - None, - vec![ - Property::UInt32Property(UInt32Property { value: 0 }), - Property::UInt32Property(UInt32Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "UInt32Property", - "properties": [ - { - "type": "UInt32Property", - "value": 0 - }, - { - "type": "UInt32Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_uint64() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("UInt64Property"), - None, - vec![ - Property::UInt64Property(UInt64Property { value: 0 }), - Property::UInt64Property(UInt64Property { value: 1 }), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "UInt64Property", - "properties": [ - { - "type": "UInt64Property", - "value": 0 - }, - { - "type": "UInt64Property", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn array_bool() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("BoolProperty"), - None, - vec![ - Property::BoolProperty(BoolProperty::new(false)), - Property::BoolProperty(BoolProperty::new(true)), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "bools": [ - false, - true - ] -}"#, - ) -} - -#[test] -fn array_double() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("DoubleProperty"), - None, - vec![ - Property::DoubleProperty(DoubleProperty::new(1f64)), - Property::DoubleProperty(DoubleProperty::new(2f64)), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "DoubleProperty", - "properties": [ - { - "type": "DoubleProperty", - "value": 1.0 - }, - { - "type": "DoubleProperty", - "value": 2.0 - } - ] -}"#, - ) -} - -#[test] -fn array_float() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("FloatProperty"), - None, - vec![ - Property::FloatProperty(FloatProperty::new(1f32)), - Property::FloatProperty(FloatProperty::new(2f32)), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "floats": [ - 1.0, - 2.0 - ] -}"#, - ) -} - -#[test] -fn array_enum() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("EnumProperty"), - None, - vec![ - Property::EnumProperty(EnumProperty::new(None, "a".to_string())), - Property::EnumProperty(EnumProperty::new(None, "b".to_string())), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "enums": [ - "a", - "b" - ] -}"#, - ) -} - -#[test] -fn array_enum_ns() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("EnumProperty"), - None, - vec![ - Property::EnumProperty(EnumProperty::new(None, "a".to_string())), - Property::EnumProperty(EnumProperty::new( - Some("ns".to_string()), - "b".to_string(), - )), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "EnumProperty", - "properties": [ - { - "type": "EnumProperty", - "value": "a" - }, - { - "type": "EnumProperty", - "enum_type": "ns", - "value": "b" - } - ] -}"#, - ) -} - -#[test] -fn array_name() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("NameProperty"), - None, - vec![ - Property::NameProperty(NameProperty::from(None)), - Property::NameProperty(NameProperty::from("b")), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "names": [ - null, - "b" - ] -}"#, - ) -} - -#[test] -fn array_object() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("ObjectProperty"), - None, - vec![ - Property::ObjectProperty(ObjectProperty::from("a")), - Property::ObjectProperty(ObjectProperty::from("b")), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "ObjectProperty", - "properties": [ - { - "type": "ObjectProperty", - "value": "a" - }, - { - "type": "ObjectProperty", - "value": "b" - } - ] -}"#, - ) -} - -#[test] -fn array_str() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("StrProperty"), - None, - vec![ - Property::StrProperty(StrProperty::new(None)), - Property::StrProperty(StrProperty::from("b")), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "strings": [ - null, - "b" - ] -}"#, - ) -} - -#[test] -fn array_map() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("MapProperty"), - None, - vec![ - Property::MapProperty(MapProperty::new( - "kta".to_string(), - "vta".to_string(), - 0, - HashableIndexMap::from([]), - )), - Property::MapProperty(MapProperty::new( - "ktb".to_string(), - "vtb".to_string(), - 1, - HashableIndexMap::from([]), - )), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "property_type": "MapProperty", - "properties": [ - { - "type": "MapProperty", - "key_type": "kta", - "value_type": "vta", - "allocation_flags": 0, - "value": [] - }, - { - "type": "MapProperty", - "key_type": "ktb", - "value_type": "vtb", - "allocation_flags": 1, - "value": [] - } - ] -}"#, - ) -} - -#[test] -fn array_struct() { - serde_json( - &Property::ArrayProperty( - ArrayProperty::new( - String::from("StructProperty"), - Some((String::from("fn"), String::from("tn"), Guid([0x11u8; 16]))), - vec![ - Property::from(StructPropertyValue::from(DateTime { ticks: 0 })), - Property::from(StructPropertyValue::from(DateTime { ticks: 1 })), - ], - ) - .expect("ArrayProperty::new"), - ), - r#"{ - "type": "ArrayProperty", - "field_name": "fn", - "type_name": "tn", - "guid": "11111111-1111-1111-1111-111111111111", - "structs": [ - { - "DateTime": { - "ticks": 0 - } - }, - { - "DateTime": { - "ticks": 1 - } - } - ] -}"#, - ) -} - -#[test] -fn delegate() { - serde_json( - &Property::DelegateProperty(DelegateProperty::new(Delegate::new( - String::from("o"), - String::from("fn"), - ))), - r#"{ - "type": "DelegateProperty", - "value": { - "object": "o", - "function_name": "fn" - } -}"#, - ) -} - -#[test] -fn enum_property() { - serde_json( - &Property::EnumProperty(EnumProperty::new( - Some(String::from("a")), - String::from("b"), - )), - r#"{ - "type": "EnumProperty", - "enum_type": "a", - "value": "b" -}"#, - ); - serde_json( - &Property::EnumProperty(EnumProperty::new(None, String::from("a"))), - r#"{ - "type": "EnumProperty", - "value": "a" -}"#, - ); -} - -#[test] -fn field_path() { - serde_json( - &Property::FieldPathProperty(FieldPathProperty::new(FieldPath::new( - vec![String::from("a"), String::from("b")], - String::from("owner"), - ))), - r#"{ - "type": "FieldPathProperty", - "value": { - "path": [ - "a", - "b" - ], - "resolved_owner": "owner" - } -}"#, - ) -} - -#[test] -fn map_enum_bool() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("EnumProperty"), - String::from("BoolProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(EnumProperty::new(None, String::from("a"))), - Property::from(BoolProperty::new(false)), - ), - ( - Property::from(EnumProperty::new(None, String::from("b"))), - Property::from(BoolProperty::new(true)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "enum_bools": { - "a": false, - "b": true - } -}"#, - ) -} - -#[test] -fn map_enum_int() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("EnumProperty"), - String::from("IntProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(EnumProperty::new(None, String::from("a"))), - Property::from(IntProperty::new(0)), - ), - ( - Property::from(EnumProperty::new(None, String::from("b"))), - Property::from(IntProperty::new(1)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "enum_ints": { - "a": 0, - "b": 1 - } -}"#, - ) -} - -#[test] -fn map_enum_unknown() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("EnumProperty"), - String::from("UnknownProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(EnumProperty::new(None, String::from("a"))), - Property::from(UnknownProperty::new(String::from("n"), vec![])), - ), - ( - Property::from(EnumProperty::new(None, String::from("b"))), - Property::from(UnknownProperty::new(String::from("m"), vec![1])), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "value_type": "UnknownProperty", - "enum_props": { - "a": { - "type": "UnknownProperty", - "property_name": "n", - "raw": [] - }, - "b": { - "type": "UnknownProperty", - "property_name": "m", - "raw": [ - 1 - ] - } - } -}"#, - ) -} - -#[test] -fn map_int_bool() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("IntProperty"), - String::from("BoolProperty"), - 0, - HashableIndexMap::from([ - ( - Property::IntProperty(IntProperty::new(0)), - Property::BoolProperty(BoolProperty::new(false)), - ), - ( - Property::IntProperty(IntProperty::new(1)), - Property::BoolProperty(BoolProperty::new(true)), - ), - ( - Property::IntProperty(IntProperty::new(2)), - Property::BoolProperty(BoolProperty::new(false)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "key_type": "IntProperty", - "value_type": "BoolProperty", - "allocation_flags": 0, - "value": [ - [ - { - "type": "IntProperty", - "value": 0 - }, - { - "type": "BoolProperty", - "value": false - } - ], - [ - { - "type": "IntProperty", - "value": 1 - }, - { - "type": "BoolProperty", - "value": true - } - ], - [ - { - "type": "IntProperty", - "value": 2 - }, - { - "type": "BoolProperty", - "value": false - } - ] - ] -}"#, - ) -} - -#[test] -fn map_name_bool() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("NameProperty"), - String::from("BoolProperty"), - 0, - HashableIndexMap::from([ - ( - Property::NameProperty(NameProperty::from("a")), - Property::BoolProperty(BoolProperty::new(false)), - ), - ( - Property::NameProperty(NameProperty::from("b")), - Property::BoolProperty(BoolProperty::new(true)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "name_bools": { - "a": false, - "b": true - } -}"#, - ); -} - -#[test] -fn map_name_int() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("NameProperty"), - String::from("IntProperty"), - 0, - HashableIndexMap::from([ - ( - Property::NameProperty(NameProperty::from("a")), - Property::IntProperty(IntProperty::new(0)), - ), - ( - Property::NameProperty(NameProperty::from("b")), - Property::IntProperty(IntProperty::new(1)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "name_ints": { - "a": 0, - "b": 1 - } -}"#, - ); -} - -#[test] -fn map_name_property() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("NameProperty"), - String::from("UnknownProperty"), - 0, - HashableIndexMap::from([ - ( - Property::NameProperty(NameProperty::from("a")), - Property::UnknownProperty(UnknownProperty::new(String::from("b"), vec![])), - ), - ( - Property::NameProperty(NameProperty::from("c")), - Property::UnknownProperty(UnknownProperty::new(String::from("d"), vec![1])), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "value_type": "UnknownProperty", - "name_props": { - "a": { - "type": "UnknownProperty", - "property_name": "b", - "raw": [] - }, - "c": { - "type": "UnknownProperty", - "property_name": "d", - "raw": [ - 1 - ] - } - } -}"#, - ); -} - -#[test] -fn map_str_bool() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("StrProperty"), - String::from("BoolProperty"), - 0, - HashableIndexMap::from([ - ( - Property::StrProperty(StrProperty::from("a")), - Property::BoolProperty(BoolProperty::new(false)), - ), - ( - Property::StrProperty(StrProperty::from("b")), - Property::BoolProperty(BoolProperty::new(true)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "str_bools": { - "a": false, - "b": true - } -}"#, - ); -} - -#[test] -fn map_str_int() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("StrProperty"), - String::from("IntProperty"), - 0, - HashableIndexMap::from([ - ( - Property::StrProperty(StrProperty::from("zero")), - Property::IntProperty(IntProperty::new(0)), - ), - ( - Property::StrProperty(StrProperty::from("one")), - Property::IntProperty(IntProperty::new(1)), - ), - ( - Property::StrProperty(StrProperty::from("two")), - Property::IntProperty(IntProperty::new(2)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "str_ints": { - "zero": 0, - "one": 1, - "two": 2 - } -}"#, - ) -} - -#[test] -fn map_str_property() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("StrProperty"), - String::from("UnknownProperty"), - 0, - HashableIndexMap::from([ - ( - Property::StrProperty(StrProperty::from("a")), - Property::UnknownProperty(UnknownProperty::new(String::from("b"), vec![])), - ), - ( - Property::StrProperty(StrProperty::from("c")), - Property::UnknownProperty(UnknownProperty::new(String::from("d"), vec![1])), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "value_type": "UnknownProperty", - "str_props": { - "a": { - "type": "UnknownProperty", - "property_name": "b", - "raw": [] - }, - "c": { - "type": "UnknownProperty", - "property_name": "d", - "raw": [ - 1 - ] - } - } -}"#, - ); -} - -#[test] -fn map_str_str() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("StrProperty"), - String::from("StrProperty"), - 0, - HashableIndexMap::from([ - ( - Property::StrProperty(StrProperty::from("a")), - Property::StrProperty(StrProperty::from("b")), - ), - ( - Property::StrProperty(StrProperty::from("c")), - Property::StrProperty(StrProperty::from("d")), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "str_strs": { - "a": "b", - "c": "d" - } -}"#, - ); -} - -#[test] -fn map_struct_float() { - serde_json( - &Property::MapProperty(MapProperty::new( - String::from("StructProperty"), - String::from("FloatProperty"), - 0, - HashableIndexMap::from([ - ( - Property::from(StructPropertyValue::from(VectorF::new(0f32, 1f32, 2f32))), - Property::FloatProperty(FloatProperty::new(0f32)), - ), - ( - Property::from(StructPropertyValue::from(Timespan::new(0))), - Property::FloatProperty(FloatProperty::new(1f32)), - ), - ( - Property::from(StructPropertyValue::from(DateTime::new(0))), - Property::FloatProperty(FloatProperty::new(2f32)), - ), - ]), - )), - r#"{ - "type": "MapProperty", - "key_type": "StructProperty", - "value_type": "FloatProperty", - "allocation_flags": 0, - "value": [ - [ - { - "type": "StructPropertyValue", - "VectorF": { - "x": 0.0, - "y": 1.0, - "z": 2.0 - } - }, - { - "type": "FloatProperty", - "value": 0.0 - } - ], - [ - { - "type": "StructPropertyValue", - "Timespan": { - "ticks": 0 - } - }, - { - "type": "FloatProperty", - "value": 1.0 - } - ], - [ - { - "type": "StructPropertyValue", - "DateTime": { - "ticks": 0 - } - }, - { - "type": "FloatProperty", - "value": 2.0 - } - ] - ] -}"#, - ) -} - -#[test] -fn name_array_index() { - serde_json( - &Property::NameProperty(NameProperty { - array_index: 1, - value: None, - }), - r#"{ - "type": "NameProperty", - "array_index": 1 -}"#, - ) -} - -#[test] -fn name_none() { - serde_json( - &Property::NameProperty(NameProperty { - array_index: 0, - value: None, - }), - r#"{ - "type": "NameProperty" -}"#, - ) -} - -#[test] -fn name_some() { - serde_json( - &Property::NameProperty(NameProperty::from("a")), - r#"{ - "type": "NameProperty", - "value": "a" -}"#, - ) -} - -#[test] -fn float() { - serde_json( - &Property::FloatProperty(FloatProperty::new(0f32)), - r#"{ - "type": "FloatProperty", - "value": 0.0 -}"#, - ) -} - -#[test] -fn double() { - serde_json( - &Property::DoubleProperty(DoubleProperty::new(0f64)), - r#"{ - "type": "DoubleProperty", - "value": 0.0 -}"#, - ) -} - -#[test] -fn byte_none_byte() { - serde_json( - &Property::ByteProperty(ByteProperty::new(None, BytePropertyValue::Byte(0))), - r#"{ - "type": "ByteProperty", - "Byte": 0 -}"#, - ) -} - -#[test] -fn byte_some_ns() { - serde_json( - &Property::ByteProperty(ByteProperty::new_namespaced( - Some(String::from("test name")), - String::from("ns"), - )), - r#"{ - "type": "ByteProperty", - "name": "test name", - "Namespaced": "ns" -}"#, - ) -} - -#[test] -fn int8() { - serde_json( - &Property::from(Int8Property::new(0i8)), - r#"{ - "type": "Int8Property", - "value": 0 -}"#, - ) -} - -#[test] -fn int16() { - serde_json( - &Property::Int16Property(Int16Property::new(0)), - r#"{ - "type": "Int16Property", - "value": 0 -}"#, - ) -} - -#[test] -fn uint16() { - serde_json( - &Property::UInt16Property(UInt16Property::new(0)), - r#"{ - "type": "UInt16Property", - "value": 0 -}"#, - ) -} - -#[test] -fn int() { - serde_json( - &Property::IntProperty(IntProperty::new(0)), - r#"{ - "type": "IntProperty", - "value": 0 -}"#, - ) -} - -#[test] -fn uint32() { - serde_json( - &Property::UInt32Property(UInt32Property::new(0)), - r#"{ - "type": "UInt32Property", - "value": 0 -}"#, - ) -} - -#[test] -fn int64() { - serde_json( - &Property::Int64Property(Int64Property::new(0)), - r#"{ - "type": "Int64Property", - "value": 0 -}"#, - ) -} - -#[test] -fn uint64() { - serde_json( - &Property::UInt64Property(UInt64Property::new(0)), - r#"{ - "type": "UInt64Property", - "value": 0 -}"#, - ) -} - -#[test] -fn object() { - serde_json( - &Property::ObjectProperty(ObjectProperty::from("a")), - r#"{ - "type": "ObjectProperty", - "value": "a" -}"#, - ) -} - -#[test] -fn set_int() { - serde_json( - &Property::SetProperty(SetProperty::new( - String::from("IntProperty"), - 0, - vec![ - Property::IntProperty(IntProperty { value: 0 }), - Property::IntProperty(IntProperty { value: 1 }), - ], - )), - r#"{ - "type": "SetProperty", - "property_type": "IntProperty", - "allocation_flags": 0, - "properties": [ - { - "type": "IntProperty", - "value": 0 - }, - { - "type": "IntProperty", - "value": 1 - } - ] -}"#, - ) -} - -#[test] -fn str_none() { - serde_json( - &Property::StrProperty(StrProperty { value: None }), - r#"{ - "type": "StrProperty" -}"#, - ) -} - -#[test] -fn str_some() { - serde_json( - &Property::StrProperty(StrProperty::from("a")), - r#"{ - "type": "StrProperty", - "value": "a" -}"#, - ) -} - -#[test] -fn struct_vectorf() { - serde_json( - &Property::from(StructPropertyValue::from(VectorF::new(0f32, 1f32, 2f32))), - r#"{ - "type": "StructPropertyValue", - "VectorF": { - "x": 0.0, - "y": 1.0, - "z": 2.0 - } -}"#, - ) -} - -#[test] -fn struct_vectord() { - serde_json( - &Property::from(StructPropertyValue::from(VectorD::new(0f64, 1f64, 2f64))), - r#"{ - "type": "StructPropertyValue", - "VectorD": { - "x": 0.0, - "y": 1.0, - "z": 2.0 - } -}"#, - ) -} - -#[test] -fn struct_rotatorf() { - serde_json( - &Property::from(StructPropertyValue::from(RotatorF::new(0f32, 1f32, 2f32))), - r#"{ - "type": "StructPropertyValue", - "RotatorF": { - "pitch": 0.0, - "yaw": 1.0, - "roll": 2.0 - } -}"#, - ) -} - -#[test] -fn struct_rotatord() { - serde_json( - &Property::from(StructPropertyValue::from(RotatorD::new(0f64, 1f64, 2f64))), - r#"{ - "type": "StructPropertyValue", - "RotatorD": { - "pitch": 0.0, - "yaw": 1.0, - "roll": 2.0 - } -}"#, - ) -} - -#[test] -fn struct_quatf() { - serde_json( - &Property::from(StructPropertyValue::from(QuatF::new( - 0f32, 1f32, 2f32, 3f32, - ))), - r#"{ - "type": "StructPropertyValue", - "QuatF": { - "x": 0.0, - "y": 1.0, - "z": 2.0, - "w": 3.0 - } -}"#, - ) -} - -#[test] -fn struct_quatd() { - serde_json( - &Property::from(StructPropertyValue::from(QuatD::new( - 0f64, 1f64, 2f64, 3f64, - ))), - r#"{ - "type": "StructPropertyValue", - "QuatD": { - "x": 0.0, - "y": 1.0, - "z": 2.0, - "w": 3.0 - } -}"#, - ) -} - -#[test] -fn struct_datetime() { - serde_json( - &Property::from(StructPropertyValue::from(QuatD::new( - 0f64, 1f64, 2f64, 3f64, - ))), - r#"{ - "type": "StructPropertyValue", - "QuatD": { - "x": 0.0, - "y": 1.0, - "z": 2.0, - "w": 3.0 - } -}"#, - ) -} - -#[test] -fn struct_linearcolor() { - serde_json( - &Property::from(StructPropertyValue::from(LinearColor::new( - 0.0, 1.0, 2.0, 3.0, - ))), - r#"{ - "type": "StructPropertyValue", - "LinearColor": { - "r": 0.0, - "g": 1.0, - "b": 2.0, - "a": 3.0 - } -}"#, - ) -} - -#[test] -fn struct_intpoint() { - serde_json( - &Property::from(StructPropertyValue::from(IntPoint::new(0, 1))), - r#"{ - "type": "StructPropertyValue", - "IntPoint": { - "x": 0, - "y": 1 - } -}"#, - ) -} - -#[test] -fn struct_custom() { - serde_json( - &Property::from(StructPropertyValue::CustomStruct(HashableIndexMap::from([ - ( - String::from("key"), - vec![Property::from(StrProperty::from("value"))], - ), - ]))), - r#"{ - "type": "StructPropertyValue", - "CustomStruct": { - "key": [ - { - "type": "StrProperty", - "value": "value" - } - ] - } -}"#, - ) -} - -#[test] -fn struct_array_index() { - serde_json( - &Property::from(StructPropertyValue::CustomStruct(HashableIndexMap::from([ - ( - String::from("TrackedQuestsNames"), - vec![ - Property::NameProperty(NameProperty { - array_index: 0, - value: Some(String::from("QU91_InvestigateTower_B2")), - }), - Property::NameProperty(NameProperty { - array_index: 1, - value: Some(String::from("QU91_InvestigateTower_B2")), - }), - ], - ), - ]))), - r#"{ - "type": "StructPropertyValue", - "CustomStruct": { - "TrackedQuestsNames": [ - { - "type": "NameProperty", - "value": "QU91_InvestigateTower_B2" - }, - { - "type": "NameProperty", - "array_index": 1, - "value": "QU91_InvestigateTower_B2" - } - ] - } -}"#, - ) -} - -#[test] -fn struct_gameplaytag() { - serde_json( - &Property::from(StructPropertyValue::GameplayTagContainer(vec![ - String::from("Gameplaytag.One"), - String::from("Gameplaytag.Two"), - ])), - r#"{ - "type": "StructPropertyValue", - "GameplayTagContainer": [ - "Gameplaytag.One", - "Gameplaytag.Two" - ] -}"#, - ) -} - -#[test] -fn text_empty() { - serde_json( - &Property::TextProperty(TextProperty::new(FText::new_none(0, None))), - r#"{ - "type": "TextProperty", - "history": "Empty" -}"#, - ); -} - -#[test] -fn text_none_some_none() { - serde_json( - &Property::TextProperty(TextProperty::new(FText::new_none(1, Some(None)))), - r#"{ - "type": "TextProperty", - "flags": 1, - "history": "None" -}"#, - ); -} - -#[test] -fn text_none_some_some() { - serde_json( - &Property::TextProperty(TextProperty::new(FText::new_none( - 2, - Some(Some(String::from("a"))), - ))), - r#"{ - "type": "TextProperty", - "flags": 2, - "history": "None", - "culture_invariant_string": "a" -}"#, - ); -} - -#[test] -fn text_base_none() { - serde_json( - &Property::TextProperty(TextProperty::new(FText::new_base(0, None, None, None))), - r#"{ - "type": "TextProperty", - "history": "Base" -}"#, - ); -} - -#[test] -fn text_base_filled() { - serde_json( - &Property::TextProperty(TextProperty::new(FText::new_base( - 1, - Some(String::from("ns")), - Some(String::from("k")), - Some(String::from("ss")), - ))), - r#"{ - "type": "TextProperty", - "flags": 1, - "history": "Base", - "namespace": "ns", - "key": "k", - "source_string": "ss" -}"#, - ); -} - -#[test] -fn text_namedformat() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::NamedFormat { - source_format: Box::new(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - }), - arguments: HashableIndexMap::from([( - String::from("key"), - FormatArgumentValue::Int(2), - )]), - }, - })), - r#"{ - "type": "TextProperty", - "history": "NamedFormat", - "source_format": { - "flags": 1, - "history": "None" - }, - "arguments": { - "key": { - "Int": 2 - } - } -}"#, - ); -} - -#[test] -fn text_orderedformat() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::OrderedFormat { - source_format: Box::new(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - }), - arguments: vec![FormatArgumentValue::UInt(2)], - }, - })), - r#"{ - "type": "TextProperty", - "history": "OrderedFormat", - "source_format": { - "flags": 1, - "history": "None" - }, - "arguments": [ - { - "UInt": 2 - } - ] -}"#, - ); -} - -#[test] -fn text_argumentformat() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::ArgumentFormat { - source_format: Box::new(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - }), - arguments: HashableIndexMap::from([( - String::from("key"), - FormatArgumentValue::UInt(2), - )]), - }, - })), - r#"{ - "type": "TextProperty", - "history": "ArgumentFormat", - "source_format": { - "flags": 1, - "history": "None" - }, - "arguments": { - "key": { - "UInt": 2 - } - } -}"#, - ); -} - -#[test] -fn text_asnumber() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::AsNumber { - source_value: Box::new(FormatArgumentValue::Text(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - })), - format_options: Some(NumberFormattingOptions { - always_include_sign: true, - use_grouping: true, - rounding_mode: RoundingMode::ToZero, - minimum_integral_digits: 2, - maximum_integral_digits: 3, - minimum_fractional_digits: 4, - maximum_fractional_digits: 5, - }), - target_culture: Some(String::from("culture")), - }, - })), - r#"{ - "type": "TextProperty", - "history": "AsNumber", - "source_value": { - "Text": { - "flags": 1, - "history": "None" - } - }, - "format_options": { - "always_include_sign": true, - "use_grouping": true, - "rounding": "ToZero", - "minimum_integral_digits": 2, - "maximum_integral_digits": 3, - "minimum_fractional_digits": 4, - "maximum_fractional_digits": 5 - }, - "target_culture": "culture" -}"#, - ); -} - -#[test] -fn text_ascurrency() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::AsNumber { - source_value: Box::new(FormatArgumentValue::Text(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - })), - format_options: Some(NumberFormattingOptions { - always_include_sign: true, - use_grouping: true, - rounding_mode: RoundingMode::ToZero, - minimum_integral_digits: 2, - maximum_integral_digits: 3, - minimum_fractional_digits: 4, - maximum_fractional_digits: 5, - }), - target_culture: Some(String::from("culture")), - }, - })), - r#"{ - "type": "TextProperty", - "history": "AsNumber", - "source_value": { - "Text": { - "flags": 1, - "history": "None" - } - }, - "format_options": { - "always_include_sign": true, - "use_grouping": true, - "rounding": "ToZero", - "minimum_integral_digits": 2, - "maximum_integral_digits": 3, - "minimum_fractional_digits": 4, - "maximum_fractional_digits": 5 - }, - "target_culture": "culture" -}"#, - ); -} - -#[test] -fn text_asdate() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::AsDate { - date_time: DateTime { ticks: 1 }, - date_style: DateTimeStyle::Default, - target_culture: String::from("culture"), - }, - })), - r#"{ - "type": "TextProperty", - "history": "AsDate", - "date_time": { - "ticks": 1 - }, - "date_style": "Default", - "target_culture": "culture" -}"#, - ); -} - -#[test] -fn text_astime() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::AsTime { - source_date_time: DateTime { ticks: 1 }, - time_style: DateTimeStyle::Default, - time_zone: String::from("zone"), - target_culture: String::from("culture"), - }, - })), - r#"{ - "type": "TextProperty", - "history": "AsTime", - "source_date_time": { - "ticks": 1 - }, - "time_style": "Default", - "time_zone": "zone", - "target_culture": "culture" -}"#, - ); -} - -#[test] -fn text_asdatetime() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::AsDateTime { - source_date_time: DateTime { ticks: 1 }, - date_style: DateTimeStyle::Default, - time_style: DateTimeStyle::Default, - time_zone: String::from("zone"), - target_culture: String::from("culture"), - }, - })), - r#"{ - "type": "TextProperty", - "history": "AsDateTime", - "source_date_time": { - "ticks": 1 - }, - "date_style": "Default", - "time_style": "Default", - "time_zone": "zone", - "target_culture": "culture" -}"#, - ); -} - -#[test] -fn text_transform() { - serde_json( - &Property::TextProperty(TextProperty::new(FText { - flags: 0, - history: FTextHistory::Transform { - source_text: Box::new(FText { - flags: 1, - history: FTextHistory::None { - culture_invariant_string: None, - }, - }), - transform_type: TransformType::ToLower, - }, - })), - r#"{ - "type": "TextProperty", - "history": "Transform", - "source_text": { - "flags": 1, - "history": "None" - }, - "transform": "ToLower" -}"#, - ); -} - -#[test] -fn unknown() { - serde_json( - &Property::UnknownProperty(UnknownProperty::new(String::from("name"), vec![0, 1, 2])), - r#"{ - "type": "UnknownProperty", - "property_name": "name", - "raw": [ - 0, - 1, - 2 - ] -}"#, - ) -}