Files
Radicade/Tools/sep-literals.py
T
2026-10-16 23:33:13 -07:00

111 lines
3.8 KiB
Python

#!/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 <needle> [-i] [--limit N]
python tools/sep-literals.py around <index> [radius]
python tools/sep-literals.py token <index>
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("<Ii", blob, 0)
if magic != 0xFAB11BAF:
raise SystemExit(
f"not a plain il2cpp metadata file (magic {magic:#x}); "
"point RADICADE_METADATA at global-metadata.interop.dat"
)
table, table_bytes, data, data_bytes = struct.unpack_from("<iiii", blob, 8)
count = table_bytes // 8
if table_bytes % 8 or table < 24 or table + table_bytes > len(blob):
raise SystemExit(f"implausible literal table ({table}, {table_bytes})")
literals = []
for index in range(count):
length, offset = struct.unpack_from("<ii", blob, table + 8 * index)
if length < 0 or offset < 0 or offset + length > 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)