diff --git a/tools/save_converter/build.bat b/tools/save_converter/build.bat deleted file mode 100644 index 4dd52e7b..00000000 --- a/tools/save_converter/build.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" x64 -cd /d "C:\Users\cole\Documents\GitHub\client\tools\save_converter" -cl /LD /O2 kernelx_shim.c /link /DEF:kernelx.def /OUT:kernelx.dll diff --git a/tools/save_converter/convert_x360_save.py b/tools/save_converter/convert_x360_save.py deleted file mode 100644 index 4b7a608a..00000000 --- a/tools/save_converter/convert_x360_save.py +++ /dev/null @@ -1,403 +0,0 @@ -""" -Xbox 360 Minecraft LCE savegame.dat -> Windows .mcs converter - -Usage: python convert_x360_save.py [output.mcs] - -This tool: -1. Strips the 4-byte STFS size prefix from the Xbox 360 save -2. Decompresses LZXRLE data (LZX via xcompress.dll, then custom RLE) -3. Byte-swaps the FileHeader and FileEntry structures (big-endian -> little-endian) -4. Byte-swaps region file data (chunk offsets and timestamps) -5. Re-compresses as ZLIBRLE (zlib + RLE) for the Windows build -6. Writes a .mcs file loadable by the LCE Windows build -""" - -import ctypes -import struct -import zlib -import os -import sys - -SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) - -# --- LZX Decompression via xcompress.dll --- - -def load_xcompress(): - """Load xcompress.dll with the kernelx.dll shim.""" - os.add_dll_directory(SCRIPT_DIR) - ctypes.WinDLL(os.path.join(SCRIPT_DIR, "kernelx.dll")) - return ctypes.WinDLL(os.path.join(SCRIPT_DIR, "xcompress.dll")) - - -def lzx_decompress(xcomp, compressed_data, decompressed_size): - """Decompress LZX data using XMemDecompress.""" - XMEMCODEC_LZX = 1 - - class XMEMCODEC_PARAMETERS_LZX(ctypes.Structure): - _fields_ = [ - ("Flags", ctypes.c_uint32), - ("WindowSize", ctypes.c_uint32), - ("CompressionPartitionSize", ctypes.c_uint32), - ] - - params = XMEMCODEC_PARAMETERS_LZX() - params.Flags = 0 - params.WindowSize = 128 * 1024 - params.CompressionPartitionSize = 128 * 1024 - - context = ctypes.c_void_p() - hr = xcomp.XMemCreateDecompressionContext( - XMEMCODEC_LZX, - ctypes.byref(params), - 0, - ctypes.byref(context), - ) - if hr != 0: - raise RuntimeError(f"XMemCreateDecompressionContext failed: 0x{hr:08X}") - - src_buf = (ctypes.c_ubyte * len(compressed_data))(*compressed_data) - dst_buf = (ctypes.c_ubyte * decompressed_size)() - dst_size = ctypes.c_size_t(decompressed_size) - - hr = xcomp.XMemDecompress( - context, - dst_buf, - ctypes.byref(dst_size), - src_buf, - ctypes.c_size_t(len(compressed_data)), - ) - - xcomp.XMemDestroyDecompressionContext(context) - - if hr != 0: - raise RuntimeError(f"XMemDecompress failed: 0x{hr:08X}") - - return bytes(dst_buf[: dst_size.value]) - - -# --- RLE Decompression (from compression.cpp DecompressLZXRLE) --- - -def rle_decompress(data): - """ - Decompress the custom RLE format used by LCE. - Format: - 0-254: literal byte - 255 followed by 0,1,2: 1,2,3 literal 255s - 255 followed by 3-255 followed by byte: run of (count+1) copies of byte - """ - result = bytearray() - i = 0 - length = len(data) - - while i < length: - b = data[i] - i += 1 - if b == 255: - if i >= length: - break - count = data[i] - i += 1 - if count < 3: - result.extend(b"\xff" * (count + 1)) - else: - if i >= length: - break - val = data[i] - i += 1 - result.extend(bytes([val]) * (count + 1)) - else: - result.append(b) - - return bytes(result) - - -# --- RLE Compression (from compression.cpp CompressRLE / CompressLZXRLE) --- - -def rle_compress(data): - """ - Compress with the custom RLE format used by LCE. - """ - result = bytearray() - i = 0 - length = len(data) - - while i < length: - b = data[i] - i += 1 - count = 1 - while i < length and data[i] == b and count < 256: - i += 1 - count += 1 - - if count <= 3: - if b == 255: - result.append(255) - result.append(count - 1) - else: - result.extend(bytes([b]) * count) - else: - result.append(255) - result.append(count - 1) - result.append(b) - - return bytes(result) - - -# --- Endianness Conversion --- - -def swap16(data, offset): - """Swap a 16-bit value in-place.""" - data[offset], data[offset + 1] = data[offset + 1], data[offset] - - -def swap32(data, offset): - """Swap a 32-bit value in-place.""" - data[offset : offset + 4] = data[offset : offset + 4][::-1] - - -def swap64(data, offset): - """Swap a 64-bit value in-place.""" - data[offset : offset + 8] = data[offset : offset + 8][::-1] - - -def swap_wchar_array(data, offset, count): - """Swap an array of wchar_t (2 bytes each).""" - for i in range(count): - swap16(data, offset + i * 2) - - -def convert_file_header(data): - """ - Convert the FileHeader from big-endian (Xbox 360) to little-endian (Windows). - - Layout of decompressed save data: - Bytes 0-3: headerOffset (uint32) - Bytes 4-7: headerSize (uint32) = number of file entries - Bytes 8-9: originalSaveVersion (int16) - Bytes 10-11: saveVersion (int16) - Bytes 12+: file data - At headerOffset: file table (headerSize entries, each 144 bytes) - """ - buf = bytearray(data) - - # Read header values in big-endian - header_offset = struct.unpack(">I", buf[0:4])[0] - header_size = struct.unpack(">I", buf[4:8])[0] - orig_version = struct.unpack(">h", buf[8:10])[0] - save_version = struct.unpack(">h", buf[10:12])[0] - - print(f" Header offset: {header_offset}") - print(f" File count: {header_size}") - print(f" Original version: {orig_version}") - print(f" Save version: {save_version}") - - # Swap the header fields to little-endian - swap32(buf, 0) # headerOffset - swap32(buf, 4) # headerSize - swap16(buf, 8) # originalSaveVersion - swap16(buf, 10) # saveVersion - - # Swap each FileEntry in the file table - # FileEntrySaveDataV2 = 144 bytes: - # wchar_t filename[64] = 128 bytes - # uint32 length = 4 bytes - # uint32 startOffset = 4 bytes - # int64 lastModified = 8 bytes - ENTRY_SIZE = 144 - - print(f"\n Files in save:") - for i in range(header_size): - entry_offset = header_offset + i * ENTRY_SIZE - - # Read filename before swapping (big-endian wchar) - raw_name = buf[entry_offset : entry_offset + 128] - name_chars = [] - for j in range(64): - c = struct.unpack(">H", raw_name[j * 2 : j * 2 + 2])[0] - if c == 0: - break - name_chars.append(chr(c)) - filename = "".join(name_chars) - - length = struct.unpack(">I", buf[entry_offset + 128 : entry_offset + 132])[0] - start = struct.unpack(">I", buf[entry_offset + 132 : entry_offset + 136])[0] - - print(f" {filename!r}: offset={start}, length={length}") - - # Swap the fields - swap_wchar_array(buf, entry_offset, 64) # filename - swap32(buf, entry_offset + 128) # length - swap32(buf, entry_offset + 132) # startOffset - swap64(buf, entry_offset + 136) # lastModifiedTime - - return bytes(buf), header_size, header_offset, save_version - - -def convert_region_files(data, header_offset, header_size): - """ - Convert region file data from big-endian to little-endian. - Region files (.mcr) contain: - - Sector 0: offset table (1024 uint32 entries) - - Sector 1: timestamp table (1024 uint32 entries) - - Sectors 2+: chunk data with per-chunk headers - - This converts the offset/timestamp tables and chunk headers. - """ - buf = bytearray(data) - ENTRY_SIZE = 144 - SECTOR_SIZE = 4096 - SECTOR_INTS = 1024 - - for i in range(header_size): - entry_offset = header_offset + i * ENTRY_SIZE - - # Read filename (now in little-endian after header conversion) - name_chars = [] - for j in range(64): - c = struct.unpack("> 8 - - if sector_offset < 2: - continue - - chunk_data_offset = file_start + sector_offset * SECTOR_SIZE - if chunk_data_offset + 8 > len(buf): - continue - - # Swap the chunk header (compressed_length and decompressed_length) - swap32(buf, chunk_data_offset) - swap32(buf, chunk_data_offset + 4) - - return bytes(buf) - - -# --- Main Conversion --- - -def convert_save(input_path, output_path): - print(f"Loading {input_path}...") - with open(input_path, "rb") as f: - raw_data = f.read() - - file_size = len(raw_data) - print(f"File size: {file_size} bytes") - - # Check for 4-byte STFS size prefix - # The first 4 bytes (BE) should match approximately (file_size - padding - 4) - prefix_size = struct.unpack(">I", raw_data[0:4])[0] - comp_flag = struct.unpack(">I", raw_data[4:8])[0] - - if comp_flag == 0 and prefix_size > 0 and prefix_size < file_size: - print(f"Detected 4-byte STFS prefix (size={prefix_size})") - save_data = raw_data[4:] - else: - save_data = raw_data - - # Parse compression header - compressed_flag = struct.unpack(">I", save_data[0:4])[0] - if compressed_flag != 0: - print("Save data does not appear to be compressed (flag != 0)") - print("Treating as raw uncompressed save data") - decompressed = save_data - else: - decomp_size = struct.unpack(">I", save_data[4:8])[0] - compressed_data = save_data[8:] - print(f"Compressed save: decompressed size = {decomp_size} ({decomp_size/1024/1024:.1f} MB)") - print(f"Compressed data size: {len(compressed_data)} bytes") - - # LZX decompress (whole-file level is pure LZX, not LZXRLE) - # The RLE layer is only used per-chunk inside region files - print("\nStep 1: LZX decompression...") - xcomp = load_xcompress() - decompressed = lzx_decompress(xcomp, compressed_data, decomp_size) - print(f" LZX decompressed: {len(decompressed)} bytes") - - # Step 2: Inspect the raw save structure (big-endian) - print("\nStep 2: Inspecting raw save data (big-endian Xbox 360 format)...") - header_offset = struct.unpack(">I", decompressed[0:4])[0] - file_count = struct.unpack(">I", decompressed[4:8])[0] - orig_version = struct.unpack(">h", decompressed[8:10])[0] - save_version = struct.unpack(">h", decompressed[10:12])[0] - print(f" Header offset: {header_offset}") - print(f" File count: {file_count}") - print(f" Original version: {orig_version}, Save version: {save_version}") - - # List files in the save - ENTRY_SIZE = 144 - print(f"\n Files in save:") - for i in range(file_count): - entry_off = header_offset + i * ENTRY_SIZE - name_chars = [] - for j in range(64): - c = struct.unpack(">H", decompressed[entry_off + j * 2 : entry_off + j * 2 + 2])[0] - if c == 0: - break - name_chars.append(chr(c)) - filename = "".join(name_chars) - length = struct.unpack(">I", decompressed[entry_off + 128 : entry_off + 132])[0] - start = struct.unpack(">I", decompressed[entry_off + 132 : entry_off + 136])[0] - print(f" {filename}: offset={start}, size={length}") - - # Step 3: Write raw decompressed data - # The game code will handle endianness conversion when loaded with plat=X360 - # Write as uncompressed (first 4 bytes != 0 tells the game it's uncompressed) - print(f"\nStep 3: Writing {output_path}...") - with open(output_path, "wb") as f: - f.write(decompressed) - - print(f"\nDone! Output: {output_path} ({len(decompressed)} bytes)") - print(f"\nTo use this save with the LCE source code:") - print(f" 1. Ensure xcompress.dll is available to the Windows build") - print(f" 2. Replace XMemDecompress stubs with real implementation") - print(f" 3. Load with: ConsoleSaveFileOriginal(name, data, size, false, SAVE_FILE_PLATFORM_X360)") - print(f" 4. Call pSave->ConvertToLocalPlatform() after loading") - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print(f"Usage: {sys.argv[0]} [output.mcs]") - sys.exit(1) - - input_file = sys.argv[1] - if len(sys.argv) >= 3: - output_file = sys.argv[2] - else: - base = os.path.splitext(input_file)[0] - output_file = base + ".mcs" - - convert_save(input_file, output_file) diff --git a/tools/save_converter/converted_world.mcs b/tools/save_converter/converted_world.mcs deleted file mode 100644 index 4f5e36a3..00000000 Binary files a/tools/save_converter/converted_world.mcs and /dev/null differ diff --git a/tools/save_converter/kernelx.def b/tools/save_converter/kernelx.def deleted file mode 100644 index c0cb516c..00000000 --- a/tools/save_converter/kernelx.def +++ /dev/null @@ -1,14 +0,0 @@ -LIBRARY kernelx -EXPORTS - XMemAlloc - XMemFree - SetUnhandledExceptionFilter = kernel32.SetUnhandledExceptionFilter - UnhandledExceptionFilter = kernel32.UnhandledExceptionFilter - GetTickCount = kernel32.GetTickCount - GetSystemTimeAsFileTime = kernel32.GetSystemTimeAsFileTime - GetCurrentThreadId = kernel32.GetCurrentThreadId - GetCurrentProcessId = kernel32.GetCurrentProcessId - QueryPerformanceCounter = kernel32.QueryPerformanceCounter - Sleep = kernel32.Sleep - TerminateProcess = kernel32.TerminateProcess - GetCurrentProcess = kernel32.GetCurrentProcess diff --git a/tools/save_converter/kernelx.dll b/tools/save_converter/kernelx.dll deleted file mode 100644 index e9e1ebba..00000000 Binary files a/tools/save_converter/kernelx.dll and /dev/null differ diff --git a/tools/save_converter/kernelx_shim.c b/tools/save_converter/kernelx_shim.c deleted file mode 100644 index 8deb608f..00000000 --- a/tools/save_converter/kernelx_shim.c +++ /dev/null @@ -1,25 +0,0 @@ -// kernelx.dll shim - forwards standard Win32 functions to kernel32.dll -// and provides stub implementations for Xbox-specific XMemAlloc/XMemFree -#include -#include - -// Forward declarations for exports -__declspec(dllexport) void* __stdcall XMemAlloc(SIZE_T dwSize, DWORD dwAllocAttributes); -__declspec(dllexport) void __stdcall XMemFree(void* pAddress, DWORD dwAllocAttributes); - -// Xbox-specific memory functions - map to standard heap -__declspec(dllexport) void* __stdcall XMemAlloc(SIZE_T dwSize, DWORD dwAllocAttributes) { - (void)dwAllocAttributes; - return malloc(dwSize); -} - -__declspec(dllexport) void __stdcall XMemFree(void* pAddress, DWORD dwAllocAttributes) { - (void)dwAllocAttributes; - free(pAddress); -} - -// All other functions are forwarded to kernel32.dll via the .def file -BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { - (void)hinstDLL; (void)fdwReason; (void)lpReserved; - return TRUE; -} diff --git a/tools/save_converter/kernelx_shim.exp b/tools/save_converter/kernelx_shim.exp deleted file mode 100644 index 8ae13163..00000000 Binary files a/tools/save_converter/kernelx_shim.exp and /dev/null differ diff --git a/tools/save_converter/kernelx_shim.lib b/tools/save_converter/kernelx_shim.lib deleted file mode 100644 index 99c9a948..00000000 Binary files a/tools/save_converter/kernelx_shim.lib and /dev/null differ diff --git a/tools/save_converter/xcompress.dll b/tools/save_converter/xcompress.dll deleted file mode 100644 index b95e3701..00000000 Binary files a/tools/save_converter/xcompress.dll and /dev/null differ