Reverse-engineered the Triumph Keihin map format from the TuneECU loader (l.java zc/sc/Nc) and data classes c/r/s/t.java, and turned the viewer's table tab into a working editor. extract_mapdefs.py pulls the table directory out of the decompiled app into mapdefs.json: r.a/s.a/t.a (calibration directory, 8-int records keyed by the map's offset-20 signature) -> c.a (48-int calibration metadata) -> c.b (32-int groups = 16 offset/length pairs each), yielding 413 candidate table offsets. FORMAT.md documents the header magic (0x18008060 masked), the directory chain, and the write-back checksum. The viewer now edits: pick a known table offset (or set it manually), toggle edit mode, click a cell to change its value, and the bytes are rewritten with the running 16-bit checksum patched by (old - new) exactly as TuneECU does, then Download the modified copy. Verified: editing 2016->9999 with the checksum word at 0 yields 57553 = (0 + 2016 - 9999) & 0xffff. Geometry offsets are read-confident but not yet validated against a real map binary; FORMAT.md flags this. Editing/checksum stay local to a downloaded copy; the flash write path remains out of the read-only tunie tool.
94 lines
3.6 KiB
Python
94 lines
3.6 KiB
Python
"""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())
|