palworld-save-tools icon indicating copy to clipboard operation
palworld-save-tools copied to clipboard

0.6 - Tides of Terraria Compatibility Issue

Open NotLazy opened this issue 6 months ago • 74 comments

Have you modified the save files No

Have you tried the latest release Yes

Describe the bug Exception: not a compressed Palworld save, found b'PlM' instead of b'PlZ'

Copy of .sav or .sav.json files Archive.zip

NotLazy avatar Jun 25 '25 09:06 NotLazy

🚀 WORKING SOLUTION CONFIRMED!

I've successfully tested @TrifingZW's Palexium tool and it works perfectly!

Test Results:

  • Successfully decompressed Level.sav (PlM1 format)
  • File size: 2.5MB → 33MB GVAS format
  • Oodle compression handled flawlessly
  • Ready for further processing with existing tools

This is exactly what the community needed! Huge thanks to the developer for creating this solution so quickly. 🙏

Tool link: https://github.com/TrifingZW/palexium

For anyone struggling with the new save format after the Summer update - this is your answer!

m358807551 avatar Jun 27 '25 15:06 m358807551

@m358807551 They made it private :(

tylercamp avatar Jun 27 '25 17:06 tylercamp

@m358807551 If you have a copy. Do you mind sharing it since the repo you linked is offline now?

d-air1 avatar Jun 27 '25 18:06 d-air1

I didn't mean to make it private, but Oodle is a commercial closed-source library. I obtained its header and static library files, and publishing them publicly raises legal concerns. So I'm figuring out how to avoid this - any good suggestions?

TrifingZW avatar Jun 27 '25 19:06 TrifingZW

I had a feeling that was the issue, which is understandable

For me pseudo-code of the process would be plenty helpful, e.g. is it just a simple call to the Oodle utils to decompress the whole stream, or is there some other parsing / magic bytes that need to be handled first? I tried using 'ooz' to decompress but only got errors

tylercamp avatar Jun 27 '25 19:06 tylercamp

Thanks for your understanding about the licensing considerations - I appreciate you recognizing those complexities. Regarding your request about the decompression process, let me provide a detailed breakdown:

Decompression Process Pseudocode

use std::io::{self, Read, Cursor};

// 存档错误类型 / Save error types
#[derive(Debug)]
pub enum SaveError {
    CorruptedFile,
    InvalidMagicBytes(Vec<u8>),
    UnknownSaveType(u8),
    // 其他错误类型保持简洁 / Other error types kept minimal
}

// 魔术字节常量 / Magic bytes constants
const MAGIC_BYTES_PLZ: &[u8; 3] = b"PlZ";  // Zlib压缩 / Zlib compression
const MAGIC_BYTES_PLM: &[u8; 3] = b"PlM";  // Oodle压缩 / Oodle compression

/// 主解压函数 / Main decompression function
pub fn decompress(data: &[u8]) -> Result<(Vec<u8>, u8), SaveError> {
    // 基本长度检查 / Basic length check
    if data.len() < 24 { return Err(SaveError::CorruptedFile); }
    
    // 解析头部 / Parse header
    let (uncompressed_len, compressed_len, magic_bytes, save_type, data_offset) = 
        parse_save_header(data)?;
    
    // 根据魔术字节选择解压方式 / Select decompression method based on magic bytes
    let uncompressed_data = if magic_bytes == *MAGIC_BYTES_PLZ {
        // Zlib解压逻辑 / Zlib decompression logic
        decompress_zlib(data, data_offset, save_type, uncompressed_len, compressed_len)?
    } else if magic_bytes == *MAGIC_BYTES_PLM {
        // Oodle解压逻辑 / Oodle decompression logic
        decompress_oodle(
            &data[data_offset..],
            uncompressed_len as usize,
            compressed_len as usize
        )?
    } else {
        return Err(SaveError::InvalidMagicBytes(magic_bytes.to_vec()));
    };
    
    // 长度验证 / Length validation
    if uncompressed_data.len() as u32 != uncompressed_len {
        return Err(/* 长度错误 */);
    }
    
    Ok((uncompressed_data, save_type))
}

/// 头部解析核心逻辑 / Core header parsing logic
fn parse_save_header(data: &[u8]) -> Result<(u32, u32, [u8; 3], u8, usize), SaveError> {
    // 1. 检测CNK格式 / Detect CNK format
    let (header_offset, data_offset) = if data.starts_with(b"CNK") {
        // CNK格式有24字节头部 / CNK format has 24-byte header
        (12, 24)
    } else {
        // 标准格式12字节头部 / Standard format 12-byte header
        (0, 12)
    };

    // 2. 读取解压后长度 / Read uncompressed length (4 bytes)
    let uncompressed_len = u32::from_le_bytes([
        data[header_offset],
        data[header_offset + 1],
        data[header_offset + 2],
        data[header_offset + 3],
    ]);

    // 3. 读取压缩后长度 / Read compressed length (4 bytes)
    let compressed_len = u32::from_le_bytes([
        data[header_offset + 4],
        data[header_offset + 5],
        data[header_offset + 6],
        data[header_offset + 7],
    ]);

    // 4. 读取魔术字节 / Read magic bytes (3 bytes)
    let magic_bytes = [
        data[header_offset + 8],
        data[header_offset + 9],
        data[header_offset + 10],
    ];

    // 5. 读取存档类型 / Read save type (1 byte)
    let save_type = data[header_offset + 11];
    
    // 6. 验证存档类型 / Validate save type
    if ![0x30, 0x31, 0x32].contains(&save_type) {
        return Err(SaveError::UnknownSaveType(save_type));
    }

    Ok((uncompressed_len, compressed_len, magic_bytes, save_type, data_offset))
}

/// Zlib解压核心逻辑 / Core Zlib decompression logic
fn decompress_zlib(
    data: &[u8],
    data_offset: usize,
    save_type: u8,
    uncompressed_len: u32,
    compressed_len: u32,
) -> Result<Vec<u8>, SaveError> {
    // 长度验证 / Length validation
    let actual_compressed_len = (data.len() - data_offset) as u32;
    if save_type == 0x31 && compressed_len != actual_compressed_len {
        return Err(/* 压缩长度错误 */);
    }

    // 第一次解压 / First decompression pass
    let mut first_pass = Vec::new();
    // 实际Zlib解压调用 / Actual Zlib decompression call
    // ... 

    // 处理双层压缩 / Handle double compression
    let uncompressed_data = if save_type == 0x32 {
        // 验证第一次解压长度 / Validate first pass length
        if first_pass.len() as u32 != compressed_len {
            return Err(/* 长度不匹配 */);
        }
        
        // 第二次解压 / Second decompression pass
        let mut second_pass = Vec::new();
        // 实际Zlib解压调用 / Actual Zlib decompression call
        // ...
        second_pass
    } else {
        first_pass
    };

    // 最终长度验证 / Final length validation
    if uncompressed_data.len() as u32 != uncompressed_len {
        return Err(/* 解压长度错误 */);
    }

    Ok(uncompressed_data)
}

/// Oodle解压核心逻辑 / Core Oodle decompression logic
fn decompress_oodle(
    compressed_data: &[u8],
    uncompressed_size: usize,
    compressed_size: usize,
) -> Result<Vec<u8>, SaveError> {
    // 长度验证 / Length validation
    if compressed_data.len() != compressed_size {
        return Err(/* 压缩长度错误 */);
    }

    // 准备输出缓冲区 / Prepare output buffer
    let mut output = vec![0u8; uncompressed_size];
    
    // 尝试多种算法 / Try multiple algorithms (Should be Kraken)
    for algorithm in ["Kraken", "Mermaid", "Selkie", "Leviathan"] {
        // 实际Oodle解压调用 / Actual Oodle decompression call
        // let result = OodleLZ_Decompress(...);
        
        // 伪代码成功检查 / Pseudo-code success check
        let success = false; // 实际实现中检查返回值 / Check return value in actual implementation
        
        if success {
            return Ok(output);
        }
    }
    
    Err(/* 所有算法失败 */)
}

TrifingZW avatar Jun 27 '25 20:06 TrifingZW

I was able to get past decompression by grabbing a copy of the Oodle dll from my local install located at D:\Program Files\Epic Games\UE_5.6\Engine\Source\Programs\Shared\EpicGames.Oodle\Sdk\2.9.10\win\redist\oo2core_9_win64.dll, I created and copied it into a folder in the project root named deps. I then refactored palsav.py to

import zlib
import ctypes
from pathlib import Path

MAGIC_BYTES = [b"PlZ", b"PlM"]

# Oodle constants
OODLE_FUZ_YES = 1
OODLE_FUZ_NO = 0


def _get_oodle_lib_path():
    current_dir = Path(__file__).parent
    deps_dir = current_dir.parent / "deps"

    dll_path = deps_dir / "oo2core_9_win64.dll"
    if dll_path.exists():
        return str(dll_path)

    raise FileNotFoundError(
        f"Oodle DLL not found in {deps_dir}. "
        f"Please ensure you have the Oodle DLL files (oo2core_win64.dll) in the deps directory. "
    )


def _load_oodle_lib():
    try:
        lib_path = _get_oodle_lib_path()
        oodle_lib = ctypes.CDLL(lib_path)

        # Define the function signature for OodleLZ_Decompress
        # int OodleLZ_Decompress(const void *compBuf, SINTa compBufSize, void *rawBuf, SINTa rawLen,
        #                        int fuzzSafe, int checkCRC, int verbosity, void *decBufBase,
        #                        SINTa decBufSize, void *fpCallback, void *callbackUserData,
        #                        void *decoderMemory, SINTa decoderMemorySize, int threadPhase)
        oodle_decompress = oodle_lib.OodleLZ_Decompress
        oodle_decompress.argtypes = [
            ctypes.c_void_p,  # compBuf
            ctypes.c_int64,  # compBufSize
            ctypes.c_void_p,  # rawBuf
            ctypes.c_int64,  # rawLen
            ctypes.c_int,  # fuzzSafe
            ctypes.c_int,  # checkCRC
            ctypes.c_int,  # verbosity
            ctypes.c_void_p,  # decBufBase
            ctypes.c_int64,  # decBufSize
            ctypes.c_void_p,  # fpCallback
            ctypes.c_void_p,  # callbackUserData
            ctypes.c_void_p,  # decoderMemory
            ctypes.c_int64,  # decoderMemorySize
            ctypes.c_int,  # threadPhase
        ]
        oodle_decompress.restype = ctypes.c_int64

        return oodle_decompress
    except Exception:
        return None


def _decompress_with_oodle(compressed_data: bytes, uncompressed_len: int) -> bytes:
    oodle_decompress = _load_oodle_lib()
    if oodle_decompress is None:
        raise Exception("Failed to load Oodle library")

    output_buffer = ctypes.create_string_buffer(uncompressed_len)

    result = oodle_decompress(
        compressed_data,  # compBuf
        len(compressed_data),  # compBufSize
        output_buffer,  # rawBuf
        uncompressed_len,  # rawLen
        OODLE_FUZ_YES,  # fuzzSafe
        0,  # checkCRC
        0,  # verbosity
        None,  # decBufBase
        0,  # decBufSize
        None,  # fpCallback
        None,  # callbackUserData
        None,  # decoderMemory
        0,  # decoderMemorySize
        0,  # threadPhase
    )

    if result <= 0:
        raise Exception(f"Oodle decompression failed with result: {result}")

    return output_buffer.raw[:result]


def decompress_sav_to_gvas(data: bytes) -> tuple[bytes, int]:
    uncompressed_len = int.from_bytes(data[0:4], byteorder="little")
    compressed_len = int.from_bytes(data[4:8], byteorder="little")
    magic_bytes = data[8:11]
    save_type = data[11]
    data_start_offset = 12
    # Check for magic bytes
    if magic_bytes == b"CNK":
        uncompressed_len = int.from_bytes(data[12:16], byteorder="little")
        compressed_len = int.from_bytes(data[16:20], byteorder="little")
        magic_bytes = data[20:23]
        save_type = data[23]
        data_start_offset = 24
    if magic_bytes not in MAGIC_BYTES:
        if (
            magic_bytes == b"\x00\x00\x00"
            and uncompressed_len == 0
            and compressed_len == 0
        ):
            raise Exception(
                "not a compressed Palworld save, found too many null bytes, this is likely corrupted"
            )
        raise Exception(
            f"not a compressed Palworld save, found {magic_bytes!r} instead of {MAGIC_BYTES!r}"
        )
    # Valid save types
    if save_type not in [0x30, 0x31, 0x32]:
        raise Exception(f"unknown save type: {save_type}")
    # We only have 0x31 (single zlib) and 0x32 (double zlib) saves
    if save_type not in [0x31, 0x32]:
        raise Exception(f"unhandled compression type: {save_type}")

    if magic_bytes == b"PlM":
        compressed_data = data[data_start_offset:]
        if compressed_len != len(compressed_data):
            raise Exception(f"incorrect compressed length: {compressed_len}")

        uncompressed_data = _decompress_with_oodle(compressed_data, uncompressed_len)
        # Verify the uncompressed length
        if uncompressed_len != len(uncompressed_data):
            raise Exception(
                f"incorrect uncompressed length after Oodle decompression: expected {uncompressed_len}, got {len(uncompressed_data)}"
            )
        return uncompressed_data, save_type

    # Decompress file
    uncompressed_data = zlib.decompress(data[data_start_offset:])
    if save_type == 0x32:
        # Check if the compressed length is correct
        if compressed_len != len(uncompressed_data):
            raise Exception(f"incorrect compressed length: {compressed_len}")
        # Decompress file
        uncompressed_data = zlib.decompress(uncompressed_data)
    # Check if the uncompressed length is correct
    if uncompressed_len != len(uncompressed_data):
        raise Exception(f"incorrect uncompressed length: {uncompressed_len}")

    return uncompressed_data, save_type

# Todo add Oodle compression
def compress_gvas_to_sav(data: bytes, save_type: int) -> bytes:
    uncompressed_len = len(data)
    compressed_data = zlib.compress(data)
    compressed_len = len(compressed_data)
    if save_type == 0x32:
        compressed_data = zlib.compress(compressed_data)

    # Create a byte array and append the necessary information
    result = bytearray()
    result.extend(uncompressed_len.to_bytes(4, byteorder="little"))
    result.extend(compressed_len.to_bytes(4, byteorder="little"))
    result.extend(MAGIC_BYTES)
    result.extend(bytes([save_type]))
    result.extend(compressed_data)

    return bytes(result)

Note: This allows decompression, however, there are many other changes to allow successful conversion. I'll share a link once I have a fully working solution.

oMaN-Rod avatar Jun 27 '25 21:06 oMaN-Rod

I hope this will be useful for somebody. As for me I'm clueless in this field.

I tested Zaigie's version and it successfully decompressed the new Oodle .sav to .gvas https://github.com/zaigie/palworld-save-tools/tree/refactor

fatsby avatar Jun 28 '25 15:06 fatsby

Can you provide a quick and normal solution

scalettovvna avatar Jun 29 '25 12:06 scalettovvna

I've implemented Oodle compression/decompression in this PR #215 . It works perfectly for Player saves (<GUID>.sav), but Level.sav still fails during GVAS parsing with an EOF not reached error. This appears to be a separate parser issue. For those facing the same error, the changes in PR #213 might be a temporary workaround for the Level.sav problem.

Image

MRHRTZ avatar Jun 29 '25 14:06 MRHRTZ

@MRHRTZ Oodle is a proprietary commercial library, and public disclosure of its contents would lead to legal issues.

TrifingZW avatar Jun 29 '25 16:06 TrifingZW

+1 to what TrifingZW said

I was able to use libooz from here successfully in my own program for Oodle decompression, which should get around licensing issues (the latest "Bun" release ZIP will have libooz which can be used on its own)

Binding signature is here, my usage is here

Note that the buffer for decompression needs an extra 64 bytes (at least - I used 128) since it writes past the "end" of the buffer (see SAFE_SPACE decl)


It would be nice if we could hook into the Oodle impl. in Palworld, but it's been statically linked and Ghidra shows the symbols have been mangled/lost (as expected), so making that work doesn't seem feasible

tylercamp avatar Jun 29 '25 16:06 tylercamp

If anyone figures out a solution, could you please let me know? I’ve been trying to set up a dedicated server and have spent hours already trying to find a way to transfer the save. I don’t really understand anything about this area. I would be really grateful if someone could help me.

Wesley89656 avatar Jun 29 '25 17:06 Wesley89656

Thanks @TrifingZW for the important heads-up on the legal issues with the official oo2core DLL.

And thank you to @tylercamp for suggesting the zao/ooz project. I've done a deep dive based on your recommendation, and here are my findings:

Decompression: Success! I can confirm that using libooz.dll via ctypes works perfectly for decompressing Palworld save files, as long as the SAFE_SPACE buffer padding is handled correctly. This provides a fully legal, open-source way to read the save files.

Compression: Roadblock. My attempt to use ooz.exe for compression has unfortunately hit a definitive wall. I've discovered that ooz.exe does not create a raw Oodle stream (which Palworld requires). Instead, it creates a .ooz archive format which includes its own file header (magic bytes \xae\xb2...). This makes its output incompatible for writing back into a .sav file.

This leads me to a final question: while libooz.dll is excellent for decompression, does anyone have a working example of using it for compression? I have not been able to find an exported Ooz_Compress function in any of the builds I've tested.

It seems we have a great open-source solution for reading saves, but a fully legal and functional solution for writing them back remains an open challenge. Any further insights on libooz.dll's compression capabilities would be greatly appreciated.

MRHRTZ avatar Jun 29 '25 19:06 MRHRTZ

It looks like libooz has python bindings at https://github.com/zao/pyooz - they're a bit out of date, sometime later this week I'll try and see whether a simple update of the submodule for libooz works.

Edit: Builds cleanly, untested. I'll try to open a PR to pyooz tomorrow

Entropy512 avatar Jun 30 '25 00:06 Entropy512

I decided not to use the pyooz Python binding, as I directly load libooz.dll using ctypes. Also, the pyooz implementation currently only available for decompression.

To support compression from Python, I forked the ooz repository and added an explicit export for the Ooz_Compress function. You can find the updated version here: https://github.com/MRHRTZ/ooz

With this change, I was able to successfully use both compression and decompression through libooz.dll from Python. I'm not sure when this PR #216 will be merged, but in the meantime, feel free to try out my updated tool: https://github.com/MRHRTZ/palworld-save-tools

However, there's a new issue: after editing and saving the data, Palworld creates a new save file instead of loading the modified one. This might be due to a change in the internal save structure or additional validation by the game.

If anyone has insight into how the new structure works or how Palworld verifies save data, it would be really helpful for further progress.

Image

MRHRTZ avatar Jun 30 '25 15:06 MRHRTZ

I decided not to use the pyooz Python binding, as I directly load libooz.dll using ctypes. Also, the pyooz implementation currently only available for decompression.

To support compression from Python, I forked the ooz repository and added an explicit export for the Ooz_Compress function. You can find the updated version here: https://github.com/MRHRTZ/ooz

The problem with this approach is that it isn't portable.

It looks like the appropriate sequence here would be:

  • Submit your libooz change to upstream
  • Plumb in compression support in pyooz (I can take a look at this)
  • Use pyooz here for cross platform compatibility

Unfortunately since you committed a formatter run and a code change in a single commit, the commit is unreadable as far as figuring out what you actually changed in the 538 lines of formatting changes... :(

Entropy512 avatar Jun 30 '25 16:06 Entropy512

has cheahjs (owner of the repo) been active/merging any PRs? the main branch says they haven't made any commits in 9 months. I'm wondering if they'll implement this fix, or if it'll have to go in a fork

Piezoelectric avatar Jun 30 '25 16:06 Piezoelectric

The problem with this approach is that it isn't portable.

You're absolutely right, this was intended primarily for testing purposes at this stage. I agree that for long-term and cross-platform support, submitting changes upstream and integrating through pyooz is the right way to go. I'll make sure to follow that direction moving forward, and also split formatting and functional changes into separate commits next time. Thanks for the feedback!

For the exporting part, I only added the following code in kraken.cpp to expose the compression function if you're curious about the actual change:

struct CompressOptions;
struct LRMCascade;

int CompressBlock(int codec_id, uint8 *src_in, uint8 *dst_in, int src_size, int level,
                  const CompressOptions *compressopts, uint8 *src_window_base, LRMCascade *lrm);

extern "C" {
OOZ_DLL_PUBLIC int Ooz_Compress(int compressor, const uint8_t *src_buf, int src_len, uint8_t *dst_buf,
                                size_t dst_capacity, int level) {
  return CompressBlock(compressor, (uint8_t *)src_buf, dst_buf, src_len, level, nullptr, nullptr, nullptr);
}
}

MRHRTZ avatar Jun 30 '25 17:06 MRHRTZ

OK, EXTREMELY preliminary compressor implementation for pyooz at https://github.com/Entropy512/pyooz/tree/add_compress - I've bumped ooz to use your PR, but looking at the pyooz implementation, your DLL export shouldn't be needed since pyooz builds the relevant source code directly. It compiles but is completely untested as I need to wrap things up for the night and work on cleaning my disaster zone in preparation for family visiting later this week. So if it segfaults and eats babies, you were warned. :)

At least on initial looking at your implementation, it might be that you're missing that "size header" stuff in the compressed data - see https://github.com/zao/ooz/blob/master/kraken.cpp#L4441 and the 8 byte offset passed to the compress routine. I'm assuming that if you were able to decompress a .sav file with the standard decompressor, your compressed output needs that size header, which might be why palworld is rejecting it.

Entropy512 avatar Jun 30 '25 23:06 Entropy512

Temporary iteration until oodle compression is figured out using an open source alternative, I've managed to convert and edit save files using this fork. It still depends on oo2core_9_win64.dll which is passed into decompress_sav_to_gvas and compress_gvas_to_sav using the oodle_path parameter, or using the --oodle-path cli arg.

However, I've noticed longer than usual load times for edited saves and occasionally the game will crash on the first load, subsequent loads work using the same save file.

oMaN-Rod avatar Jul 01 '25 01:07 oMaN-Rod

The save loading issue seems to be partially resolved. Header size doesn’t affect Players/<GUID>.sav, since its content matches the original. But for Level.sav, the problem might be related to Oodle compression, possibly because Players/.sav has already been successfully compressed using libooz, while Level.sav may require specific options.

Based on what I saw in @oMaN-Rod repository, Level.sav was originally compressed with Oodle and later recompressed using Zlib and it still worked. So as long as Zlib is still supported, this approach seems reliable.

I also tested creating a new save and modifying both items and character data, and it worked fine. However, migrating old saves from version 0.4 results in crashes or stuck loading, maybe due to structural differences.

For now, the converter is working well, and I’ve updated my forked GitHub repo with the latest commit. I’ll continue implementing Oodle compression support using pyooz.

MRHRTZ avatar Jul 01 '25 07:07 MRHRTZ

I'm not surprised that recompressing to zlib worked.

Ah, so you reused the existing size header for players.sav?

Entropy512 avatar Jul 01 '25 10:07 Entropy512

Temporary iteration until oodle compression is figured out using an open source alternative, I've managed to convert and edit save files using this fork. It still depends on oo2core_9_win64.dll which is passed into decompress_sav_to_gvas and compress_gvas_to_sav using the oodle_path parameter, or using the --oodle-path cli arg.

However, I've noticed longer than usual load times for edited saves and occasionally the game will crash on the first load, subsequent loads work using the same save file.

@oMaN-Rod Your fork didnt work for me. Sorry for asking here, but i cant create an issue on your fork. Can u help me?

C900B9B6FBDF47A78260D4E719F0E681.zip

PS C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6> python -m palworld_save_tools.commands.convert palworld_save_tools\commands\Level.sav --to-json --oodle-path "C:\Users\Chiko\Desktop\PAL-Tools\oodle\Windows\oo2core_9_win64.dll" Converting palworld_save_tools\commands\Level.sav to JSON, saving to palworld_save_tools\commands\Level.sav.json Decompressing sav file Loading GVAS file Warning: Unknown map object concrete model PalMapObjectItemChest_AffectCorruption, skipping Warning: Map object 'BaseCampWorkerExtraStation' not in database, skipping Warning: Map object 'BaseCampWorkerExtraStation' not in database, skipping Warning: Map object 'BaseCampWorkerExtraStation' not in database, skipping Warning: Map object 'BaseCampWorkerExtraStation' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Warning: Map object 'OperatingTable' not in database, skipping Traceback (most recent call last): File "<frozen runpy>", line 198, in _run_module_as_main File "<frozen runpy>", line 88, in _run_code File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\commands\convert.py", line 178, in <module> main() File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\commands\convert.py", line 80, in main convert_sav_to_json( File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\commands\convert.py", line 132, in convert_sav_to_json gvas_file = GvasFile.read( ^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\gvas.py", line 131, in read gvas_file.properties = reader.properties_until_end() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 386, in properties_until_end properties[name] = self.property(type_name, size, f"{path}.{name}") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 399, in property value = self.struct(path) ^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 554, in struct value = self.struct_value(struct_type, path) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 581, in struct_value return self.properties_until_end(path) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 386, in properties_until_end properties[name] = self.property(type_name, size, f"{path}.{name}") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\archive.py", line 396, in property value = self.custom_properties[path][0](self, type_name, size, path) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\rawdata\map_object.py", line 45, in decode map_concrete_model.decode_bytes( File "C:\Users\Chiko\Desktop\PAL-Tools\palworld-save-tools-0.6\palworld_save_tools\rawdata\map_concrete_model.py", line 564, in decode_bytes raise Exception( Exception: Warning: EOF not reached for BreedFarm PalMapObjectBreedFarmModel: ori: 2fe852810c476e0c5dd301a8d4cd5c8a0856c3ac514618534a956ebcd8f562460000000005000000562a0f72684fa28a568cd5a43b4a25718ac8fe6af745030b557b60815f5b1aef24aca7f852426787520dc5abe298f4484c95ef25154a2705da69cb800b7642564cc4b2ae32493d470ab9f997d6732b2a00000000 remaining: 80

Chiko1337 avatar Jul 01 '25 20:07 Chiko1337

@Chiko1337 I just pushed some changes and also enabled issues, try again and open an issue if you run into any problems.

oMaN-Rod avatar Jul 01 '25 20:07 oMaN-Rod

@Chiko1337 I just pushed some changes and also enabled issues, try again and open an issue if you run into any problems.

I got this error now Checking if Python is installed as python3 C:\Users\PC\AppData\Local\Microsoft\WindowsApps\python3.exe Found Python at python3 Python version: Python 3.13.5 Converting C:\Users\PC\Downloads\palworld_2535421831072500_2025-07-01_11_35_23\56F07D DD43A9E71C95D33D96DA0892DF\Level.sav to JSON, saving to C:\Users\PC\Downloads\palworl d_2535421831072500_2025-07-01_11_35_23\56F07DDD43A9E71C95D33D96DA0892DF\Level.sav.jso n Decompressing sav file Traceback (most recent call last): File "C:\Users\PC\Downloads\palworld-save-tools-windows-v0.24.0\convert.py", line 1 64, in main() ~~~~^^ File "C:\Users\PC\Downloads\palworld-save-tools-windows-v0.24.0\convert.py", line 7 5, in main convert_sav_to_json( ~~~~~~~~~~~~~~~~~~~^ args.filename, ^^^^^^^^^^^^^^ ...<4 lines>... custom_properties_keys=args.custom_properties, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "C:\Users\PC\Downloads\palworld-save-tools-windows-v0.24.0\convert.py", line 1 09, in convert_sav_to_json raw_gvas, _ = decompress_sav_to_gvas(data) ~~~~~~~~~~~~~~~~~~~~~~^^^^^^ TypeError: decompress_sav_to_gvas() missing 1 required positional argument: 'oodle_pa th' Presione una tecla para continuar . . .

hunksurvivorx avatar Jul 01 '25 20:07 hunksurvivorx

@Chiko1337 I noticed after the fact you attached your save, I tested it and was able to convert successfully. I did notice some unknown map objects and work types that are reverting to raw data, I'll work on getting them added this evening.

@hunksurvivorx You have to acquire the oodle dll and pass in the path as a cli arg, i.e.,

python -m palworld_save_tools.commands.convert D:\debug\C900B9B6FBDF47A78260D4E719F0E681\Level.sav --oodle-path .\oodle\oo2core_9_win64.dll

I enabled issues on that fork to avoid polluting this thread, please report any issues specific to that fork there.

oMaN-Rod avatar Jul 01 '25 21:07 oMaN-Rod

@Chiko1337 I noticed after the fact you attached your save, I tested it and was able to convert successfully. I did notice some unknown map objects and work types that are reverting to raw data, I'll work on getting them added this evening.

@hunksurvivorx You have to acquire the oodle dll and pass in the path as a cli arg, i.e.,

python -m palworld_save_tools.commands.convert D:\debug\C900B9B6FBDF47A78260D4E719F0E681\Level.sav --oodle-path .\oodle\oo2core_9_win64.dll

I enabled issues on that fork to avoid polluting this thread, please report any issues specific to that fork there.

I have the same issues as @hunksurvivorx When I tried those arguments, it fails.

``C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0>python -m palworld_save_tools.commands.convert C:\Users\patri\Desktop\49DF068A456F5A889B7974AFF139B62B\Level.sav --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll usage: palworld-save-tools [-h] [--to-json] [--from-json] [--output OUTPUT] [--force] [--convert-nan-to-null] [--custom-properties CUSTOM_PROPERTIES] [--minify-json] filename palworld-save-tools: error: unrecognized arguments: --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll

C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0>``

Yes the path to oo2core_9_win64.dll is located there. I made the folder and put it there myself manually. Please hold my hand because I am stupid.

Thwarted2100 avatar Jul 01 '25 23:07 Thwarted2100

@Chiko1337 I noticed after the fact you attached your save, I tested it and was able to convert successfully. I did notice some unknown map objects and work types that are reverting to raw data, I'll work on getting them added this evening. @hunksurvivorx You have to acquire the oodle dll and pass in the path as a cli arg, i.e., python -m palworld_save_tools.commands.convert D:\debug\C900B9B6FBDF47A78260D4E719F0E681\Level.sav --oodle-path .\oodle\oo2core_9_win64.dll I enabled issues on that fork to avoid polluting this thread, please report any issues specific to that fork there.

I have the same issues as @hunksurvivorx When I tried those arguments, it fails.

``C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0>python -m palworld_save_tools.commands.convert C:\Users\patri\Desktop\49DF068A456F5A889B7974AFF139B62B\Level.sav --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll usage: palworld-save-tools [-h] [--to-json] [--from-json] [--output OUTPUT] [--force] [--convert-nan-to-null] [--custom-properties CUSTOM_PROPERTIES] [--minify-json] filename palworld-save-tools: error: unrecognized arguments: --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll

C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0>``

Yes the path to oo2core_9_win64.dll is located there. I made the folder and put it there myself manually. Please hold my hand because I am stupid.

Make sure u use this fork. It worked for me.

python -m palworld_save_tools.commands.convert palworld_save_tools\commands\Level.sav --to-json --oodle-path "C:\Users\Chiko\Desktop\PAL-Tools\oodle\Windows\oo2core_9_win64.dll"

Chiko1337 avatar Jul 01 '25 23:07 Chiko1337

@Chiko1337 I noticed after the fact you attached your save, I tested it and was able to convert successfully. I did notice some unknown map objects and work types that are reverting to raw data, I'll work on getting them added this evening. @hunksurvivorx You have to acquire the oodle dll and pass in the path as a cli arg, i.e., python -m palworld_save_tools.commands.convert D:\debug\C900B9B6FBDF47A78260D4E719F0E681\Level.sav --oodle-path .\oodle\oo2core_9_win64.dll I enabled issues on that fork to avoid polluting this thread, please report any issues specific to that fork there.

I have the same issues as @hunksurvivorx When I tried those arguments, it fails. C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0>python -m palworld_save_tools.commands.convert C:\Users\patri\Desktop\49DF068A456F5A889B7974AFF139B62B\Level.sav --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll usage: palworld-save-tools [-h] [--to-json] [--from-json] [--output OUTPUT] [--force] [--convert-nan-to-null] [--custom-properties CUSTOM_PROPERTIES] [--minify-json] filename palworld-save-tools: error: unrecognized arguments: --oodle-path C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0\palworld_save_tools\oodle\oo2core_9_win64.dll C:\Users\patri\Desktop\palworld-save-tools-windows-v0.24.0> Yes the path to oo2core_9_win64.dll is located there. I made the folder and put it there myself manually. Please hold my hand because I am stupid.

Make sure u use this fork. It worked for me.

python -m palworld_save_tools.commands.convert palworld_save_tools\commands\Level.sav --to-json --oodle-path "C:\Users\Chiko\Desktop\PAL-Tools\oodle\Windows\oo2core_9_win64.dll"

That fork seems broken for me. Same exact error. I thought I fixed it once because I had a different error.

Error while finding module specification for 'palworld_save_tools.commands.convert' (ModuleNotFoundError: No module named 'palworld_save_tools')

Thing is, I wasn't using arguments for it, it just decided it doesn't exist. Even moved it to root to see if that was an issue but it's not.

Windows CMD is not what I am used too, that might be the problem.

Thwarted2100 avatar Jul 02 '25 03:07 Thwarted2100