#!/usr/bin/env python3
# autofix2 — self-healing match runner. Improvements over autofix.py:
#  1) SANITIZER: re-implements UnityEngine.Object::IsNativeObjectAlive (0x788ab7) crash-proof —
#     a null/misaligned/unmapped object pointer returns "dead" instead of segfaulting. This kills
#     the ENTIRE "==null on a garbage object" class (pvCache/PhotonView etc.) generically, live,
#     with no per-wall stubbing and no restart.
#  2) Reliable naming via resolve_addr (proven correct this session).
#  3) Progress tracking: menu -> Thief.Awake xN -> GameController.Awake -> LoadLevel -> gameplay.
#  4) Managed-NRE walls (bd06f4 -> game caller) are reported + live-stubbed via post-back, so ONE
#     run chews through many walls instead of one-restart-per-stub.
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/autofix2_stubs.json"
# Minimal necessary stub set (per Tertiary audit). Dropped as vestigial/unsafe:
#   0xd53c49 CollectTutorialProgress (NON-VOID: bare-ret returns garbage String ptr — unsafe),
#   0xd3ce0b OnLevelBeginLoad (redundant w/ SendEvent), 0xd29beb UpdateMe + 0xd2a24a UpdateSlot
#   (dead once Update+SetSlot stubbed — only vault-click handlers reach them).
# PROVEN 9-stub baseline (reliably reached 4-thieves-spawn). Stub-minimization deferred — the
# Tertiary "minimal 5" broke the LoadLevel telemetry chain (CollectTutorialProgress->Gu3.Json crash).
INIT_STUBS=[0xd53c49,0xd3ce0b,0xd494ee,0xd4dcda,      # analytics (incl. OnLevelBeginLoad + CollectTutorialProgress)
            0xd29beb,0xd299a4,0xd2a24a,0xd29aea,       # LootSafe UI
            0xe4a362]                                   # StartThiefIntro
# If analytics is ever narrowed off the broad SendEvent stub, also stub SendToAppsFlyer 0xd4e047.
# Filled from Primary agent's targeted pvCache/PhotonView fix (e.g. get_photonView -> ret null).
# Methods here are byte-stubbed to `xor eax,eax; ret` (return null) instead of plain `ret`.
EXTRA_NULL_STUBS=[]
NOISE=("TerrainData","ImageConversion","WritableAttribute","OffMeshLink","Tilemap",
       "CountMessageDelegate","closedir","char_traits","Rb_tree","vector","cxxabi","basic_string")

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 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 shot(n):
    d=cap(); open("/root/ants/shots/"+n,"wb").write(d); return len(d)
