"""
memread.py — read the live player struct (and later the entity list) straight from
WASM memory, to build a reliable memory-read sensor (replaces vision in grind_local).

From the WASM RE: the player-object POINTER is at linear addr 1230588 (PLAYER_PTR_ADDR).
We read that pointer, then dump/inspect the player struct to locate HP / position / target.

Commands (game must be running on a debug port; you logged in + in-game):
  python memread.py ptr                 # show the player-object pointer + sanity
  python memread.py dump [nbytes]       # dump the player struct -> memdump.bin (+ summary)
  python memread.py find <value> [type] # offsets in the struct == value (type: i32 u16 f32 u8)
  python memread.py watch <off:type> ...# poll given offsets live (Ctrl+C to stop)
  python memread.py track <secs>        # poll struct N s; report offsets that CHANGED (min/max)
                                        #   -> run while you TAKE DAMAGE to pin HP
Types: i32 u32 i16 u16 f32 u8
"""
import sys, time, struct, urllib.request
from playwright.sync_api import sync_playwright

PLAYER_PTR_ADDR = 1230588
DUMP_BYTES = 9216

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 live game on ports 9222-9250 — launch flyffbot.bat / host.py and log in.")
        sys.exit(1)
    pw = sync_playwright().start()
    return pw.chromium.connect_over_cdp(f"http://127.0.0.1:{p}").contexts[0].pages[0]

def has_heap(pg):
    return pg.evaluate("typeof HEAPU8 !== 'undefined'")

def read_ptr(pg, addr):
    return pg.evaluate("(a)=>{const dv=new DataView(HEAPU8.buffer);return dv.getUint32(a,true);}", addr)

def read_bytes(pg, addr, n):
    return bytes(pg.evaluate("(a)=>Array.from(HEAPU8.subarray(a[0],a[0]+a[1]))", [addr, n]))

def player_base(pg):
    return read_ptr(pg, PLAYER_PTR_ADDR)

def as_vals(buf, off, t):
    try:
        if t == "i32": return struct.unpack_from("<i", buf, off)[0]
        if t == "u32": return struct.unpack_from("<I", buf, off)[0]
        if t == "i16": return struct.unpack_from("<h", buf, off)[0]
        if t == "u16": return struct.unpack_from("<H", buf, off)[0]
        if t == "u8":  return buf[off]
        if t == "f32": return struct.unpack_from("<f", buf, off)[0]
    except Exception:
        return None

SCAN_JS = """(a)=>{
  const val=a.value, t=a.type, buf=HEAPU8.buffer;
  let arr, sz;
  if(t==='i32'){arr=new Int32Array(buf);sz=4;}
  else if(t==='u16'){arr=new Uint16Array(buf);sz=2;}
  else if(t==='f32'){arr=new Float32Array(buf);sz=4;}
  else {arr=new Uint8Array(buf);sz=1;}
  const c=[]; for(let i=0;i<arr.length;i++){ if(arr[i]===val){ c.push(i*sz); if(c.length>300000)break; } }
  window.__cands=c; window.__ctype=t; window.__csz=sz; return c.length;
}"""
NARROW_JS = """(val)=>{
  const t=window.__ctype, sz=window.__csz, buf=HEAPU8.buffer;
  let arr; if(t==='i32')arr=new Int32Array(buf); else if(t==='u16')arr=new Uint16Array(buf);
  else if(t==='f32')arr=new Float32Array(buf); else arr=new Uint8Array(buf);
  const c=window.__cands.filter(o=>arr[o/sz]===val); window.__cands=c; return c.length;
}"""
READVALS_JS = """()=>{
  const t=window.__ctype, sz=window.__csz, buf=HEAPU8.buffer, c=window.__cands;
  let arr; if(t==='i32')arr=new Int32Array(buf); else if(t==='u16')arr=new Uint16Array(buf);
  else if(t==='f32')arr=new Float32Array(buf); else arr=new Uint8Array(buf);
  const out=new Array(c.length); for(let k=0;k<c.length;k++) out[k]=arr[c[k]/sz]; return out;
}"""

