228 lines
6.8 KiB
Python
228 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import zlib
|
|
|
|
FILE_ENTRY_SIZE = 144
|
|
SAVE_HEADER_SIZE = 12
|
|
REGION_FORMAT_16_NAME = "region_format_16"
|
|
|
|
|
|
def read_u16(data, offset, endian):
|
|
return int.from_bytes(data[offset:offset + 2], endian)
|
|
|
|
|
|
def read_u32(data, offset, endian):
|
|
return int.from_bytes(data[offset:offset + 4], endian)
|
|
|
|
|
|
def read_u64(data, offset, endian):
|
|
return int.from_bytes(data[offset:offset + 8], endian)
|
|
|
|
|
|
def write_u32(buf, offset, value, endian):
|
|
buf[offset:offset + 4] = int(value).to_bytes(4, endian, signed=False)
|
|
|
|
|
|
def write_u64(buf, offset, value, endian):
|
|
buf[offset:offset + 8] = int(value).to_bytes(8, endian, signed=False)
|
|
|
|
|
|
def detect_payload_endian(payload):
|
|
for endian in ("little", "big"):
|
|
header_offset = read_u32(payload, 0, endian)
|
|
header_count = read_u32(payload, 4, endian)
|
|
if header_offset < SAVE_HEADER_SIZE:
|
|
continue
|
|
if header_count == 0 or header_count > 50000:
|
|
continue
|
|
if header_offset + (header_count * FILE_ENTRY_SIZE) == len(payload):
|
|
return endian
|
|
return None
|
|
|
|
|
|
def parse_entries(payload, endian):
|
|
header_offset = read_u32(payload, 0, endian)
|
|
header_count = read_u32(payload, 4, endian)
|
|
original_version = read_u16(payload, 8, endian)
|
|
current_version = read_u16(payload, 10, endian)
|
|
|
|
if header_offset + (header_count * FILE_ENTRY_SIZE) != len(payload):
|
|
raise ValueError("Invalid save header layout")
|
|
|
|
entries = []
|
|
text_encoding = "utf-16-le" if endian == "little" else "utf-16-be"
|
|
for i in range(header_count):
|
|
entry_offset = header_offset + (i * FILE_ENTRY_SIZE)
|
|
raw_name = payload[entry_offset:entry_offset + 128]
|
|
name = raw_name.decode(text_encoding, errors="ignore").split("\x00", 1)[0]
|
|
length = read_u32(payload, entry_offset + 128, endian)
|
|
start = read_u32(payload, entry_offset + 132, endian)
|
|
last_modified = read_u64(payload, entry_offset + 136, endian)
|
|
entries.append(
|
|
{
|
|
"name": name,
|
|
"length": length,
|
|
"start": start,
|
|
"last_modified": last_modified,
|
|
}
|
|
)
|
|
|
|
return {
|
|
"header_offset": header_offset,
|
|
"original_version": original_version,
|
|
"current_version": current_version,
|
|
"entries": entries,
|
|
}
|
|
|
|
|
|
def serialize_payload_from_entries(entries, original_version, current_version, endian):
|
|
data_payload = bytearray(SAVE_HEADER_SIZE)
|
|
serialized_entries = []
|
|
|
|
for entry in entries:
|
|
start = len(data_payload)
|
|
data = entry["data"]
|
|
data_payload.extend(data)
|
|
serialized_entries.append(
|
|
{
|
|
"name": entry["name"],
|
|
"start": start,
|
|
"length": len(data),
|
|
"last_modified": entry.get("last_modified", 0),
|
|
}
|
|
)
|
|
|
|
header_offset = len(data_payload)
|
|
out_payload = bytearray(data_payload)
|
|
write_u32(out_payload, 0, header_offset, endian)
|
|
write_u32(out_payload, 4, len(serialized_entries), endian)
|
|
out_payload[8:10] = int(original_version).to_bytes(2, endian, signed=False)
|
|
out_payload[10:12] = int(current_version).to_bytes(2, endian, signed=False)
|
|
|
|
text_encoding = "utf-16-le" if endian == "little" else "utf-16-be"
|
|
header_table = bytearray()
|
|
for entry in serialized_entries:
|
|
record = bytearray(FILE_ENTRY_SIZE)
|
|
name_bytes = entry["name"].encode(text_encoding, errors="ignore")
|
|
name_bytes = name_bytes[:128]
|
|
name_bytes = name_bytes + (b"\x00" * (128 - len(name_bytes)))
|
|
record[0:128] = name_bytes
|
|
write_u32(record, 128, entry["length"], endian)
|
|
write_u32(record, 132, entry["start"], endian)
|
|
write_u64(record, 136, entry["last_modified"], endian)
|
|
header_table.extend(record)
|
|
|
|
out_payload.extend(header_table)
|
|
return bytes(out_payload)
|
|
|
|
|
|
def toggle_region_format_16(input_path, output_path, source_endian="auto"):
|
|
with open(input_path, "rb") as f:
|
|
blob = f.read()
|
|
|
|
if len(blob) < 12:
|
|
raise ValueError("Input file is too small to be a valid save file")
|
|
|
|
compressed_payload = blob[8:]
|
|
payload = zlib.decompress(compressed_payload)
|
|
|
|
endian = source_endian
|
|
if endian == "auto":
|
|
endian = detect_payload_endian(payload)
|
|
if endian is None:
|
|
raise ValueError("Could not detect payload endianness!")
|
|
|
|
parsed = parse_entries(payload, endian)
|
|
|
|
has_marker = any(e["name"] == REGION_FORMAT_16_NAME for e in parsed["entries"])
|
|
|
|
entries_out = []
|
|
for e in parsed["entries"]:
|
|
if e["name"] == REGION_FORMAT_16_NAME:
|
|
continue
|
|
entries_out.append(
|
|
{
|
|
"name": e["name"],
|
|
"data": payload[e["start"]:e["start"] + e["length"]],
|
|
"last_modified": e["last_modified"],
|
|
}
|
|
)
|
|
|
|
if has_marker:
|
|
action = "removed"
|
|
else:
|
|
action = "added"
|
|
entries_out.append(
|
|
{
|
|
"name": REGION_FORMAT_16_NAME,
|
|
"data": b"",
|
|
"last_modified": 0,
|
|
}
|
|
)
|
|
|
|
out_payload = serialize_payload_from_entries(
|
|
entries_out, parsed["original_version"], parsed["current_version"], endian
|
|
)
|
|
|
|
out_blob = bytearray()
|
|
out_blob.extend(b"\x00\x00\x00\x00")
|
|
out_blob.extend(len(out_payload).to_bytes(4, endian, signed=False))
|
|
out_blob.extend(zlib.compress(out_payload, level=9))
|
|
|
|
out_dir = os.path.dirname(output_path)
|
|
if out_dir:
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
with open(output_path, "wb") as f:
|
|
f.write(out_blob)
|
|
|
|
return {
|
|
"action": action,
|
|
"endian": endian,
|
|
"entry_count": len(entries_out),
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Toggles the region_format_16 marker entry in any given mcs or ms save"
|
|
)
|
|
parser.add_argument("input", help="Input .mcs/.ms path")
|
|
parser.add_argument(
|
|
"output",
|
|
nargs="?",
|
|
help="Output path (overwrites the input file by default)",
|
|
)
|
|
parser.add_argument(
|
|
"--source-endian",
|
|
choices=["auto", "big", "little"],
|
|
default="auto",
|
|
help="Override the input source's endianness",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
output_path = args.output or args.input
|
|
|
|
try:
|
|
result = toggle_region_format_16(
|
|
input_path=args.input,
|
|
output_path=output_path,
|
|
source_endian=args.source_endian,
|
|
)
|
|
except Exception as exc:
|
|
print(f"Error: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"region_format_16 {result['action']} successfully!")
|
|
print(f" Endian: {result['endian']}")
|
|
print(f" Entry Count: {result['entry_count']}")
|
|
print(f" Output Path: {output_path}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|