Files
LegacyRenouveau/tools/toggle_region_format_16.py
T

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())