"""Extract the Triumph Keihin map-format tables from the decompiled TuneECU. These decompiled data classes are TuneECU's equivalent of a TunerPro XDF -- they describe where every table lives inside a map binary. Recovered from com/tuneecu/{c,r,s,t}.java and cross-read against the loader in l.java (zc/sc/Nc). Record strides were derived from how the loader indexes each array: r.a / s.a / t.a 8 ints per record calibration directory field[0] = 4-byte signature matched against map bytes [20..23] (see sc()) field[1] = index into c.a (the "Qd" calibration-metadata record) field[2] = %100 -> group index into c.b (table geometry); /100 -> flags field[3..7] = sizes / addresses / flags (not fully decoded) c.a 48 ints per record per-calibration metadata (memory size, region, checksum location; exact field map still being confirmed) c.b 32 ints per record = 16 (offset, length) pairs TABLE GEOMETRY. Each pair is (byte offset into the map, byte length of the table). Confirmed by the loader reading c.b in 32-int strides into Dd and then using Dd[i*2] / Dd[i*2+1] as (offset, size). NOTE: geometry decode is read-confident but UNVERIFIED against a real map file (we don't have one yet). Treat offsets as candidates until checked against a ROM dump. The extraction itself (the raw numbers) is exact. """ from __future__ import annotations import argparse import json import re from pathlib import Path STRIDES = {"c.a": 48, "c.b": 32, "r.a": 8, "s.a": 8, "t.a": 8} def _array(java: str, field: str) -> list[int]: m = re.search(rf'\b{field} = \{{(.*?)\}};', java, re.S) if not m: return [] out = [] for tok in m.group(1).split(','): tok = tok.strip().rstrip('L') if not tok: continue try: out.append(int(tok, 0)) except ValueError: out.append(0) # jadx resource-name corruption -> placeholder return out def _records(flat: list[int], stride: int) -> list[list[int]]: return [flat[i:i + stride] for i in range(0, len(flat) - stride + 1, stride)] def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--src", required=True, type=Path, help="decompiled com/tuneecu source dir (has c.java, r.java …)") ap.add_argument("--out", required=True, type=Path) args = ap.parse_args() data = {} for key, stride in STRIDES.items(): cls, field = key.split(".") flat = _array((args.src / f"{cls}.java").read_text(encoding="utf-8", errors="replace"), field) recs = _records(flat, stride) data[key] = {"stride": stride, "count": len(recs), "records": recs} # Derive candidate table geometry from c.b: 16 (offset,length) pairs / group, # dropping empty pairs. geometry = [] for gi, rec in enumerate(data["c.b"]["records"]): tables = [] for p in range(0, 32, 2): off, ln = rec[p], rec[p + 1] if off and ln: tables.append({"offset": off, "offset_hex": f"0x{off:X}", "length": ln}) geometry.append({"group": gi, "tables": tables}) data["geometry"] = geometry args.out.write_text(json.dumps(data, separators=(",", ":")), encoding="utf-8") tot = sum(len(g["tables"]) for g in geometry) print(f"wrote {args.out}: " f"c.a {data['c.a']['count']} recs, c.b {data['c.b']['count']} groups " f"({tot} candidate tables), r/s/t " f"{data['r.a']['count']}/{data['s.a']['count']}/{data['t.a']['count']} dir entries") return 0 if __name__ == "__main__": raise SystemExit(main())