#!/usr/bin/env python3 """Inspect the il2cpp string-literal table of the 20230831 client. global-metadata.dat ships lightly obfuscated (header magic is not 0xFAB11BAF), but the interop copy BepInEx writes (global-metadata.interop.dat) is plain v27, which is the file the plugin itself reads. String literals live at header offsets 8..24: int32 stringLiteralOffset // table of (length, dataOffset) pairs int32 stringLiteralCount // BYTES in that table (8 per literal) int32 stringLiteralDataOffset // concatenated utf8 bytes int32 stringLiteralDataCount // BYTES of literal data Usage: python tools/sep-literals.py find [-i] [--limit N] python tools/sep-literals.py around [radius] python tools/sep-literals.py token Environment: RADICADE_METADATA override the metadata path """ import os import re import struct import sys DEFAULT_METADATA = ( r"C:\RecRoom\2023-09-06T06-53-47Z\RecRoom_Data\il2cpp_data" r"\Metadata\global-metadata.interop.dat" ) def load(path): with open(path, "rb") as handle: blob = handle.read() magic, version = struct.unpack_from(" len(blob): raise SystemExit(f"implausible literal table ({table}, {table_bytes})") literals = [] for index in range(count): length, offset = struct.unpack_from(" data_bytes: raise SystemExit(f"literal #{index} escapes the data segment") literals.append(blob[data + offset : data + offset + length].decode("utf-8", "replace")) return version, literals def main(argv): if len(argv) < 2: raise SystemExit(__doc__) path = os.environ.get("RADICADE_METADATA", DEFAULT_METADATA) version, literals = load(path) command, rest = argv[1], argv[2:] if command == "find": if not rest: raise SystemExit("find needs a needle") ignore_case = "-i" in rest limit = None positional = [] index = 0 while index < len(rest): if rest[index] == "--limit": limit = int(rest[index + 1]) index += 2 continue if rest[index] != "-i": positional.append(rest[index]) index += 1 if not positional: raise SystemExit("find needs a needle") needle = positional[0] target = needle.lower() if ignore_case else needle hits = 0 for index, text in enumerate(literals): haystack = text.lower() if ignore_case else text if target not in haystack: continue print(f"#{index}\t{text!r}") hits += 1 if limit and hits >= limit: break print(f"-- {hits} match(es); {len(literals)} literals; metadata v{version}") elif command == "around": index = int(rest[0]) radius = int(rest[1]) if len(rest) > 1 else 25 for other in range(max(0, index - radius), min(len(literals), index + radius + 1)): mark = ">>" if other == index else " " print(f"{mark} #{other}\t{literals[other]!r}") elif command == "token": for raw in rest: index = int(raw) print(f"#{index}\t0x{(5 << 29) | (index << 1):X}\t{literals[index]!r}") else: raise SystemExit(__doc__) if __name__ == "__main__": main(sys.argv)