#!/usr/bin/env python3
"""Runtime revival harness for SvT.

Attaches Frida and applies the minimal instrumentation needed to get the
discontinued client past its boot gates against our reimplemented server:

  1. ServerMessenger.get_sessionId  -> never empty  (unblocks the analytics-
     ready gate that stalls boot; the real handshake carried a session id we
     cannot fully reconstruct from the parse tree).
  2. GDPRHelper.get_TermsAccepted / get_FacebookTermsAccepted -> true
     (suppresses the Terms-of-Use popup, which we otherwise cannot dismiss
     because the windowed emulator does not deliver touches into Unity).

Everything else — account creation, dataset sync, scene loads — is driven by
the real client talking to the real (reimplemented) protocol.
"""
import json, sys, time, subprocess, frida

ADB = "/opt/android-sdk/platform-tools/adb"
A = json.load(open("/root/ants/sid_addr.json"))

JS = r"""
var A = %s;
function ins(){
  var m = Process.findModuleByName("libil2cpp.so"); if(!m) return false;
  var b = m.base;
  var e = m.findExportByName ? m.findExportByName("il2cpp_string_new")
                             : Module.getGlobalExportByName("il2cpp_string_new");
  var strnew = new NativeFunction(e, 'pointer', ['pointer']);
  send({t:"info", m:"hooks installing"});

  // 1. keep sessionId non-empty
  Interceptor.attach(b.add(A["get_sessionId"]), { onLeave:function(rv){
    var empty = true;
    try { if(!rv.isNull()){ empty = (rv.add(8).readS32() <= 0); } } catch(e){}
    if(empty){ rv.replace(strnew(Memory.allocUtf8String("revived-session-0001"))); }
  }});

  // 2. force terms-of-use accepted (bool return -> 1)
  ["get_TermsAccepted","get_FacebookTermsAccepted"].forEach(function(k){
    if(A[k]) Interceptor.attach(b.add(A[k]), { onLeave:function(rv){ rv.replace(ptr(1)); } });
  });

  // 3. no-op Check25RoundsPlayed: it dereferences a Stats object that is still
  // null during profile load and NREs, which aborts PlayerProfile.Start and
  // keeps the title from exiting to the vault. It is analytics-only.
  if(A["Check25RoundsPlayed"]){
    Interceptor.replace(b.add(A["Check25RoundsPlayed"]),
      new NativeCallback(function(self){ }, 'void', ['pointer']));
  }

  send({t:"info", m:"hooks installed"});
  return true;
}
if(!ins()){ var iv=setInterval(function(){ if(ins()) clearInterval(iv); }, 150); }
""" % json.dumps(A)

dev = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
subprocess.run([ADB, "shell", "am", "force-stop", "se.foglo.svt"])
subprocess.run([ADB, "shell", "monkey", "-p", "se.foglo.svt", "-c",
                "android.intent.category.LAUNCHER", "1"],
               stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
pid = None
for _ in range(80):
    out = subprocess.run([ADB, "shell", "pidof", "se.foglo.svt"],
                         capture_output=True, text=True).stdout.strip()
    if out:
        pid = int(out.split()[0]); break
    time.sleep(0.25)
if pid is None:
    print("no process"); sys.exit(1)
print("attached pid", pid, flush=True)
session = dev.attach(pid)
script = session.create_script(JS)
script.on("message", lambda m, d: print("[msg]", m.get("payload") if m["type"] == "send" else m, flush=True))
script.load()
time.sleep(int(sys.argv[1]) if len(sys.argv) > 1 else 600)
