#!/usr/bin/env python3
# Auto-fixer: iteratively run the bot match, catch each crash, resolve the crashing
# il2cpp method from the backtrace, stub it (xor eax,eax; ret), and retry until the
# match runs clean (thieves spawned, no crash) or no further progress is possible.
import frida, time, subprocess, json, os, re, sys
sys.path.insert(0, "/root/ants/patch")
import resolve_addr as R
ADB="/opt/android-sdk/platform-tools/adb"

STUBS_FILE="/root/ants/autofix_stubs.json"
INIT_STUBS=[0xd53c49,0xd3ce0b,0xd494ee,0xd4dcda,     # DeltaDNA/analytics
            0xd29beb,0xd299a4,0xd2a24a,0xd29aea,      # LootSafe menu UI (fixes cold-boot flakiness)
            0xe4a362]                                  # StartThiefIntro (controller-dependent, sidestepped)
NOISE=("TerrainData","CountMessageDelegate","ImageConversion","WritableAttribute",
       "IOSWrapper","OffMeshLink","InboxMessage","Tilemap","ParseCsvMethod","Object::.ctor")

def load_stubs():
    if os.path.exists(STUBS_FILE):
        try: return sorted(set(json.load(open(STUBS_FILE))))
        except Exception: pass
    return sorted(set(INIT_STUBS))
def save_stubs(s): json.dump(sorted(set(s)), open(STUBS_FILE,"w"))

def method_of(va):
    name=R.resolve(va)
    m=re.search(r'\+0x([0-9a-f]+)\)', name)
    off=int(m.group(1),16) if m else 0
    return va-off, name

def sh(*a): return subprocess.run([ADB]+list(a),capture_output=True,text=True)
def tap(x,y): subprocess.run([ADB,"shell","input","tap",str(x),str(y)],capture_output=True)
def cap(): return subprocess.run([ADB,"exec-out","screencap","-p"],capture_output=True).stdout

def build_js(stubs):
    arr="["+",".join("0x%x"%s for s in stubs)+"]"
    return (r"""
var B=null, isn=null;
function main(){
 var m=Process.findModuleByName("libil2cpp.so");if(!m){setTimeout(main,150);return;}
 B=m.base;
 isn=new NativeFunction(m.findExportByName("il2cpp_string_new"),'pointer',['pointer']);
 // STUBS: xor eax,eax; ret  (return null/0)
 var STUBS=__STUBS__;
 STUBS.forEach(function(a){try{var p=B.add(a);Memory.protect(p,16,'rwx');p.writeU8(0x31);p.add(1).writeU8(0xc0);p.add(2).writeU8(0xc3);}catch(e){}});
 // behavior fixes: master-client, role by rank sign (persona+gameplayer), avatar key
 try{Interceptor.attach(B.add(0xd6fede),{onLeave:function(r){r.replace(ptr(1));}});}catch(e){}
 try{Interceptor.attach(B.add(0xe5b13b),{onEnter:function(){var p=this.context.ecx;try{var rk=p.add(0xe0).readS32();p.add(0xd0).writeU8(rk>=0?1:2);}catch(e){}}});}catch(e){}
 try{Interceptor.attach(B.add(0xe4be5a),{onEnter:function(){var it=this.context.esi;try{var rk=it.add(0xe0).readS32();it.add(0xd0).writeU8(rk>=0?1:2);}catch(e){}}});}catch(e){}
 // avatar+bag now come from the bmm loadout (avtr=ava_1, gear=bg_0) - no injection needed
 var spawned=false, reported=false;
 try{Interceptor.attach(B.add(0xdf9152),{onEnter:function(){spawned=true;send({g:"THIEF_SPAWNED"});}});}catch(e){}
 // hook the null-ref throw helper: its caller is the method that null-derefs on the half-built bot.
 // Report the first game-range caller after spawn (pointer-compared). This is the reliable signal.
 try{Interceptor.attach(B.add(0xbd06f4),{onEnter:function(){
   if(!spawned||reported)return;
   var ra=this.returnAddress; if(ra.isNull())return;
   var mm=Process.findModuleByAddress(ra); if(!mm||mm.name!="libil2cpp.so")return;
   var off=ra.sub(mm.base);
   if(off.compare(ptr(0xd00000))>=0 && off.compare(ptr(0xf00000))<=0){ reported=true; send({g:"NREFRAME:0x"+off.toString(16)}); }
 }});}catch(e){}
 // swallow native crashes so a flaky segfault doesn't kill the run before bd06f4 reports
 Process.setExceptionHandler(function(d){ send({g:"NATIVE_CRASH"}); return false; });
 send({g:"__ready"});
}
main();
""").replace("__STUBS__", arr)

