#!/usr/bin/env python3
"""Live read-only probe: did the pushed LSX <Login> event actually land?

READ-ONLY. Never writes to the game. Safe to run against the live FIFA17.exe
while the main session drives it.

Watches, once per second:

  OriginMgr.m_isLoggedIn   = *(u8)( *[0x1448acf50] + 0x13 )
        Set to 1 by the Origin event dispatcher @0x146f1e060 case 2 (verified:
        `cmp DWORD PTR [r9],1` -> `mov BYTE PTR [rcx+0x13],1`), which is reached
        only from a server-pushed <Event sender="LOGIN_EVENT"><Login
        IsLoggedIn="true"/>.  0 -> 1 means the push was dispatched.

        CAVEAT (verify: will-it-reach-login): this byte is a PROXY, not the
        decisive consumer.  It is written on the SAME dispatcher line that then
        falls into the FE re-broadcast loop @0x146f1e116 -> callback 0x147350e30
        (packs the event tagged 0xdea12004 and republishes on FIFA's FE event
        bus).  Nothing downstream READS +0x13; the FE broadcast is what actually
        propagates.  So treat 0 -> 1 as "the frame was accepted", and confirm
        real propagation with a gdb breakpoint on 0x147350e30 (bytes 48 83 ec 58).
        Also note LoginStatePCLogin's own gate is a DIFFERENT object
        ([0x144b86bf8]->vtbl+0x60), so this flag flipping does not guarantee
        GetAuthCode fires.

  OriginMgr.m_loginError   = *(u32)( *[0x1448acf50] + 0x14 )
        Cleared to 0 by the same code path.

  origin "online" byte     = *(u8)[0x1448a3ac0]
        INIT-SET, NOT DIAGNOSTIC: OriginMgr::Initialize writes this to 1
        unconditionally @0x146f340e1 (`mov BYTE PTR [rip+...],0x1`), so it does
        NOT reflect GetInternetConnectedState.  Shown for reference only; do not
        read it as a live state field.

  FirstPartyAuthTokenRetriever slots
        retriever = *[0x1448a3b20] + 0x4e98 ; slots at +0x08 and +0x10
        DoTick @0x146f199c0 walks these two; if both stay 0 no auth-code
        request was ever enqueued and RequestAuthCodeSync @0x1470db3c0 is
        never called. Non-zero here = GetAuthCode is imminent.

Usage:  python3 origin_login_probe.py [seconds]
"""
import glob
import struct
import sys
import time

ORIGIN_MGR_PP = 0x1448acf50      # -> OriginMgr*
ORIGIN_ONLINE_BYTE = 0x1448a3ac0  # FIFA's separate "origin online" flag
SDK_PP = 0x1448a3b20             # -> OriginSDK*, retriever at +0x4e98
RETRIEVER_OFF = 0x4e98


def find_pid():
    for d in glob.glob("/proc/[0-9]*"):
        try:
            if open(d + "/comm").read().strip() == "FIFA17.exe":
                return int(d.rsplit("/", 1)[-1])
        except OSError:
            pass
    return None


class Mem(object):
    def __init__(self, pid):
        self.f = open("/proc/%d/mem" % pid, "rb")

    def rd(self, va, n):
        try:
            self.f.seek(va)
            b = self.f.read(n)
            return b if b and len(b) == n else None
        except OSError:
            return None

    def u8(self, va):
        b = self.rd(va, 1)
        return None if b is None else b[0]

    def u32(self, va):
        b = self.rd(va, 4)
        return None if b is None else struct.unpack("<I", b)[0]

    def u64(self, va):
        b = self.rd(va, 8)
        return None if b is None else struct.unpack("<Q", b)[0]


def hx(v):
    return "??" if v is None else ("%#x" % v)


def main():
    limit = float(sys.argv[1]) if len(sys.argv) > 1 else 1e9
    pid = find_pid()
    if pid is None:
        print("no FIFA17.exe running")
        return 1
    print("pid", pid)
    m = Mem(pid)
    t0 = time.time()
    last = None
    while time.time() - t0 < limit:
        mgr = m.u64(ORIGIN_MGR_PP)
        logged = m.u8(mgr + 0x13) if mgr else None
        err = m.u32(mgr + 0x14) if mgr else None
        online = m.u8(ORIGIN_ONLINE_BYTE)
        sdk = m.u64(SDK_PP)
        r = (sdk + RETRIEVER_OFF) if sdk else None
        s1 = m.u64(r + 0x08) if r else None
        s2 = m.u64(r + 0x10) if r else None
        row = (logged, err, online, s1, s2)
        if row != last:
            print("[%s] OriginMgr=%s m_isLoggedIn=%s loginError=%s "
                  "onlineByte=%s(init-set) | authSlots=%s,%s"
                  % (time.strftime("%H:%M:%S"), hx(mgr), logged, hx(err),
                     online, hx(s1), hx(s2)))
            # Fire on any non-1 -> 1 transition (including the very first sample
            # where OriginMgr was still null and last[0] was None), so a
            # None -> 1 flip is not silently missed.
            if last is not None and last[0] != 1 and logged == 1:
                print("  *** m_isLoggedIn -> 1 : the pushed <Login> event was "
                      "DISPATCHED (proxy signal; confirm FE re-broadcast at "
                      "0x147350e30). Watch for LSX GetAuthCode next. ***")
            if last is not None and not (last[3] or last[4]) and (s1 or s2):
                print("  *** auth-code request ENQUEUED into "
                      "FirstPartyAuthTokenRetriever. ***")
            last = row
        time.sleep(1.0)
    return 0


if __name__ == "__main__":
    sys.exit(main())
