trumank / uesave-rs

Rust library to read and write Unreal Engine save files
MIT License
274 stars 54 forks source link

Found non-standard magic, io error: failed to fill whole buffer, not Palworld, ue5.4 #42

Closed Egaliterrier closed 4 months ago

Egaliterrier commented 5 months ago

Game is Serum, running on UE 5.4, getting this message:

Found non-standard magic: [c1, 83, 2a, 9e] (��*�) expected: GVAS, continuing to parse... Error: at offset 12309: io error: failed to fill whole buffer

Player.zip

trumank commented 4 months ago

I believe this save has been compressed with FArchive::SerializeCompressedNew which does some kind of block compression (blocks are marked with C1 83 2A 9E 22 22 22 22.)

Egaliterrier commented 4 months ago

Alright, not sure what I can do then. I assume it’s beyond the scope for this project to account for that?

trumank commented 4 months ago

I'm not sure yet. It seems to be a newly added function for general purpose compression, so if it ends up being used by a lot of games, it might be worth supporting.

trumank commented 4 months ago

I hacked together a script to decompress the save (uses Oodle compressor too🤮) and the decompressed file actually is actually not quite a normal UE save. It lacks the GVAS magic and header, but otherwise does look like UE tagged serialization. It'd be doable to parse but would take a bit of work and I think is safely out of the scope of this tool.

uncompressed.zip

[dependencies]
anyhow = "1.0.86"
byteorder = "1.5.0"
oodle_loader = { git = "https://github.com/trumank/repak" }
use std::io::Read;

use anyhow::Result;
use byteorder::{ReadBytesExt, LE};

fn main() -> Result<()> {
    let mut data = std::io::Cursor::new(std::fs::read("Player.sav")?);

    let mut output = vec![];

    while data.position() < data.get_ref().len() as u64 {
        let _magic = data.read_u64::<LE>();
        let _compressed_size = data.read_u64::<LE>();
        let _compressor_num = data.read_u8();

        let a_compressed_size = data.read_u64::<LE>()?;
        let a_uncompressed_size = data.read_u64::<LE>()?;
        let _b_compressed_size = data.read_u64::<LE>()?;
        let _b_uncompressed_size = data.read_u64::<LE>()?;

        let mut buf = vec![0; a_compressed_size as usize];
        data.read_exact(&mut buf)?;

        let mut uncompressed = vec![0; a_uncompressed_size as usize];
        let size = (oodle_loader::decompress().unwrap())(&buf, &mut uncompressed) as usize;
        output.extend_from_slice(&uncompressed[..size]);
    }
    std::fs::write("output.bin", &output)?;

    println!("decompressed to {} bytes", output.len());
    Ok(())
}
Egaliterrier commented 4 months ago

Thanks for checking it out. Odd! The game uses a common UE save plugin, so it's weird to me that this file lacks that data. Each saved game generates three files, and the file I posted here was just the one uesave had trouble with. It might be that the relevant GVAS magic and header data is split across one or both of the other files in some way. Granted, I'm guessing with limited experience in this area. Looking through my dump, there doesn't seem to be anything outside of the ordinary going on as far as saving, but then again, I'm not that knowledgeable on it yet, and I don't exactly have access to the exact code. This MultiLevelSaving setting stuck out to me, but it might be unrelated:

[EasyMultiSave.EMSPluginSettings]
MultiLevelSaving=ML_Stream
bAdvancedSpawnCheck=True
AsyncWaitDelay=10.000000
bMultiThreadSaving=False
LoadMethod=LM_Deferred
CompressionMethod=Oodle
SlotInfoSaveGameClass=/Script/EasyMultiSave.SEInfoSaveGame

Attached the full save game directory if you're bored and feel like tinkering someday. Anyway, thanks for the effort in any case:) SerumSaveGames-2024-06-11-12-08.zip