"""Extract reference Excel/custom data offline; never execute the reference binary. Requires protobuf plus the sibling dump_embedded_proto.py. Output is evidence, not an automatically trusted replacement for current Client configuration. """ import argparse import hashlib import json import re import zlib from pathlib import Path from google.protobuf import descriptor_pool, json_format, message_factory from dump_embedded_proto import extract def sha(data): return hashlib.sha256(data).hexdigest() def save_json(path, value): path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--binary", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() binary = args.binary.read_bytes() args.output.mkdir(parents=True, exist_ok=True) pool = descriptor_pool.DescriptorPool() descriptors = [] for name in ("gridfight_excel", "rogue_tourn_excel", "resources"): start, end, desc = extract(binary, "proto/" + name + ".proto") pool.Add(desc) descriptors.append({"name": desc.name, "offset": start, "length": end - start, "sha256": sha(binary[start:end])}) excel_type = message_factory.GetMessageClass(pool.FindMessageTypeByName("excel.ExcelData")) manifest = {"format_version": 1, "binary_sha256": sha(binary), "binary_size": len(binary), "descriptors": descriptors, "resources": [], "limitations": [ "Reference binary data; no runtime execution or Client verification.", "Custom config files are reference-authored rules, not original Client JSON.", "int64 protobuf fields remain JSON strings to retain exact values."]} found = set() # Exact gzip headers, finite carrier size and 64 MiB output cap per candidate. for match in re.finditer(b"\x1f\x8b\x08\x00", binary): offset = match.start() try: decoder = zlib.decompressobj(31) raw = decoder.decompress(binary[offset:], 64 * 1024 * 1024) if not decoder.eof: continue compressed_length = len(binary) - offset - len(decoder.unused_data) if raw.startswith(b"{"): value = json.loads(raw) if not isinstance(value, dict) or "config_files" not in value: continue stem = "gridfight-custom-data" else: message = excel_type.FromString(raw) if not message.HasField("grid_fight"): continue value = json_format.MessageToDict(message, preserving_proto_field_name=True) stem = "excel-data" if stem in found: raise RuntimeError("Ambiguous embedded resource: " + stem) found.add(stem) save_json(args.output / (stem + ".json"), value) if stem == "excel-data": save_json(args.output / "gridfight-excel-data.json", value["grid_fight"]) manifest["resources"].append({"output": stem + ".json", "offset": offset, "compressed_length": compressed_length, "compressed_sha256": sha(binary[offset:offset+compressed_length]), "uncompressed_length": len(raw), "uncompressed_sha256": sha(raw)}) except (zlib.error, ValueError, UnicodeError): continue except Exception as error: # A random gzip-like sequence need not be a protobuf resource. if type(error).__name__ != "DecodeError": raise if found != {"excel-data", "gridfight-custom-data"}: raise RuntimeError("Expected unique Excel and CW custom resources; got " + repr(found)) # This separate JSON supplement is not part of ExcelData. Locate its named # root rather than baking in one build's offset or any game identifiers. supplements = [] for match in re.finditer(rb'\{\s*"challenge_peak_extra_data"\s*:', binary): candidate = binary[match.start():match.start()+1024*1024] try: # Decode only through the JSON terminator; following PE bytes are # unrelated and may not be valid UTF-8. text = candidate.decode("utf-8", errors="surrogateescape") value, end = json.JSONDecoder().raw_decode(text) raw = text[:end].encode("utf-8", errors="surrogateescape") if isinstance(value.get("challenge_peak_extra_data"), list): supplements.append((match.start(), raw, value)) except (ValueError, UnicodeError): continue if len(supplements) != 1: raise RuntimeError("Expected unique named Peak scene supplement") offset, raw, value = supplements[0] save_json(args.output / "challenge_peak_extra_data.json", { "binary_sha256": sha(binary), "source_file_offset": hex(offset), "decoded_byte_length": len(raw), "data": value}) manifest["resources"].append({"output": "challenge_peak_extra_data.json", "offset": offset, "uncompressed_length": len(raw), "uncompressed_sha256": sha(raw), "encoding": "utf-8-json"}) save_json(args.output / "resource-manifest.json", manifest) print(json.dumps({"binary_sha256": manifest["binary_sha256"], "resources": manifest["resources"]})) if __name__ == "__main__": main()