re_match: byte-match functions with the game's compiler (Visual C++ 2003)

Runs R:/Workspace/Tools/re/match71.py for up to 50 addresses from main or
an agent's worktree; configured by matcher/match_flags in [reverse].
This commit is contained in:
KeelrLeek committed 2026-10-05 13:10:01 -05:00
1 parent 584fabeb74
commit c7e678eacb
5 files changed
+78 -3

No files matched your search

+10 -3
View File
@@ -255,9 +255,10 @@ def build_parser():
p = sub.add_parser("who", help="who has worked on a scope (notes, reviews, claims, messages)")
p.add_argument("scope")
p = sub.add_parser("re", help="reverse-engineering lookups from the project's image and annotations")
p.add_argument("action", choices=["disasm", "xrefs", "bytes", "lookup", "ghidra", "ghidra-tools", "debugger-tools"])
p.add_argument("action", choices=["disasm", "xrefs", "bytes", "lookup", "ghidra", "ghidra-tools", "debugger-tools", "match"])
p.add_argument("target", nargs="?", default="", help="an address (0x4AD850) or a name; for ghidra, the tool")
p.add_argument("arguments", nargs="?", default="{}", help="ghidra: the tool's arguments as JSON")
p.add_argument("--decomp", help="match: the checkout or worktree to compile from")
p.add_argument("--max-bytes", type=int, dest="max_bytes")
p.add_argument("--count", type=int, default=16)
p.add_argument("--as", dest="as_type", default="hex", choices=["hex", "int32", "uint32", "float", "double", "string"])
@@ -675,8 +676,14 @@ def main(argv=None):
out(r, f"summary #{r['id']} pinned to {r['thread']}")
elif cmd == "re":
from .reverse import Ghidra, Reverse
r = None if args.action.startswith(("ghidra", "debugger")) else Reverse(harbor.project.reverse)
if args.action == "debugger-tools":
r = None if args.action.startswith(("ghidra", "debugger", "match")) else Reverse(harbor.project.reverse)
if args.action == "match":
from .reverse import match_functions
res = match_functions(harbor.project.reverse, args.target, args.decomp)
out(res, res["summary"] + "\n" + "\n".join(
f"{x['address']} {x['status']:<9} {x.get('similarity', '')!s:<6} {x['file']} {x.get('note', '')}"
for x in res["results"]))
elif args.action == "debugger-tools":
from .reverse import Debugger
res = Debugger(harbor.project.reverse).tools()
out(res, "\n".join(f"{t['name']:<22} {(t['description'] or '').splitlines()[0][:90] if t['description'] else ''}"
+15
View File
@@ -503,6 +503,21 @@ def _gh(fn):
raise ToolError(str(error)) from None
@mcp.tool()
def re_match(targets: str, decomp: str | None = None) -> dict:
"""Compile functions with the game's own compiler (Visual C++ 2003, cl 13.10) and compare them with the
original bytes. targets: up to 50 addresses, e.g. '0x412F70, 0x4ADC70'; decomp: your worktree (default main).
status 'match' means byte-identical apart from linker-filled addresses; 'arguments in registers' means the
game passed them in registers (link-time code generation) and the rest is identical. similarity (0-1)
compares instructions, so you can see whether a rewrite moved closer. Matching is a bonus, not a gate:
readable, correct C comes first."""
try:
from .reverse import match_functions
return match_functions(harbor().project.reverse, targets, decomp)
except (HarborError, projects.ProjectError) as error:
raise ToolError(str(error)) from None
@mcp.tool()
def ghidra_tools() -> list:
"""The tools of the team's shared Ghidra session (GhidrAssistMCP, read-only, the curated project with every
+29
View File
@@ -452,3 +452,32 @@ class Debugger:
pass
self.proc.terminate()
self.proc = None
def match_functions(config, targets, decomp=None, flags=None):
"""Compile functions with the game's compiler (Visual C++ 2003) and compare them with the original bytes.
targets: addresses ("0x4ADC70, 0x412F70"); decomp: the checkout or worktree whose src/ holds them."""
import subprocess
import sys
import tempfile
config = config or {}
matcher = config.get("matcher")
if not matcher:
raise HarborError("this project has no matcher in its [reverse] table")
wanted = [t.strip() for t in re.split(r"[,\s]+", str(targets)) if t.strip()]
if not wanted or len(wanted) > 50:
raise HarborError("give 1 to 50 addresses")
with tempfile.TemporaryDirectory(dir=config.get("debugger_temp")) as tmp:
out = Path(tmp) / "match.json"
command = [sys.executable, "-B", str(matcher), "--all", "--only", ",".join(wanted), "--json", str(out),
"--flags", flags or config.get("match_flags", "-TC -O2")]
if decomp:
command += ["--decomp", str(decomp)]
result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=600, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
if not out.exists():
raise HarborError("the matcher failed: " + (result.stdout + result.stderr).strip()[-800:])
rows = json.loads(out.read_text(encoding="utf-8"))
errors = [line for line in result.stdout.splitlines() if "compile errors" in line]
return {"results": rows, "summary": result.stdout.strip().splitlines()[0] if result.stdout.strip() else "",
**({"compile": errors[0]} if errors else {})}