"""
opcodelog.py — hook WebSocket.send and DECRYPT the opcode of every outgoing packet,
using the cipher we reverse-engineered. This proves our packet understanding and
identifies which opcode = select / attack / etc. (do the action in-game, watch the log).

Frame (from RE): [0]=0x1f marker, [1-4]=u32 len, [5-8]=u32 CRC-32, then body XOR-encrypted
with the 32-byte keystream @mem 338112 (byte i uses key[(i+1)&31]). Body[0..3] = opcode
(message type id, e.g. move=1054). 187 writes opcode raw when >1023 (all our opcodes are).

Usage (game running):
  python opcodelog.py inject     # install the hook (persists in-page)
  python opcodelog.py watch [s]  # stream decrypted opcodes for [s] seconds (default 60)
  python opcodelog.py off        # remove the hook
"""
import sys, time, json, os, urllib.request
from playwright.sync_api import sync_playwright

HOOK_JS = r"""(()=>{
  window.__opHook = true;
  window.__opLog = [];
  const KS = 338112;
  const orig = window.__origSend || WebSocket.prototype.send;
  window.__origSend = orig;
  WebSocket.prototype.send = function(data){
    try {
      let b = null;
      if (data instanceof ArrayBuffer) b = new Uint8Array(data);
      else if (data && data.buffer instanceof ArrayBuffer) b = new Uint8Array(data.buffer, data.byteOffset||0, data.byteLength);
      if (b && b.length >= 13 && b[0] === 0x1f) {
        const H = HEAPU8;
        const len = (b[1]|(b[2]<<8)|(b[3]<<16)|(b[4]<<24))>>>0;
        const d = [];                                  // decrypt body[0..15] with keystream snapshot NOW
        for (let i=0;i<16 && 9+i<b.length;i++) d.push(b[9+i] ^ H[KS + ((i+1)&31)]);
        const opcode = (d[0]|(d[1]<<8)|(d[2]<<16)|(d[3]<<24))>>>0;
        window.__opLog.push({op:opcode, len:len, n:b.length, body:d, t:Date.now()});
        if (window.__opLog.length>1000) window.__opLog.shift();
      }
    } catch(e){}
    return orig.apply(this, arguments);
  };
  return 'hooked';
})()"""

INIT_HOOK_JS = HOOK_JS  # same hook, installed via add_init_script before the game caches send

OFF_JS = "(()=>{window.__opHook=false; window.__opLog=[];})()"  # leaves wrapper but stops logging via flag check? simpler: just clear

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 load_catalog():
    """opcode -> serializer family label, from command_vtables.json if present."""
    lab = {1054: "MOVE", 1044: "STOP/interact"}
    return lab

def main():
    p = port()
    if p is None:
        print("No live game on 9222-9250."); return
    cmd = sys.argv[1] if len(sys.argv) > 1 else "watch"
    with sync_playwright() as pw:
        page = pw.chromium.connect_over_cdp(f"http://127.0.0.1:{p}").contexts[0].pages[0]
        if cmd == "reload":
            page.add_init_script(HOOK_JS)
            print("init-script hook registered; reloading the game so it's in place before embind caches send...")
            page.reload(wait_until="domcontentloaded")
            print("reloaded. Wait for the game to reconnect/log in, get near mobs, then run: python opcodelog.py watch")
        elif cmd == "inject":
            print("hook:", page.evaluate(HOOK_JS))
            print("Now run: python opcodelog.py watch   and do actions in-game.")
        elif cmd == "off":
            page.evaluate("(()=>{const o=window.__opLog;window.__opLog=[];})()")
            print("log cleared (hook wrapper stays until reload).")
        else:
            secs = float(sys.argv[2]) if len(sys.argv) > 2 else 60.0
            if not page.evaluate("!!window.__opHook"):
                print("hook:", page.evaluate(HOOK_JS))
            lab = load_catalog()
            seen = 0
            print(f"streaming decrypted opcodes for {secs}s — DO ACTIONS IN-GAME (move, select a mob, attack)...")
            t0 = time.time()
            from collections import Counter
            counts = Counter()
            while time.time() - t0 < secs:
                log = page.evaluate("window.__opLog")
                for e in log[seen:]:
                    op = e["op"]; counts[op] += 1
                    tag = f"  <- {lab[op]}" if op in lab else ""
                    print(f"  op={op:<7} (0x{op:x})  bodyLen={e['len']} frame={e['n']}B{tag}")
                seen = len(log)
                time.sleep(0.2)
            print("\nopcode frequency this session:")
            for op, c in counts.most_common():
                tag = f"  <- {lab[op]}" if op in lab else ""
                print(f"  op={op:<7} x{c}{tag}")

if __name__ == "__main__":
    main()