def method_start(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 build_js(stubs):
    arr="["+",".join("0x%x"%s for s in stubs)+"]"
    narr="["+",".join("0x%x"%s for s in EXTRA_NULL_STUBS)+"]"
    return (r"""
var B=null;
function main(){
 var m=Process.findModuleByName("libil2cpp.so");if(!m){setTimeout(main,150);return;}
 B=m.base;
 var STUBS=__STUBS__;
 function stub(a){try{var p=B.add(a);Memory.protect(p,16,'rwx');p.writeU8(0xc3);}catch(e){}}
 function nullstub(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){}} // xor eax,eax; ret
 STUBS.forEach(stub);
 __NULLSTUBS__.forEach(nullstub);
 // behavior fixes
 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){}
 // === SANITIZER: IsNativeObjectAlive (0x788ab7) reads its object from [esp+8] at entry.
 // If that object is null/misaligned/unmapped, swap in a zeroed dummy so the original
 // safely computes "dead" ([dummy+8]==0) instead of segfaulting on push [esi+8]. ===
 // === SANITIZER at Photon.MonoBehaviour::get_photonView (0xd6485b) — the ROOT crash site.
 // It does `if(pvCache==null) pvCache=GetComponent<PhotonView>()`. For bot thieves pvCache
 // holds stale garbage (non-null) -> the ==null compare deref-crashes. Zero garbage pvCache
 // (+0xc) on entry: then GetComponent runs -> real view for local player, null for bots
 // (bots take Thief.Start's clean early-return). Non-hot, safe, catches every object. ===
 var san=0;
 function badptr(v){ var u=v.toUInt32();
   if(u==0) return false;                           // already clean null
   if((u&3)!=0) return true;                         // misaligned: garbage
   if(u>=0xc0000000 && u<0xd8000000) return false;   // normal il2cpp heap: valid view
   try{ v.add(8).readPointer(); return false; }catch(e){ return true; } // unmapped: garbage
 }
 var pvCalls=0;
 try{Interceptor.attach(B.add(0xd6485b),{onEnter:function(){ pvCalls++;
   var self=this.context.esp.add(4).readPointer(); var pv=self.add(0xc);
   if(badptr(pv.readPointer())){ pv.writePointer(ptr(0)); san++; }
 }});}catch(e){send({g:"SANITIZER FAIL "+e});}
 setInterval(function(){ send({g:"PROG get_photonView calls="+pvCalls+" sanitized="+san}); },4000);
 // pinpoint the 0xbded37 fatal crash: allocator bef6b5 returns null to the crashing call site
 try{Interceptor.attach(B.add(0xbef6b5),{
   onEnter:function(){this.ra=this.returnAddress;},
   onLeave:function(r){ if(r.isNull() && this.ra.equals(B.add(0xbded37))){
     var fr="";for(var i=0;i<0x2000;i+=4){try{var v=this.context.esp.add(i).readPointer();var m2=Process.findModuleByAddress(v);
       if(m2&&m2.name=="libil2cpp.so"){var o=v.sub(m2.base);if(o.compare(ptr(0xd00000))>=0&&o.compare(ptr(0x1200000))<=0)fr+="0x"+o.toString(16)+" ";}}catch(e){}}
     send({g:"CRASHCALLER game-frames="+fr});
   }}});}catch(e){}
 // progress markers
 var awoke=0;
 try{Interceptor.attach(B.add(0xdf9152),{onEnter:function(){awoke++;send({g:"PROG Thief.Awake #"+awoke});}});}catch(e){}
 try{Interceptor.attach(B.add(0xe47cab),{onEnter:function(){send({g:"PROG GameController.Awake"});}});}catch(e){}
 try{Interceptor.attach(B.add(0xd3cb37),{onEnter:function(){send({g:"PROG LoadLevel"});}});}catch(e){}
 try{Interceptor.attach(B.add(0xe0b90c),{onEnter:function(){send({g:"PROG Thief.OnHealthChange (SHOOTABLE!)"});}});}catch(e){}
 // managed-NRE wall reporter: game-range caller of the null-throw helper, after spawn
 var spawned=false; try{Interceptor.attach(B.add(0xdf9152),{onEnter:function(){spawned=true;}});}catch(e){}
 var seen={};
 try{Interceptor.attach(B.add(0xbd06f4),{onEnter:function(){
   if(!spawned)return; var ra=this.returnAddress; var mm=Process.findModuleByAddress(ra);
   if(!mm||mm.name!="libil2cpp.so")return; var o=ra.sub(mm.base);
   if(o.compare(ptr(0xd00000))>=0&&o.compare(ptr(0x1200000))<=0){var k=o.toString(16); if(!seen[k]){seen[k]=1;send({g:"NREWALL:0x"+k});}}
 }});}catch(e){}
 // live-stub requests from python
 recv('stub',function f(msg){ stub(msg.addr); send({g:"live-stubbed 0x"+msg.addr.toString(16)}); recv('stub',f); });
 // native crash: report faulting site + resolvable game frames on the stack
 var ncrash=0;
 Process.setExceptionHandler(function(d){ ncrash++;
   if(ncrash<=2){var a=d.address;var mm=Process.findModuleByAddress(a);
     var loc=mm?mm.name+"+0x"+a.sub(mm.base).toString(16):(""+a);
     // deep stack scan for GAME-range return addresses (0xd00000..0x1200000 = game code, not runtime)
     var fr="";try{for(var i=0;i<0x1800;i+=4){var v=d.context.esp.add(i).readPointer();var m2=Process.findModuleByAddress(v);
       if(m2&&m2.name=="libil2cpp.so"){var off=v.sub(m2.base);if(off.compare(ptr(0xd00000))>=0&&off.compare(ptr(0x1200000))<=0)fr+="0x"+off.toString(16)+" ";}}}catch(e){}
     send({g:"NATIVE @"+loc+" game-frames="+fr});}
   return false;});
 send({g:"__ready"});
}
main();
""").replace("__STUBS__",arr).replace("__NULLSTUBS__",narr)

def run(stubs, secs=150):
    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 {"result":"nomenu"}
    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 {"result":"nomenu"}
    st={"walls":[],"native":[],"prog":[],"san":0,"new_stubs":[]}
    def on_msg(m,d):
        if not isinstance(m.get("payload"),dict): return
        g=m["payload"].get("g","")
        if g.startswith("NREWALL:"):
            va=int(g.split(":")[1],16); start,name=method_start(va)
            if any(n in name for n in NOISE): return
            if start in stubs or start in st["new_stubs"]: return
            st["new_stubs"].append(start); st["walls"].append((start,name))
            print("   NRE wall: %s @0x%x -> live-stub"%(name,start),flush=True)
            try: sc.post({'type':'stub','addr':start})
            except Exception: pass
        elif g.startswith("PROG"): st["prog"].append(g); print("  ",g,flush=True)
        elif g.startswith("NATIVE"): st["native"].append(g); print("  ",g,flush=True)
        elif g.startswith("SAN calls="):
            try: st["san"]=int(g.split("sanitized=")[1])
            except Exception: pass
        elif g.startswith("SANITIZER FAIL"): print("  ",g,flush=True)
        else: print("   js:",g,flush=True)
    s=dev.attach(pid); sc=s.create_script(build_js(stubs)); sc.on("message",on_msg); sc.load()
    time.sleep(2); tap(700,730); time.sleep(6); tap(1355,960)
    for k in range(secs//8):
        time.sleep(8)
        if k in (6,10,secs//8-1): shot("af2_%02d.png"%k)
    try: sc.unload(); s.detach()
    except Exception: pass
    st["result"]="ran"
    return st

def main():
    stubs=load_stubs()
    print("=== AUTOFIX2 (sanitizer + progress) start, %d stubs ==="%len(stubs),flush=True)
    for it in range(1,8):
        print("\n##### RUN %d (%d stubs) #####"%(it,len(stubs)),flush=True)
        st=run(stubs)
        if st["result"]=="nomenu": print("  nomenu, retry",flush=True); continue
        # fold any newly discovered stubs in and persist
        if st["new_stubs"]:
            stubs=sorted(set(stubs)|set(st["new_stubs"])); save_stubs(stubs)
        prog=" | ".join(st["prog"][-6:])
        print("  PROGRESS:",prog or "(none)",flush=True)
        print("  sanitized:",st["san"],"| native crashes:",len(st["native"]),"| new stubs:",len(st["new_stubs"]),flush=True)
        # success heuristic: reached gameplay (health change = shootable) or stable match with no native crash
        if any("SHOOTABLE" in p for p in st["prog"]):
            print("\n*** MATCH PLAYABLE — thief took damage (shootable) ***",flush=True); break
        if not st["native"] and any("LoadLevel" in p for p in st["prog"]) and not st["new_stubs"]:
            print("\n*** Match loaded, no crashes, no new walls — likely playable ***",flush=True); break
        if not st["new_stubs"] and not st["native"]:
            print("  no new walls and no crashes but no gameplay marker — inspect shots",flush=True); break
    print("\n=== AUTOFIX2 done. stubs -> %s ==="%STUBS_FILE,flush=True)

if __name__=="__main__": main()
