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:
Ludovic Marie
2026-10-11 14:08:47 +02:00
parent 925e4d0f5f
commit 24a58c40d8
3 changed files with 133 additions and 0 deletions
+14
View File
@@ -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
View File
@@ -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"
}
}
+110
View File
@@ -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())