Query the shipped vanilla bytecode through REA
tools/rea_vanilla.py runs REA's JADX provider over the pinned client and server JARs: class search, class members, method bodies and callers (including lambdas). REA and the JADX engine are pinned in research-lock.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,3 +21,17 @@ The full outputs and machine-readable reports are local and gitignored:
|
||||
```
|
||||
|
||||
Recheck with `python3 tools/lab.py status`. File coverage and the absence of detected failure markers do not prove every recovered method behaves identically, or that this source can be rebuilt. Original bytecode remains the behavior reference. There has been no gameplay baseline, full Rust port, or measured real-game optimization yet.
|
||||
|
||||
## Querying the shipped bytecode with REA
|
||||
|
||||
`tools/rea_vanilla.py` answers questions about the pinned game JARs with [REA](https://github.com/morluto/rea), which drives a headless JADX over the bytecode itself. Use it next to the Vineflower sources when you need what a text search can't give reliably: every caller of a method, including lambdas and bridge methods; every overload; or a second decompiler's view of a method whose Vineflower output looks wrong.
|
||||
|
||||
```sh
|
||||
python3 tools/rea_vanilla.py classes DispenserBlock
|
||||
python3 tools/rea_vanilla.py class net.minecraft.world.level.block.DispenserBlock
|
||||
python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom
|
||||
python3 tools/rea_vanilla.py callers net.minecraft.server.level.ServerLevel tickChunk
|
||||
python3 tools/rea_vanilla.py --jar server-game classes DedicatedServer
|
||||
```
|
||||
|
||||
The REA and JADX engine versions are pinned in `research-lock.json`; the first run downloads the engine into `.local/tools/`. REA only admits JADX targets named `.apk`, so the helper keeps an `.apk`-named copy of each JAR in `.local/rea/`. Each query loads the JAR again, which takes about five seconds. Like the Vineflower output, decompiled method bodies are a reading aid: the bytecode and the oracle harnesses remain the behavior reference.
|
||||
|
||||
@@ -9,5 +9,14 @@
|
||||
"version": "1.12.0",
|
||||
"url": "https://github.com/Vineflower/vineflower/releases/download/1.12.0/vineflower-1.12.0.jar",
|
||||
"sha256": "1dfcfe974395734fa467ce620661c7623d05ba83670de0529b1fbd63ff548b9d"
|
||||
},
|
||||
"rea": {
|
||||
"package": "rea-agents",
|
||||
"version": "6.4.0"
|
||||
},
|
||||
"jadx_headless_mcp": {
|
||||
"version": "0.7.1",
|
||||
"url": "https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar",
|
||||
"sha256": "6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Query the pinned vanilla bytecode through REA (Reverse Engineer Anything).
|
||||
|
||||
REA drives a headless JADX over the shipped game JAR, so answers come from the
|
||||
bytecode Mojang ships rather than from the Vineflower sources: callers through
|
||||
lambdas and bridges, every overload of a method, and members the decompiled
|
||||
sources fold away. Prints REA's normalized result as JSON.
|
||||
|
||||
python3 tools/rea_vanilla.py classes DispenserBlock
|
||||
python3 tools/rea_vanilla.py class net.minecraft.world.level.block.DispenserBlock
|
||||
python3 tools/rea_vanilla.py method net.minecraft.world.level.block.DispenserBlock dispenseFrom
|
||||
python3 tools/rea_vanilla.py callers net.minecraft.server.level.ServerLevel tickChunk
|
||||
python3 tools/rea_vanilla.py --jar server-game classes DedicatedServer
|
||||
|
||||
The first run downloads the pinned JADX engine into .local/tools/; REA itself
|
||||
runs from the global `rea` command when it is the pinned version, otherwise
|
||||
through npx. Needs Node.js 22.19+ and Java 17+.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import lab
|
||||
|
||||
COMMANDS = {
|
||||
"classes": ("search-android-classes", ["query"]),
|
||||
"class": ("inspect-android-class", ["class_name"]),
|
||||
"method": ("inspect-android-method", ["class_name", "method_name"]),
|
||||
"callers": ("trace-android-references", ["class_name", "method_name"]),
|
||||
"refs": ("trace-android-references", ["class_name"]),
|
||||
}
|
||||
|
||||
|
||||
def engine(lock):
|
||||
pin = lock["jadx_headless_mcp"]
|
||||
jar = lab.LOCAL / "tools" / f"jadx-headless-mcp-{pin['version']}-all.jar"
|
||||
return lab.download(pin["url"], jar, pin["sha256"], "sha256")
|
||||
|
||||
|
||||
def target(game_dir, name):
|
||||
"""REA admits JADX targets by their .apk name; JADX reads the class files either way."""
|
||||
source = game_dir / "jars" / f"{name}.jar"
|
||||
if not source.exists():
|
||||
raise SystemExit(f"{source} is missing; run python3 tools/prepare_native_client.py --prepare")
|
||||
copy = lab.LOCAL / "rea" / f"{name}-{lab.digest(source, 'sha256')[:16]}.apk"
|
||||
if not copy.exists():
|
||||
copy.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, copy)
|
||||
return copy
|
||||
|
||||
|
||||
def rea_command(lock):
|
||||
pin = lock["rea"]
|
||||
installed = shutil.which("rea")
|
||||
if installed:
|
||||
version = subprocess.run([installed, "--version"], capture_output=True, text=True).stdout.strip()
|
||||
if version == pin["version"]:
|
||||
return [installed]
|
||||
return ["npx", "-y", f"{pin['package']}@{pin['version']}"]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("--jar", default="client", choices=["client", "server-game"])
|
||||
parser.add_argument("--overload-index", type=int)
|
||||
parser.add_argument("--raw", action="store_true", help="print REA's whole evidence record")
|
||||
parser.add_argument("command", choices=COMMANDS)
|
||||
parser.add_argument("arguments", nargs="+")
|
||||
args = parser.parse_args()
|
||||
subcommand, names = COMMANDS[args.command]
|
||||
if len(args.arguments) != len(names):
|
||||
parser.error(f"{args.command} takes {' '.join(names).upper()}")
|
||||
lock, game_dir = lab.config()
|
||||
command = rea_command(lock) + [subcommand, str(target(game_dir, args.jar))]
|
||||
if args.command in ("callers", "refs"):
|
||||
command += [args.arguments[0]] + (["--method-name", args.arguments[1]] if args.command == "callers" else [])
|
||||
else:
|
||||
command += args.arguments
|
||||
if args.overload_index is not None:
|
||||
command += ["--overload-index", str(args.overload_index)]
|
||||
environment = dict(os.environ, REA_JADX_MCP_JAR=str(engine(lock)))
|
||||
result = subprocess.run(command + ["--json"], capture_output=True, text=True, env=environment)
|
||||
try:
|
||||
record = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
sys.stderr.write(result.stdout + result.stderr)
|
||||
return result.returncode or 1
|
||||
if "normalized_result" not in record:
|
||||
details = record.get("details", {})
|
||||
reasons = [issue["message"] for issue in details.get("issues", [])] or [details.get("reason")]
|
||||
print(record.get("message"), *filter(None, reasons), sep="\n", file=sys.stderr)
|
||||
return 1
|
||||
body = record if args.raw else record["normalized_result"]
|
||||
if not args.raw:
|
||||
body.pop("engine", None)
|
||||
if args.command == "method" and not args.raw and body.get("source", {}).get("text"):
|
||||
print(f"// {body['class_name']}.{body['method']['reported_signature']}"
|
||||
f" (overload {body['method']['overload_index']} of {body['overload_count']}, {body['source']['status']})")
|
||||
print(body["source"]["text"])
|
||||
else:
|
||||
print(json.dumps(body, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user