trumank / uesave-rs

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

Palworld save file incompatability #22

Closed TristanWiley closed 7 months ago

TristanWiley commented 7 months ago

Hi! I saw some other GH issues here with various game incompatibilities/requests and figured I'd make one as well.

Looks like Palworld must have some compression that's causing it to not work with the tool. I get the following error:

Found non-standard magic: [62, 16, 00, 00] (b) expected: GVAS, continuing to parse...
Error: at offset 2674: io error: failed to fill whole buffer

Here's a couple sample player save files: PalworldSaveFiles.zip

Any help would be appreciated!

cheahjs commented 7 months ago

Palworld wraps their save file with compression as you said. The format is:

4 bytes - uncompressed length
4 bytes - compressed length
3 bytes - magic bytes PlZ
1 byte - save type. valid options: 0x30, 0x31, 0x32

For save type 0x31, it is compressed with zlib once. For save type 0x32, it is compressed with zlib twice, and the lengths refer to the data after first decompression.

There is however issues with loading Level.sav, all other save files parse correctly.

$ uesave to-json --input Level.sav.gvas
offset 1773: StructType for ".worldSaveData.CharacterSaveParameterMap.Key" unspecified, assuming Guid
offset 1773: StructType for ".worldSaveData.CharacterSaveParameterMap.Value" unspecified, assuming Struct(None)
Error: at offset 3461993: io error: failed to fill whole buffer

Here's a sample of uncompressed save files: PalWorldUncompressedSave.zip

This looks like it's just bad inferred types.

cheahjs commented 7 months ago

uesave works fine (for converting to JSON, still working on the inverse process to load into the game) with the following types (the issue mainly stems from Keys being inferred as Guids, but IDs in PalWorld tend to be structs)

uesave to-json --input Level.sav.gvas \
    --type .worldSaveData.CharacterSaveParameterMap.Key=Struct \
    --type .worldSaveData.FoliageGridSaveDataMap.Key=Struct \
    --type .worldSaveData.FoliageGridSaveDataMap.ModelMap.InstanceDataMap.Key=Struct \
    --type .worldSaveData.MapObjectSpawnerInStageSaveData.Key=Struct \
    --type .worldSaveData.ItemContainerSaveData.Key=Struct \
    --type .worldSaveData.CharacterContainerSaveData.Key=Struct
TristanWiley commented 7 months ago

That makes sense! Might be a silly question, how did you go about decompressing the save files?

cheahjs commented 7 months ago

That makes sense! Might be a silly question, how did you go about decompressing the save files?

https://gist.github.com/cheahjs/300239464dd84fe6902893b6b9250fd0

TristanWiley commented 7 months ago

That worked! Thanks so much! Let me know if you happen to figure out the inverse process oops didn't see the second file. I really appreciate the help

TristanWiley commented 7 months ago

One more unrelated question, any idea what the file name structure is? I assume it relates to a Steam ID of some sort but I've spent hours trying to decipher it and how it's encoded and I can't figure it out. I know the 0000000000... file is for a user that creates the world. But the other ones are definitely related to some unique identifier for the user.

mcd1992 commented 7 months ago

One more unrelated question, any idea what the file name structure is? I assume it relates to a Steam ID of some sort but I've spent hours trying to decipher it and how it's encoded and I can't figure it out. I know the 0000000000... file is for a user that creates the world. But the other ones are definitely related to some unique identifier for the user.

Not sure either. I've been trying to figure it out so I can convert my dedicated server player.savs to another machine since those IDs are per-machine as well. So I can't just copy paste my Player/*.savs

The filename does match the first 32 bits of IndividualId and PlayerUId in the GVAS/json data.

EDIT: The reason my UIDs were different between hosts is because the dedi I had symlinked the steamclient.so to the palworld's linux64/steamclient.so and on my desktop it was using my Linux client's steam install steamclient.so. I guess the steamclient.so managed by steam is newer than the one shipped with palworld and generates that UID based on the steamid differently? Regardless symlinking to the palworld shipped one does fix my UIDs being different. And for anyone trying to convert a localhost save to a dedicated save it looks like someone made a script (based on cheahjs' code) https://github.com/xNul/palworld-host-save-fix to convert the 00..001 GUID to a proper GUID.

war3i4i commented 7 months ago

One more unrelated question, any idea what the file name structure is? I assume it relates to a Steam ID of some sort but I've spent hours trying to decipher it and how it's encoded and I can't figure it out. I know the 0000000000... file is for a user that creates the world. But the other ones are definitely related to some unique identifier for the user.

I've did some research and basically your Player.sav file has GUID's of your container (inventory) and other stuff. These GUID's then used inside Level.sav itself where it contains data about items / slots / item amounts inside these container GUID's

Example:

image

^My character CommonContainerId

After looking into Level.json you can search for this GUID and look at every item attached

image

I think same logic applied to every other aspect of game and its containers and data holders. Basically your character is just a pointer-holder object that 100% depends on server

trajedyraines commented 7 months ago

Hello.

I am trying to fix the BaseCampWorkerMax limit in Palworld, on my dedicated server. I have uesave but it does nothing. Is there a way to copy a file from a solo world that was generated with the limit increased? I understand the dedicated server files are a bit problematic but several sources online seem to think the process should work but I have not had that experience.

Let me know if you know anything I can do to fix this. Thanks.

DKingAlpha commented 7 months ago

This ByteProperty seems to be breaking the deserialization. It could be possible the same problem of #24

image

image

EDIT: I have managed to parse most of the Level.sav. It's not an issue with uesave-rs, just an internal structure of Palworld game.

DKingAlpha commented 7 months ago

Yeah, this works now. https://github.com/DKingAlpha/palworld-uesave-rs

Converting back and forth, only floats differ. Bytes-accurate for other data.

trumank commented 7 months ago

Excellent work! Gonna close this now as it's out of scope for this repo.