mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
111 lines
3.8 KiB
Python
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)
|