#!/usr/bin/env python3
# Autonomous PIERole/CreateMatch experiment runner.
# Runs detached; tries several strategies to make the offline bot-match load an arena.
# Logs everything to autopie.log and screenshots per attempt. Safe to close SSH.
import frida, time, subprocess, sys, datetime, shutil, traceback
sys.path.insert(0,"/root/ants"); import emu

LOG="/root/ants/autopie.log"
def log(*a):
    line="[%s] %s"%(datetime.datetime.now().strftime("%H:%M:%S")," ".join(str(x) for x in a))
    print(line,flush=True)
    open(LOG,"a").write(line+"\n")

JS_TEMPLATE=r"""
var STRATEGY = __STRAT__;
var domain_get,dga,asm_img,cfn,onew,oinit,gmn,rinv,cgt,tgo;
function setup(mod){
 function E(n){var p=mod.findExportByName?mod.findExportByName(n):null; if(!p&&Module.getGlobalExportByName)p=Module.getGlobalExportByName(n); if(!p)throw new Error("no export "+n); return p;}
 domain_get=new NativeFunction(E("il2cpp_domain_get"),'pointer',[]);
 dga=new NativeFunction(E("il2cpp_domain_get_assemblies"),'pointer',['pointer','pointer']);
 asm_img=new NativeFunction(E("il2cpp_assembly_get_image"),'pointer',['pointer']);
 cfn=new NativeFunction(E("il2cpp_class_from_name"),'pointer',['pointer','pointer','pointer']);
 onew=new NativeFunction(E("il2cpp_object_new"),'pointer',['pointer']);
 oinit=new NativeFunction(E("il2cpp_runtime_object_init"),'void',['pointer']);
 gmn=new NativeFunction(E("il2cpp_class_get_method_from_name"),'pointer',['pointer','pointer','int']);
 rinv=new NativeFunction(E("il2cpp_runtime_invoke"),'pointer',['pointer','pointer','pointer','pointer']);
 cgt=new NativeFunction(E("il2cpp_class_get_type"),'pointer',['pointer']);
 tgo=new NativeFunction(E("il2cpp_type_get_object"),'pointer',['pointer']);
}
function cstr(s){return Memory.allocUtf8String(s);}
function sstr(p){try{if(p.isNull()||p.compare(ptr(0x10000))<0)return "n/a"; var n=p.add(8).readS32();if(n<0||n>300)return "?len"; return "'"+p.add(0xc).readUtf16String(n)+"'";}catch(e){return "e";}}
function findClass(ns,nm){
 var dom=domain_get();var cnt=Memory.alloc(8);var arr=dga(dom,cnt);var n=cnt.readU32();
 var nsp=cstr(ns),nmp=cstr(nm);
 for(var i=0;i<n;i++){var asm=arr.add(i*Process.pointerSize).readPointer();if(asm.isNull())continue;var img=asm_img(asm);if(img.isNull())continue;
  var k=cfn(img,nsp,nmp); if(!k.isNull())return k;}
 return ptr(0);
}
var B=null, frames=0, done=false, mmThis=ptr(0);
function setOffline(PN){
 if(PN.isNull())return;
 var s=gmn(PN,cstr("set_offlineMode"),1); if(s.isNull())return;
 var bval=Memory.alloc(1);bval.writeU8(1);var pp=Memory.alloc(Process.pointerSize);pp.writePointer(bval);
 var exc=Memory.alloc(Process.pointerSize);exc.writePointer(ptr(0));
 rinv(s,ptr(0),pp,exc); send({g:"offlineMode=true"});
}
function makePIE(PIERole,GO){
 var go=onew(GO); oinit(go);
 var ptype=tgo(cgt(PIERole));
 var addM=gmn(GO,cstr("AddComponent"),1); if(addM.isNull())addM=gmn(GO,cstr("Internal_AddComponentWithType"),1);
 var params=Memory.alloc(Process.pointerSize); params.writePointer(ptype);
 var exc=Memory.alloc(Process.pointerSize); exc.writePointer(ptr(0));
 var comp=rinv(addM,go,params,exc); if(comp.isNull())comp=go;
 send({g:"made PIERole comp="+comp+" exc="+exc.readPointer()});
 return comp;
}
function doWork(){
 try{
  var PIERole=findClass("","PIERole"), GO=findClass("UnityEngine","GameObject"), PN=findClass("","PhotonNetwork");
  send({g:"cls PIERole="+PIERole+" GO="+GO+" PN="+PN+" mmThis="+mmThis});
  if(STRATEGY==1){ // synthetic PIERole + DIRECT OnSelectSniper(this,methodInfo)
    setOffline(PN); var comp=makePIE(PIERole,GO);
    var osM=gmn(PIERole,cstr("OnSelectSniper"),0);
    send({g:"S1 methodPtr="+(osM.isNull()?"null":osM.readPointer())+" expect="+B.add(0xd860ba)});
    var fn=new NativeFunction(B.add(0xd860ba),'void',['pointer','pointer']);
    fn(comp,osM); send({g:"S1 direct OnSelectSniper returned"});
  } else if(STRATEGY==2){ // synthetic PIERole + runtime_invoke OnSelectSniper (control)
    setOffline(PN); var comp=makePIE(PIERole,GO);
    var osM=gmn(PIERole,cstr("OnSelectSniper"),0);
    var exc=Memory.alloc(Process.pointerSize);exc.writePointer(ptr(0));
    rinv(osM,comp,ptr(0),exc); send({g:"S2 rinv OnSelectSniper exc="+exc.readPointer()});
  } else if(STRATEGY==3){ // use REAL Matchmaking this (captured), call CreateBotMatch via direct VA
    setOffline(PN);
    if(mmThis.isNull()){send({g:"S3 no Matchmaking this captured"});return;}
    // GetLocalPersona(retptr,0,1,0,0) -> 216-byte struct
    var persona=Memory.alloc(256);
    var glp=new NativeFunction(B.add(0xd31e77),'void',['pointer','int','int','int','pointer']);
    glp(persona,0,1,0,ptr(0)); send({g:"S3 GetLocalPersona done"});
    // CreateBotMatch(this, persona-byval, param2=0). Layout uncertain; try [this][persona 216][0]
    var cbm=new NativeFunction(B.add(0xd34da2),'void',['pointer','pointer','int']);
    // pass persona pointer (frida copies? no) - use direct: build arg buffer replicate byval via alloca not possible; try pointer
    cbm(mmThis,persona,0); send({g:"S3 CreateBotMatch returned"});
  }
 }catch(e){ send({g:"doWork EXC "+e+" "+e.stack}); }
}
function main(){
 var m=Process.findModuleByName("libil2cpp.so");if(!m){setTimeout(main,150);return;}
 B=m.base;
 try{setup(m);}catch(e){send({g:"setup err "+e});setTimeout(main,300);return;}
 function H(a,nm){try{Interceptor.attach(B.add(a),{onEnter:function(){send({g:"HIT "+nm});}});}catch(e){}}
 // S4: force offlineMode globally by patching the getter to return 1
 if(STRATEGY==4){ try{Interceptor.attach(B.add(0xd6bb99),{onLeave:function(r){r.replace(ptr(1));}}); send({g:"S4 forcing get_offlineMode=1"});}catch(e){send({g:"S4 hook err "+e});} }
 // capture Matchmaking singleton this from its Update
 try{Interceptor.attach(B.add(0xd3053c),{onEnter:function(){ if(mmThis.isNull()) mmThis=this.context.esp.readPointer(); }});}catch(e){}
 H(0xd31e77,"GetLocalPersona");H(0xd85fc5,"CreateMatch");H(0xd34da2,"CreateBotMatch");
 H(0xe47cab,"GC.Awake(ARENA)");H(0xe4bd6a,"SpawnPlayers(ARENA)");H(0xe5a928,"AssemblePlayerList");
 try{Interceptor.attach(B.add(0x7947d4),{onEnter:function(){var sp=this.context.esp;send({g:"LoadSceneAsync "+sstr(sp.add(4).readPointer())});}});}catch(e){}
 try{Interceptor.attach(B.add(0x100382f),{onEnter:function(){ frames++; if(frames==300&&!done){done=true; send({g:"=== doWork strategy "+STRATEGY+" ==="}); doWork();}}});}catch(e){}
 send({g:"__ready S"+STRATEGY});
}
main();
"""