SCANALL_JS = """(v)=>{
  const buf=HEAPU8.buffer;
  const i32=new Int32Array(buf), u16=new Uint16Array(buf), f32=new Float32Array(buf), f64=new Float64Array(buf);
  const off=[], typ=[];
  for(let i=0;i<i32.length;i++){ if(i32[i]===v){off.push(i*4);typ.push(0);} }
  for(let i=0;i<f32.length;i++){ if(f32[i]===v){off.push(i*4);typ.push(2);} }
  for(let i=0;i<f64.length;i++){ if(f64[i]===v){off.push(i*8);typ.push(3);} }
  for(let i=0;i<u16.length;i++){ if(u16[i]===v){off.push(i*2);typ.push(1);} }
  window.__co=off; window.__ct=typ;
  return {i32: typ.filter(t=>t===0).length, f32: typ.filter(t=>t===2).length, f64: typ.filter(t=>t===3).length, u16: typ.filter(t=>t===1).length, total: off.length};
}"""
READVALS2_JS = """()=>{
  const buf=HEAPU8.buffer, u8=HEAPU8, off=window.__co, typ=window.__ct;
  const i32=new Int32Array(buf), u16=new Uint16Array(buf), f32=new Float32Array(buf), f64=new Float64Array(buf);
  const out=new Array(off.length);
  for(let k=0;k<off.length;k++){const t=typ[k],o=off[k];
    out[k]= t===0? i32[o/4] : t===2? f32[o/4] : t===3? f64[o/8] : t===1? u16[o/2]
          : t===4? (u8[o]|(u8[o+1]<<8))
          : ((u8[o]|(u8[o+1]<<8)|(u8[o+2]<<16)|(u8[o+3]<<24))>>>0);}
  return out;
}"""
SNAP_JS = "()=>{ window.__snap = new Uint8Array(HEAPU8); return window.__snap.length; }"
DIFF_JS = """(a)=>{
  const u8=HEAPU8, s=window.__snap;
  if(!s) return -1;
  const n=u8.length-3, off=[], typ=[];
  const oL=a.old-a.tol, oH=a.old+a.tol, nL=a.nw-a.tol, nH=a.nw+a.tol;
  for(let i=0;i<n;i++){
    const ov=s[i]|(s[i+1]<<8), nv=u8[i]|(u8[i+1]<<8);
    if(ov>=oL&&ov<=oH&&nv>=nL&&nv<=nH){off.push(i);typ.push(4);}
    const ov4=(s[i]|(s[i+1]<<8)|(s[i+2]<<16)|(s[i+3]<<24))>>>0;
    const nv4=(u8[i]|(u8[i+1]<<8)|(u8[i+2]<<16)|(u8[i+3]<<24))>>>0;
    if(ov4>=oL&&ov4<=oH&&nv4>=nL&&nv4<=nH){off.push(i);typ.push(5);}
    if(off.length>200000)break;
  }
  window.__co=off; window.__ct=typ; return off.length;
}"""
DIFFR_JS = """(a)=>{
  const R=a.ratio, tol=a.tol;
  const sb=window.__snap.buffer, cb=HEAPU8.buffer;
  if(!window.__snap) return -1;
  const off=[], typ=[];
  const sf=new Float32Array(sb), cf=new Float32Array(cb);
  for(let i=0;i<cf.length;i++){const o=sf[i],nw=cf[i];
    if(o>0.4 && o<1e7 && Math.abs(o-nw)>o*0.03 && o>nw && Math.abs(nw/o - R)<tol){off.push(i*4);typ.push(2);}
    if(off.length>120000)break;}
  const sd=new Float64Array(sb), cd=new Float64Array(cb);
  for(let i=0;i<cd.length;i++){const o=sd[i],nw=cd[i];
    if(o>0.4 && o<1e7 && Math.abs(o-nw)>o*0.03 && o>nw && Math.abs(nw/o - R)<tol){off.push(i*8);typ.push(3);}
    if(off.length>120000)break;}
  const si=new Int32Array(sb), ci=new Int32Array(cb);
  for(let i=0;i<ci.length;i++){const o=si[i],nw=ci[i];
    if(o>10 && o<1e8 && Math.abs(o-nw)>o*0.03 && o>nw && Math.abs(nw/o - R)<tol){off.push(i*4);typ.push(0);}
    if(off.length>60000)break;}
  window.__co=off; window.__ct=typ; return off.length;
}"""
NARROWR_JS = """(a)=>{
  const R=a.ratio, tol=a.tol, sb=window.__snap.buffer, cb=HEAPU8.buffer;
  const sf=new Float32Array(sb), cf=new Float32Array(cb);
  const sd=new Float64Array(sb), cd=new Float64Array(cb);
  const si=new Int32Array(sb), ci=new Int32Array(cb);
  const off=window.__co, typ=window.__ct, no=[], nt=[];
  for(let k=0;k<off.length;k++){const o=off[k],t=typ[k]; let ov,nv;
    if(t===2){ov=sf[o/4];nv=cf[o/4];} else if(t===3){ov=sd[o/8];nv=cd[o/8];} else {ov=si[o/4];nv=ci[o/4];}
    if(ov>0 && Math.abs(nv/ov - R)<tol){no.push(o);nt.push(t);}
  }
  window.__co=no; window.__ct=nt; return no.length;
}"""
SCANU_JS = """(v)=>{
  const u8=HEAPU8, n=u8.length-3, off=[], typ=[];
  for(let i=0;i<n;i++){
    if((u8[i]|(u8[i+1]<<8))===v){off.push(i);typ.push(4);}
    if(((u8[i]|(u8[i+1]<<8)|(u8[i+2]<<16)|(u8[i+3]<<24))>>>0)===v){off.push(i);typ.push(5);}
    if(off.length>800000)break;
  }
  window.__co=off; window.__ct=typ;
  return {u16u: typ.filter(t=>t===4).length, u32u: typ.filter(t=>t===5).length, total: off.length};
}"""
NARROW2_JS = """(v)=>{
  const buf=HEAPU8.buffer, u8=HEAPU8, off=window.__co, typ=window.__ct;
  const i32=new Int32Array(buf), u16=new Uint16Array(buf), f32=new Float32Array(buf), f64=new Float64Array(buf);
  const no=[], nt=[];
  for(let k=0;k<off.length;k++){const t=typ[k],o=off[k];
    let cur = t===0? i32[o/4] : t===2? f32[o/4] : t===3? f64[o/8] : t===1? u16[o/2]
            : t===4? (u8[o]|(u8[o+1]<<8))
            : ((u8[o]|(u8[o+1]<<8)|(u8[o+2]<<16)|(u8[o+3]<<24))>>>0);
    if(Math.abs(cur - v) < 0.5){ no.push(o); nt.push(t); } }
  window.__co=no; window.__ct=nt; return no.length;
}"""
TNAME = {0: "i32", 1: "u16", 2: "f32", 3: "f64", 4: "u16u", 5: "u32u"}

