"""
flyff_send.py — send commands to the game by REUSING its own WASM send path (no clicks).
Proven: attack a chosen mob by writing its id to controller+2656 and calling wasmTable.get(1707)
(func 4226 = "attack current target"); the game serializes+encrypts+sends a real 22B attack frame.

  python flyff_send.py mobs              # list valid attackable mobs (id + position + distance)
  python flyff_send.py attack            # attack the nearest valid mob (and leave it engaged)
  python flyff_send.py attack <lo> <hi>  # attack a specific mob id (decimal)

Tools: needs the game running. The opcode hook (opcodelog.py) is optional but lets us verify sends.
"""
import sys, time, urllib.request, math
from playwright.sync_api import sync_playwright

CTRL_PTR = 1230588      # *(this) = controller
TARGET_OFF = 2656       # controller+2656 = selected target i64 (lo @+2656, hi @+2660)
ATTACK_SLOT = 1707      # wasmTable slot for func 4226 (attack current target)
REG_MAP, REG_CAP = 362748, 362752

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 page():
    p = port()
    if p is None:
        print("no game on 9222-9250"); sys.exit(1)
    pw = sync_playwright().start()
    return pw.chromium.connect_over_cdp(f"http://127.0.0.1:{p}").contexts[0].pages[0]

# returns valid mobs: [{lo,hi,ent,x,y,z}], plus player pos
LIST_JS = """()=>{
  const dv=new DataView(HEAPU8.buffer); const ok=p=>p>0x10000&&p<0x10000000-512;
  const map=dv.getUint32(362748,true), cap=dv.getUint32(362752,true);
  const ctrl=dv.getUint32(1230588,true);
  const f=(a,o)=>dv.getFloat32(a+o,true);
  const mobs=[]; const seen={};
  for(let b=0;b<cap;b++){ let node=dv.getUint32(map+b*4,true),g=0;
    while(ok(node)&&g++<30){ const lo=dv.getUint32(node+8,true),hi=dv.getUint32(node+12,true),ent=dv.getUint32(node+16,true);
      const key=hi+'_'+lo;
      if(ok(ent)&&!seen[key]){ seen[key]=1;
        const st=dv.getInt32(ent+204,true), alive=dv.getUint32(ent+192,true), dying=dv.getInt32(ent+220,true);
        if(st===5 && alive!==0 && dying===0){ mobs.push({lo,hi,ent, x:f(ent,72), y:f(ent,76), z:f(ent,80)}); } }
      node=dv.getUint32(node,true);} }
  return {mobs, ctrl};
}"""

def main():
    pg = page()
    cmd = sys.argv[1] if len(sys.argv) > 1 else "mobs"
    data = pg.evaluate(LIST_JS)
    mobs = data["mobs"]
    if cmd == "mobs":
        print(f"{len(mobs)} valid attackable entities (controller=0x{data['ctrl']:x}):")
        for m in mobs[:40]:
            print(f"  id=0x{m['hi']:x}_{m['lo']:x}  pos=({m['x']:.0f},{m['y']:.0f},{m['z']:.0f})  ent=0x{m['ent']:x}")
        return
    if cmd == "attack":
        if len(sys.argv) >= 4:
            lo, hi = int(sys.argv[2]), int(sys.argv[3])
        else:
            if not mobs:
                print("no valid mobs found"); return
            # nearest to the bulk centroid as a rough 'near me' proxy (player pos TBD); just pick first for now
            m = mobs[0]; lo, hi = m["lo"], m["hi"]
            print(f"attacking mob id=0x{hi:x}_{lo:x} pos=({m['x']:.0f},{m['y']:.0f},{m['z']:.0f})")
        pg.evaluate("window.__opLog && (window.__opLog=[])")
        err = pg.evaluate("""(m)=>{ const dv=new DataView(HEAPU8.buffer); const c=dv.getUint32(1230588,true);
            dv.setUint32(c+2656,m[0],true); dv.setUint32(c+2660,m[1],true);
            try{ wasmTable.get(1707)(); return null; }catch(e){ return String(e);} }""", [lo, hi])
        print("call error:", err)
        time.sleep(0.6)
        log = pg.evaluate("window.__opLog||[]")
        atk = [e for e in log if e["n"] == 22]
        if atk:
            d = atk[-1]["body"]; tlo = (d[4]|(d[5]<<8)|(d[6]<<16)|(d[7]<<24)) & 0xffffffff
            thi = (d[8]|(d[9]<<8)|(d[10]<<16)|(d[11]<<24)) & 0xffffffff
            print(f"  -> ATTACK frame sent, target=0x{thi:x}_{tlo:x}  {'(MATCH)' if (tlo==lo and thi==hi) else '(mismatch!)'}")
        else:
            print("  (no 22B attack frame seen in hook — hook may be off; the call may still have worked)")
        print("target left engaged. Watch the character — it should walk to and attack the mob.")
        return

if __name__ == "__main__":
    main()