def run_strategy(strat, wait=35):
    log("==== STRATEGY", strat, "launch ====")
    emu.sh("shell","am","force-stop","se.foglo.svt"); time.sleep(1)
    emu.sh("shell","input","keyevent","3"); time.sleep(1)
    emu.sh("shell","monkey","-p","se.foglo.svt","-c","android.intent.category.LAUNCHER","1")
    time.sleep(30)
    try:
        dev=frida.get_usb_device(timeout=10)
        out=subprocess.run([emu.ADB,"shell","pidof","se.foglo.svt"],capture_output=True,text=True).stdout.split()
        if not out: log("STRATEGY",strat,"NO PID"); return
        pid=int(out[0]); log("pid",pid)
        s=dev.attach(pid)
        signals={"arena":False}
        def on_msg(m,d):
            if isinstance(m.get("payload"),dict):
                g=m["payload"].get("g","")
                log("  ",g)
                if "ARENA" in g or ("LoadSceneAsync" in g and "menu" not in g.lower()):
                    signals["arena"]=True
            elif m.get("type")=="error":
                log("  JSERR",m.get("description"))
        sc=s.create_script(JS_TEMPLATE.replace("__STRAT__",str(strat)))
        sc.on("message",on_msg); sc.load()
        global SHOT
        def shot(tag):
            global SHOT
            try:
                emu.screenshot(); dst="/root/ants/shots/S%d_r%d_%s_%03d.png"%(strat,ROUND,tag,SHOT); SHOT+=1
                shutil.copy("/root/ants/_emu.png",dst); log("  shot ->",dst)
            except Exception as e: log("shot err",e)
        if strat==4:
            # real flow forced offline: tap SNIPER then GO, then observe the match over time
            time.sleep(12); log("S4 tap SNIPER(700,730)"); emu.tap(700,730); time.sleep(3); shot("afterSNIPER")
            time.sleep(4); log("S4 tap GO(1355,960)"); emu.tap(1355,960)
            for k in range(8):   # ~120s observation, screenshot every 15s to catch thieves spawning
                time.sleep(15); shot("t%d"%(k*15+15))
        else:
            time.sleep(wait); shot("end")
        log("STRATEGY",strat,"VERDICT:", "ARENA/SCENE-LOAD ***" if signals["arena"] else "no arena")
        try: sc.unload(); s.detach()
        except: pass
    except Exception as e:
        log("STRATEGY",strat,"RUN EXC",e, traceback.format_exc())

SHOT=0
ROUND=0
def main():
    global ROUND
    import os; os.makedirs("/root/ants/shots",exist_ok=True)
    open(LOG,"a").write("\n\n===== autopie run %s =====\n"%datetime.datetime.now())
    log("autonomous runner start (S4-focused: force offline + real SNIPER->GO, observe match)")
    while ROUND<8:
        ROUND+=1
        log("##### ROUND",ROUND,"#####")
        try: run_strategy(4)
        except Exception as e: log("strat loop exc",e)
        log("##### ROUND",ROUND,"done #####")
    log("autonomous runner FINISHED all rounds")

if __name__=="__main__":
    main()