def main():
    if len(sys.argv) < 2:
        print(__doc__); return
    cmd = sys.argv[1]
    pg = page()
    if not has_heap(pg):
        print("HEAPU8 not found on the page — is the game fully loaded/in-game?"); return
    base = player_base(pg)
    plausible = 0x10000 < base < 0x10000000
    if cmd == "ptr":
        print(f"player ptr @ {PLAYER_PTR_ADDR} = {base} (0x{base:x})  {'OK' if plausible else 'SUSPECT (try 1230584 / not in-game?)'}")
        return

    # --- GLOBAL heap commands (no player base needed) ---
    if cmd == "gscan":
        val = sys.argv[2]; t = sys.argv[3] if len(sys.argv) > 3 else "i32"
        v = float(val) if t == "f32" else int(val)
        n = pg.evaluate(SCAN_JS, {"value": v, "type": t})
        print(f"gscan {val} as {t}: {n} candidate(s) kept in page (window.__cands).")
        return
    if cmd == "gnarrow":
        val = sys.argv[2]; t = pg.evaluate("window.__ctype")
        v = float(val) if t == "f32" else int(val)
        n = pg.evaluate(NARROW_JS, v)
        print(f"gnarrow {val}: {n} candidate(s) remain.")
        if n <= 12:
            cands = pg.evaluate("window.__cands")
            print("  offsets:", cands)
        return
    if cmd == "gscanall":
        v = int(sys.argv[2])
        r = pg.evaluate(SCANALL_JS, v)
        print(f"gscanall {v}: i32={r['i32']} f32={r['f32']} f64={r['f64']} u16={r['u16']}  total={r['total']} candidates (window.__co).")
        return
    if cmd == "gwatch":
        tc = {"i32":0,"u16":1,"f32":2,"f64":3,"u16u":4,"u32u":5}
        specs = [[int(s.split(":")[0]), tc[s.split(":")[1]]] for s in sys.argv[2:]]
        js = """(specs)=>{const u8=HEAPU8,buf=HEAPU8.buffer;
          const i32=new Int32Array(buf),u16=new Uint16Array(buf),f32=new Float32Array(buf),f64=new Float64Array(buf);
          return specs.map(s=>{const o=s[0],t=s[1];
            return t===0?i32[o/4]:t===1?u16[o/2]:t===2?f32[o/4]:t===3?f64[o/8]:t===4?(u8[o]|(u8[o+1]<<8)):((u8[o]|(u8[o+1]<<8)|(u8[o+2]<<16)|(u8[o+3]<<24))>>>0);});}"""
        print("gwatch (Ctrl+C to stop):", sys.argv[2:])
        try:
            while True:
                vals = pg.evaluate(js, specs)
                print("  " + "  ".join(f"{sys.argv[2+i]}={vals[i]}" for i in range(len(vals))))
                time.sleep(0.3)
        except KeyboardInterrupt:
            print("stopped.")
        return
    if cmd == "snap":
        n = pg.evaluate(SNAP_JS)
        print(f"snapshot taken: {n} bytes copied (window.__snap). Now change HP, then run 'diff'.")
        return
    if cmd == "diff":
        old = int(sys.argv[2]); nw = int(sys.argv[3]); tol = int(sys.argv[4]) if len(sys.argv) > 4 else 1
        n = pg.evaluate(DIFF_JS, {"old": old, "nw": nw, "tol": tol})
        if n < 0:
            print("no snapshot — run 'snap' first (at full HP)."); return
        print(f"diff old~{old} -> new~{nw} (tol {tol}): {n} candidate(s) (window.__co).")
        if n <= 24:
            co = pg.evaluate("window.__co"); ct = pg.evaluate("window.__ct")
            for o, t in zip(co, ct):
                print(f"  addr {o} (0x{o:x}) {TNAME[t]}")
        return
    if cmd == "diffr":
        full = float(sys.argv[2]); now = float(sys.argv[3]); tol = float(sys.argv[4]) if len(sys.argv) > 4 else 0.012
        ratio = now / full
        n = pg.evaluate(DIFFR_JS, {"ratio": ratio, "tol": tol})
        if n < 0:
            print("no snapshot — run 'snap' first."); return
        print(f"diffr full={full} now={now} ratio={ratio:.4f} (tol {tol}): {n} candidate(s) decreased proportionally.")
        if n <= 40:
            co = pg.evaluate("window.__co"); ct = pg.evaluate("window.__ct")
            for o, t in zip(co, ct):
                print(f"  addr {o} (0x{o:x}) {TNAME[t]}")
        return
    if cmd == "trackr":
        secs = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
        READSNAP_JS = """()=>{const sb=window.__snap.buffer, off=window.__co, typ=window.__ct;
          const i32=new Int32Array(sb),f32=new Float32Array(sb),f64=new Float64Array(sb),u16=new Uint16Array(sb);
          const out=new Array(off.length);
          for(let k=0;k<off.length;k++){const t=typ[k],o=off[k]; out[k]= t===0?i32[o/4]:t===2?f32[o/4]:t===3?f64[o/8]:u16[o/2];} return out;}"""
        snap = pg.evaluate(READSNAP_JS)
        co = pg.evaluate("window.__co"); ct = pg.evaluate("window.__ct")
        if not co:
            print("no candidates — run diffr first."); return
        print(f"trackr {len(co)} candidates for {secs}s — let HP RECOVER to full now...")
        def ok(x):
            return x == x and -1e30 < x < 1e30   # not NaN/inf
        cur0 = pg.evaluate(READVALS2_JS)
        mn = [x if ok(x) else None for x in cur0]
        mx = list(mn)
        t0 = time.time()
        while time.time() - t0 < secs:
            vv = pg.evaluate(READVALS2_JS)
            for k, x in enumerate(vv):
                if not ok(x):
                    mn[k] = None; continue
                if mn[k] is None: mn[k] = mx[k] = x; continue
                if x < mn[k]: mn[k] = x
                if x > mx[k]: mx[k] = x
            time.sleep(0.05)
        res = []
        for k in range(len(co)):
            s = snap[k]
            if ct[k] != 2 or mn[k] is None or not ok(s):
                continue
            # HP ratio: ~1.0 at full (snapshot), dipped and recovered, stays in [0,1.2]
            if 0.85 <= s <= 1.15 and 0 <= mn[k] < 0.75 * s and mx[k] > 0.8 * s and mx[k] <= 1.2:
                res.append((co[k], s, mn[k], mx[k]))
        res.sort(key=lambda r: r[2])
        print(f"{len(res)} HP-RATIO candidate(s) (f32 ~1.0 that dipped & recovered):")
        for o, s, lo, hi in res[:40]:
            print(f"  addr {o} (0x{o:x}) f32 snap={s:.3f} min={lo:.3f} max={hi:.3f}")
        return
    if cmd == "narrowr":
        full = float(sys.argv[2]); now = float(sys.argv[3]); tol = float(sys.argv[4]) if len(sys.argv) > 4 else 0.012
        ratio = now / full
        n = pg.evaluate(NARROWR_JS, {"ratio": ratio, "tol": tol})
        print(f"narrowr now={now}/{full} ratio={ratio:.4f} (tol {tol}): {n} candidate(s) remain.")
        if n <= 40:
            co = pg.evaluate("window.__co"); ct = pg.evaluate("window.__ct")
            for o, t in zip(co, ct):
                print(f"  addr {o} (0x{o:x}) {TNAME[t]}")
        return
    if cmd == "gscanu":
        v = int(sys.argv[2])
        r = pg.evaluate(SCANU_JS, v)
        print(f"gscanu {v} (UNALIGNED): u16u={r['u16u']} u32u={r['u32u']}  total={r['total']} candidates (window.__co).")
        return
    if cmd == "gnarrow2":
        v = float(sys.argv[2])
        n = pg.evaluate(NARROW2_JS, v)
        print(f"gnarrow2 {v}: {n} candidate(s) remain.")
        if n <= 16:
            co = pg.evaluate("window.__co"); ct = pg.evaluate("window.__ct")
            for o, t in zip(co, ct):
                print(f"  addr {o} (0x{o:x}) {TNAME[t]}")
        return
    if cmd == "gtrack":
        secs = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
        multi = pg.evaluate("!!window.__co")
        cands = pg.evaluate("window.__co" if multi else "window.__cands")
        types = pg.evaluate("window.__ct") if multi else None
        if not cands:
            print("no candidates — run gscanall/gscan first."); return
        rv = READVALS2_JS if multi else READVALS_JS
        print(f"tracking {len(cands)} candidates for {secs}s — TAKE DAMAGE now (let a mob hit you)...")
        vals0 = pg.evaluate(rv)
        mn = list(vals0); mx = list(vals0)
        t0 = time.time()
        while time.time() - t0 < secs:
            vv = pg.evaluate(rv)
            for k, x in enumerate(vv):
                if x < mn[k]: mn[k] = x
                if x > mx[k]: mx[k] = x
            time.sleep(0.04)
        dipped = [(cands[k], mn[k], mx[k], (types[k] if types else None)) for k in range(len(cands)) if mn[k] != mx[k]]
        dipped.sort(key=lambda r: r[1])
        print(f"{len(dipped)} candidate(s) CHANGED (these track a live value like HP):")
        for off, lo, hi, t in dipped[:50]:
            tn = TNAME.get(t, "?") if t is not None else "?"
            print(f"  addr {off} (0x{off:x}) {tn}  min={lo} max={hi}")
        if not dipped:
            print("  none changed — HP didn't dip, or scan value was stale. Take MORE damage / retry.")
        return

    if not plausible:
        print(f"player ptr = {base} looks wrong — are you in-game? (expected a heap address)")
        return

    if cmd == "findhp2":
        mx = int(sys.argv[2]) if len(sys.argv) > 2 else 315
        pg.evaluate(SCAN_JS, {"value": mx, "type": "i32"})
        cands = pg.evaluate("window.__cands")
        co = [c - 8 for c in cands if c - 8 >= 0]
        ct = [2] * len(co)   # f32 ratio at maxHP-8
        pg.evaluate("(a)=>{window.__co=a.co; window.__ct=a.ct;}", {"co": co, "ct": ct})
        print(f"{len(co)} HP-ratio candidates (f32 @ each maxHP={mx} minus 8 bytes).")
        print("Now run: python memread.py gtrack 45   while taking damage. The live HP ratio drops; statics stay flat.")
        return
    if cmd == "entities":
        REG_MAP, REG_CAP = 362748, 362752
        mapp = read_ptr(pg, REG_MAP); cap = read_ptr(pg, REG_CAP)
        print(f"registry: map=0x{mapp:x} buckets={cap}")
        js = """(a)=>{
          const dv=new DataView(HEAPU8.buffer), map=a.map, cap=a.cap, out=[];
          const ok=p=>p>0x10000&&p<0x10000000;
          if(!ok(map)||cap<=0||cap>5000000) return out;
          for(let b=0;b<cap;b++){
            let node=dv.getUint32(map+b*4,true), guard=0;
            while(ok(node)&&guard++<100000){
              const key=dv.getUint32(node+8,true);
              const val=dv.getUint32(node+16,true);
              let maxhp=null, ratio=null;
              if(ok(val)){ maxhp=dv.getInt32(val+316,true); ratio=dv.getFloat32(val+308,true); }
              out.push([node,key,val,maxhp,ratio]);
              node=dv.getUint32(node,true);
              if(out.length>20000) return out;
            }
          }
          return out;
        }"""
        nodes = pg.evaluate(js, {"map": mapp, "cap": cap})
        print(f"{len(nodes)} registry nodes walked")
        ents = [(n, k, v, m, r) for (n, k, v, m, r) in nodes if v and 0x10000 < v < 0x10000000 and m and 0 < m < 100000]
        print(f"{len(ents)} look like entities (valid ptr + plausible maxHP @+316):")
        for n, k, v, m, r in ents[:60]:
            hp = (r * m) if (r is not None and -1 < r < 2) else None
            print(f"  key={k:<12} entity=0x{v:x} maxHP={m} ratio={r:.3f} HP={hp:.0f}" if hp is not None
                  else f"  key={k:<12} entity=0x{v:x} maxHP={m} ratio={r}")
        return
    if cmd == "hp":
        controller = read_ptr(pg, PLAYER_PTR_ADDR)
        entity = read_ptr(pg, controller + 7792)
        ratio = struct.unpack("<f", read_bytes(pg, entity + 308, 4))[0]
        maxhp = struct.unpack("<i", read_bytes(pg, entity + 316, 4))[0]
        print(f"controller=0x{controller:x} entity=0x{entity:x}")
        print(f"HP ratio(+308)={ratio:.4f}  maxHP(+316)={maxhp}  ->  HP = {ratio*maxhp:.1f}/{maxhp}")
        return
    if cmd == "hpwatch":
        secs = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
        print(f"watching HP via pointer chain for {secs}s (change your HP):")
        t0 = time.time()
        while time.time() - t0 < secs:
            controller = read_ptr(pg, PLAYER_PTR_ADDR)
            entity = read_ptr(pg, controller + 7792)
            ratio = struct.unpack("<f", read_bytes(pg, entity + 308, 4))[0]
            maxhp = struct.unpack("<i", read_bytes(pg, entity + 316, 4))[0]
            print(f"  HP = {ratio*maxhp:5.1f}/{maxhp}  (ratio {ratio:.3f})")
            time.sleep(0.7)
        return
    if cmd == "gwatchn":
        secs = float(sys.argv[2])
        tc = {"i32":0,"u16":1,"f32":2,"f64":3}
        specs = [[int(s.split(":")[0]), tc[s.split(":")[1]]] for s in sys.argv[3:]]
        labels = sys.argv[3:]
        js = """(specs)=>{const buf=HEAPU8.buffer;
          const i32=new Int32Array(buf),u16=new Uint16Array(buf),f32=new Float32Array(buf),f64=new Float64Array(buf);
          return specs.map(s=>{const o=s[0],t=s[1];return t===0?i32[o/4]:t===1?u16[o/2]:t===2?f32[o/4]:f64[o/8];});}"""
        v0 = pg.evaluate(js, specs); mn = list(v0); mx = list(v0)
        print(f"gwatchn {secs}s over {len(specs)} addrs — TAKE DAMAGE now...")
        t0 = time.time()
        while time.time() - t0 < secs:
            vv = pg.evaluate(js, specs)
            for k, x in enumerate(vv):
                if x < mn[k]: mn[k] = x
                if x > mx[k]: mx[k] = x
            time.sleep(0.05)
        for k, lab in enumerate(labels):
            print(f"  {lab}: min={mn[k]:.4f} max={mx[k]:.4f}")
        return
    if cmd == "gpeek":
        addr = int(sys.argv[2], 0); cnt = int(sys.argv[3]) if len(sys.argv) > 3 else 32
        buf = read_bytes(pg, addr, cnt * 4)
        print(f"gpeek 0x{addr:x}:")
        for i in range(0, cnt * 4, 4):
            iv = struct.unpack_from("<i", buf, i)[0]
            fv = struct.unpack_from("<f", buf, i)[0]
            fs = f"{fv:.2f}" if -1e9 < fv < 1e9 else "·"
            print(f"  +{i:4d} (0x{addr+i:x}) i32={iv:<12} f32={fs}")
        return
    if cmd == "chase":
        val = int(sys.argv[2])
        struct_n = int(sys.argv[3]) if len(sys.argv) > 3 else 8192
        sub_n = 4096
        cbuf = read_bytes(pg, base, struct_n)
        seen = set(); found = []
        for off in range(0, struct_n - 4, 4):
            ptr = struct.unpack_from("<I", cbuf, off)[0]
            if not (0x10000 < ptr < 0x10000000) or ptr in seen:
                continue
            seen.add(ptr)
            if len(seen) > 400:
                break
            try:
                sub = read_bytes(pg, ptr, sub_n)
            except Exception:
                continue
            for so in range(0, len(sub) - 4, 4):
                iv = struct.unpack_from("<i", sub, so)[0]
                fv = struct.unpack_from("<f", sub, so)[0]
                if iv == val:
                    found.append((off, ptr, so, "i32"))
                elif abs(fv - val) < 0.5:
                    found.append((off, ptr, so, "f32"))
        print(f"chase {val} via {len(seen)} pointer fields: {len(found)} hit(s):")
        for off, ptr, so, t in found[:50]:
            print(f"  controller+{off} -> 0x{ptr:x} +{so} {t}")
        return

    if cmd == "dump":
        n = int(sys.argv[2]) if len(sys.argv) > 2 else DUMP_BYTES
        buf = read_bytes(pg, base, n)
        open("memdump.bin", "wb").write(buf)
        print(f"player base 0x{base:x}; dumped {len(buf)} bytes -> memdump.bin")
        # quick scan: small positive i32 / u16 that could be HP-like, and finite floats
        print("offsets with i32 in [1,100000] (HP/level/stat candidates), first 40:")
        shown = 0
        for off in range(0, len(buf) - 4, 4):
            v = struct.unpack_from("<i", buf, off)[0]
            if 1 <= v <= 100000:
                print(f"  +{off:5d}  i32={v}")
                shown += 1
                if shown >= 40: break

    elif cmd == "peek":
        off0 = int(sys.argv[2]); cnt = int(sys.argv[3]) if len(sys.argv) > 3 else 32
        buf = read_bytes(pg, base, off0 + cnt*4 + 8)
        print(f"player base 0x{base:x}; i32 / f32 from +{off0}:")
        for off in range(off0, off0 + cnt*4, 4):
            i = struct.unpack_from("<i", buf, off)[0]
            f = struct.unpack_from("<f", buf, off)[0]
            fs = f"{f:.3f}" if -1e9 < f < 1e9 else "·"
            print(f"  +{off:6d}  i32={i:<12} f32={fs}")

    elif cmd == "scan":
        val = sys.argv[2]
        size = int(sys.argv[3]) if len(sys.argv) > 3 else 131072
        buf = read_bytes(pg, base, size)
        print(f"player base 0x{base:x}; scanning {len(buf)} bytes for {val} across types:")
        for t, step, fmt in (("i32",4,"<i"),("u16",2,"<H"),("f32",4,"<f")):
            tgt = float(val) if t == "f32" else int(val)
            hits = []
            for off in range(0, len(buf)-4, step):
                v = struct.unpack_from(fmt, buf, off)[0]
                ok = (abs(v-tgt) < 0.5) if t == "f32" else (v == tgt)
                if ok: hits.append(off)
            print(f"  {t}: {len(hits)} hit(s){' -> ' + str(hits[:40]) if hits else ''}")

    elif cmd == "find":
        val = sys.argv[2]; t = sys.argv[3] if len(sys.argv) > 3 else "i32"
        target = float(val) if t == "f32" else int(val)
        buf = read_bytes(pg, base, DUMP_BYTES)
        step = 1 if t in ("u8",) else (2 if t in ("i16", "u16") else 4)
        hits = []
        for off in range(0, len(buf) - 4, step):
            v = as_vals(buf, off, t)
            if v is None: continue
            if t == "f32":
                if abs(v - target) < 0.5: hits.append((off, v))
            elif v == target:
                hits.append((off, v))
        print(f"player base 0x{base:x}; {len(hits)} offset(s) == {val} as {t}:")
        for off, v in hits[:60]:
            print(f"  +{off:5d}  {t}={v}")

    elif cmd == "watch":
        specs = [(int(s.split(":")[0]), s.split(":")[1]) for s in sys.argv[2:]]
        print("watching (Ctrl+C to stop):", specs)
        try:
            while True:
                buf = read_bytes(pg, base, DUMP_BYTES)
                print("  " + "  ".join(f"+{o}={as_vals(buf,o,t)}" for o, t in specs))
                time.sleep(0.3)
        except KeyboardInterrupt:
            print("stopped.")

    elif cmd == "track":
        secs = float(sys.argv[2]) if len(sys.argv) > 2 else 30.0
        print(f"tracking the player struct for {secs}s — TAKE DAMAGE / move now...")
        first = read_bytes(pg, base, DUMP_BYTES)
        n = len(first) - 4
        mn = [struct.unpack_from("<i", first, o)[0] for o in range(0, n)]
        mx = list(mn)
        t0 = time.time()
        while time.time() - t0 < secs:
            buf = read_bytes(pg, base, DUMP_BYTES)
            for o in range(0, n):
                v = struct.unpack_from("<i", buf, o)[0]
                if v < mn[o]: mn[o] = v
                if v > mx[o]: mx[o] = v
            time.sleep(0.05)
        changed = [(o, mn[o], mx[o]) for o in range(0, n, 4)
                   if mn[o] != mx[o] and -100000 < mn[o] and mx[o] < 100000]
        changed.sort(key=lambda r: r[2] - r[1])
        print(f"{len(changed)} i32 offsets changed (range-sorted). HP-like = max~your maxHP, dipped:")
        for o, lo, hi in changed[:60]:
            print(f"  +{o:5d}  i32 min={lo} max={hi} range={hi-lo}")

if __name__ == "__main__":
    main()
