From 05fea8e01ef6b380f694061cb6241039613629ce Mon Sep 17 00:00:00 2001 From: "Mereak Firmaxe (John Owens)" <76136571+jbowensii@users.noreply.github.com> Date: Fri, 17 Apr 2026 16:38:01 -0400 Subject: [PATCH] Fix UE4.27 cooked asset parsing (ObjectVersionUE5 + compression blocks) Two fixes for parsing cooked assets from UE4.27 games that use IoStore: 1. ObjectVersionUE5 enum panic: UE4.27 files with legacy_file_version <= -8 write a small integer (e.g. 1) in the UE5 version field. The enum has no variant between UNKNOWN (0) and INITIAL_VERSION (1000), causing TryFromPrimitive to fail. Fix: fall back to UNKNOWN for unrecognized values. 2. Compression block count rejection: UE4.27 cooked assets use Zlib compression with non-zero block counts. The parser hard-rejected these. Fix: skip the FCompressedChunk entries (16 bytes each) instead of erroring. These fixes enable tools like Stove to open cooked .umap files from UE4.27 games such as Return to Moria and DeltaForce. Fixes bananaturtlesandwich/stove#39 Co-Authored-By: Claude Opus 4.6 --- unreal_asset/src/asset.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/unreal_asset/src/asset.rs b/unreal_asset/src/asset.rs index 8505604..2a5d450 100644 --- a/unreal_asset/src/asset.rs +++ b/unreal_asset/src/asset.rs @@ -565,7 +565,10 @@ impl<'a, C: Read + Seek> Asset { } if self.legacy_file_version <= -8 { - let object_version_ue5: ObjectVersionUE5 = self.read_i32::()?.try_into()?; + let ue5_ver_raw = self.read_i32::()?; + let object_version_ue5: ObjectVersionUE5 = ue5_ver_raw + .try_into() + .unwrap_or(ObjectVersionUE5::UNKNOWN); if object_version_ue5 > ObjectVersionUE5::UNKNOWN { self.asset_data.object_version_ue5 = object_version_ue5; } @@ -670,9 +673,12 @@ impl<'a, C: Read + Seek> Asset { self.compression_flags = self.read_u32::()?; let compression_block_count = self.read_u32::()?; if compression_block_count > 0 { - return Err(Error::invalid_file( - "Compression block count is not zero".to_string(), - )); + // UE4.27 cooked assets may have compressed chunks - skip them + // Each FCompressedChunk is 16 bytes (2x i64: UncompressedOffset + UncompressedSize) + for _i in 0..compression_block_count { + self.read_i64::()?; + self.read_i64::()?; + } } self.package_source = self.read_u32::()?;