def run_iteration(stubs):
    """Returns ('crash', frames) | ('playable', None) | ('nospawn', None) | ('nomenu', None)"""
    sh("shell","am","force-stop","se.foglo.svt"); time.sleep(1)
    sh("shell","input","keyevent","3"); time.sleep(1)
    sh("shell","monkey","-p","se.foglo.svt","-c","android.intent.category.LAUNCHER","1")
    t0=time.time(); menu=False
    while time.time()-t0<140:
        time.sleep(4)
        if len(cap())>600000: menu=True; break
    if not menu: return ("nomenu", None)
    time.sleep(3)
    dev=frida.get_usb_device(timeout=10)
    try: pid=int(sh("shell","pidof","se.foglo.svt").stdout.split()[0])
    except Exception: return ("nomenu", None)
    state={"crash":None,"spawned":False}
    def on_msg(m,d):
        if isinstance(m.get("payload"),dict):
            g=m["payload"].get("g","")
            if g.startswith("CRASHFRAMES:") and state["crash"] is None:
                state["crash"]=[f for f in g[len("CRASHFRAMES:"):].split(",") if f]
            elif g.startswith("NREFRAME:") and state["crash"] is None:
                state["crash"]=[g[len("NREFRAME:"):]]
            elif g=="THIEF_SPAWNED": state["spawned"]=True
            elif g=="__ready": pass
            else: print("   js:",g,flush=True)
    try:
        s=dev.attach(pid); sc=s.create_script(build_js(stubs))
        sc.on("message",on_msg); sc.load()
    except Exception as e:
        print("   attach/script err:",e,flush=True); return ("nomenu", None)
    time.sleep(2)
    tap(700,730); time.sleep(6)          # SNIPER
    tap(1355,960)                         # GO
    # watch up to 60s for crash; if none and spawned, it's playable
    for _ in range(30):
        time.sleep(2)
        if state["crash"]: break
    try: sc.unload(); s.detach()
    except Exception: pass
    if state["crash"]: return ("crash", state["crash"])
    if state["spawned"]: return ("playable", None)
    return ("nospawn", None)

def main():
    stubs=load_stubs()
    print("=== AUTO-FIX START, %d initial stubs ==="%len(stubs),flush=True)
    for it in range(1,26):
        print("\n##### ITERATION %d (%d stubs) #####"%(it,len(stubs)),flush=True)
        result,frames=run_iteration(stubs)
        print("  result:",result,flush=True)
        if result=="playable":
            print("\n*** MATCH PLAYABLE (thieves spawned, no crash) after %d iterations ***"%it,flush=True)
            print("stubs:",[hex(s) for s in stubs],flush=True); break
        if result=="nomenu":
            print("  (menu not reached - retrying)",flush=True); continue
        if result=="nospawn":
            print("  (no thief spawn, no crash - flow stalled earlier; retrying)",flush=True); continue
        # crash: pick deepest real game method not already stubbed
        picked=None
        for f in frames:
            va=int(f,16); start,name=method_of(va)
            if any(n in name for n in NOISE): continue
            if start in stubs: continue
            picked=(start,name); break
        if picked is None:
            print("  no new stubbable method in backtrace:",frames[:8],flush=True)
            # last resort: stub the outermost real frame even if it's Start
            for f in frames:
                va=int(f,16); start,name=method_of(va)
                if any(n in name for n in NOISE): continue
                if start in stubs: continue
                picked=(start,name); break
            if picked is None: print("  STUCK - cannot progress",flush=True); break
        start,name=picked
        stubs.append(start); save_stubs(stubs)
        print("  >>> auto-stubbing %s @ 0x%x"%(name,start),flush=True)
    print("\n=== AUTO-FIX DONE. Final stubs saved to %s ==="%STUBS_FILE,flush=True)

if __name__=="__main__":
    main()
