"""capture_session.py — persistent passive logger. Continuously accumulates every
decrypted outgoing frame (via the opcodelog hook) for a long window while you grind
normally, writes them to capture_session.jsonl, and prints a full analysis at the end.

No timing pressure: just play. Your repeating cycle (select->attack->move) makes the
attack command and move frames easy to separate across many cycles.

Run: python capture_session.py [seconds]   (default 360 = 6 min)
"""
import sys, time, json, struct, urllib.request
from collections import OrderedDict, Counter
from playwright.sync_api import sync_playwright

def port():
    for p in range(9222, 9251):
        try:
            urllib.request.urlopen(f"http://127.0.0.1:{p}/json/version", timeout=0.3); return p
        except Exception:
            pass
    return None

def f32(d, o):
    try: return struct.unpack_from("<f", bytes(d), o)[0]
    except Exception: return None

def u32(d, o):
    return (d[o] | (d[o+1] << 8) | (d[o+2] << 16) | (d[o+3] << 24)) & 0xffffffff if len(d) >= o+4 else None

def interp(d):
    """interpret body[4:] fields: float (position) or int/id."""
    parts = []
    for o in range(4, min(len(d), 20), 4):
        v = u32(d, o); fv = f32(d, o)
        if fv is not None and 50 < abs(fv) < 30000:
            parts.append(f"f32@{o}={fv:.1f}")
        elif v is not None and v != 0:
            parts.append(f"u32@{o}={v}(^k=0x{v ^ 0x77b2fc:x})")
    return " ".join(parts)

def main():
    secs = float(sys.argv[1]) if len(sys.argv) > 1 else 360.0
    p = port()
    if p is None:
        print("no game"); return
    with sync_playwright() as pw:
        pg = pw.chromium.connect_over_cdp(f"http://127.0.0.1:{p}").contexts[0].pages[0]
        if not pg.evaluate("!!window.__opHook"):
            print("hook not active — run: python opcodelog.py reload  (then re-enter game)"); return
        pg.evaluate("window.__opLog=[]")
        frames = OrderedDict()
        print(f"LOGGING for {secs:.0f}s — grind normally (select->attack->move, repeat). Progress every 20s:")
        t0 = time.time(); lastp = 0
        while time.time() - t0 < secs:
            for e in pg.evaluate("window.__opLog||[]"):
                k = (e["t"], e["op"], e["n"])
                if k not in frames: frames[k] = e
            el = time.time() - t0
            if el - lastp >= 20:
                lastp = el
                print(f"  [{el:4.0f}s] {len(frames)} frames")
            time.sleep(0.25)
        fl = sorted(frames.values(), key=lambda e: e["t"])
        with open("capture_session.jsonl", "w") as f:
            for e in fl: f.write(json.dumps(e) + "\n")
        base = fl[0]["t"] if fl else 0
        print(f"\n=== captured {len(fl)} frames -> capture_session.jsonl ===")
        print("frame-type counts (frameBytes):", dict(Counter(e["n"] for e in fl)))
        print("\n=== full timeline of NON-heartbeat frames (op!=256) ===")
        for e in fl:
            if e["op"] == 256: continue
            d = e["body"]; seq = (u32(d, 0) ^ 0x64000000) if u32(d, 0) else None
            print(f"  +{(e['t']-base)/1000.0:6.1f}s  {e['n']:4d}B seq={seq}  {interp(d)}")

if __name__ == "__main__":
    main()
