#!/usr/bin/env python3
"""Force PlayerProfile to initialise.

Root cause established at runtime: PlayerProfile is a MonoBehaviour singleton
whose GameObject never activates, so Awake() never runs, so the `instance`
static is never set and get_instance() returns null forever -> boot stalls.

This captures the PlayerProfile `this` pointer from its .ctor and manually
invokes Awake(this) via NativeFunction, so the singleton registers itself and
subscribes to Dataset.onSectionChange. Then the (delayed) SyncReply's section
changes reach it and the profile loads.

IL2CPP x86 instance-method ABI: cdecl, first arg = `this`, last arg = a
MethodInfo* (passed 0 here; non-generic methods ignore it).
"""
import json
import sys
import time
import frida

A = json.load(open("/root/ants/force_addrs.json"))

JS = r"""
var A = %s;
var base = null;
var ppThis = null;
var awoken = false;

function rvaName(rva) {
    for (var k in A) if (A[k] === rva) return k;
    return "0x" + rva.toString(16);
}

function install() {
    var m = Process.findModuleByName("libil2cpp.so");
    if (m === null) return false;
    base = m.base;

    var awakeAddr = base.add(A["PlayerProfile::Awake/0"]);
    var Awake = new NativeFunction(awakeAddr, 'void', ['pointer', 'pointer']);

    // capture `this` from the constructor
    Interceptor.attach(base.add(A["PlayerProfile::.ctor/0"]), {
        onEnter: function (args) {
            ppThis = args[0];
            send({t: "log", m: "PlayerProfile.ctor this=" + ppThis});
        }
    });

    // observe the methods that should now start firing
    ["PlayerProfile::Awake/0", "PlayerProfile::OnSectionChange/2",
     "PlayerProfile::LoadProfile/1", "FE.EnterVault::Awake/0",
     "FE.EnterVault::Show/0", "FE.Director::EnterMenuScene/2"].forEach(function (n) {
        Interceptor.attach(base.add(A[n]), {
            onEnter: function () { send({t: "hit", n: n}); }
        });
    });

    // once we have `this`, force Awake shortly after (well before SyncReply)
    var iv = setInterval(function () {
        if (ppThis !== null && !awoken) {
            awoken = true;
            try {
                send({t: "log", m: ">>> forcing PlayerProfile.Awake(" + ppThis + ")"});
                Awake(ppThis, ptr(0));
                send({t: "log", m: ">>> Awake returned OK"});
            } catch (e) {
                send({t: "log", m: "!!! Awake threw: " + e});
            }
            clearInterval(iv);
        }
    }, 50);

    send({t: "log", m: "installed, base=" + base});
    return true;
}

if (!install()) {
    var iv2 = setInterval(function () { if (install()) clearInterval(iv2); }, 100);
}
"""


def main():
    dev = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
    pid = dev.spawn(["se.foglo.svt"])
    session = dev.attach(pid)
    script = session.create_script(JS % json.dumps(A))
    hits = {}

    def on_message(msg, data):
        if msg["type"] != "send":
            print("!!", msg.get("description", msg)); return
        p = msg["payload"]
        if p["t"] == "hit":
            hits[p["n"]] = hits.get(p["n"], 0) + 1
            if hits[p["n"]] <= 2:
                print("%7.2fs  HIT  %s" % (time.time() - t0, p["n"]))
        else:
            print("%7.2fs  %s" % (time.time() - t0, p["m"]))

    script.on("message", on_message)
    script.load()
    t0 = time.time()
    dev.resume(pid)
    time.sleep(int(sys.argv[1]) if len(sys.argv) > 1 else 40)
    print("\n=== hit counts ===")
    for n, c in sorted(hits.items(), key=lambda x: -x[1]):
        print("  %-38s %d" % (n, c))


if __name__ == "__main__":
    main()
