"""D2 QUICK SELL, batch 9: the wallet. Q2 assign-vs-add, final attempt.

PLAN. Find the credit-carrying response classes and their deserialisers:
    RS4:FutUserCreditsServerResponse    @ 0x18021dc18
    RS4:FutUpdateCreditsServerResponse  @ 0x18022cc10
    RS4:FutDiscardCardServerResponse    @ 0x180220540 (vtable 0x180220488, deser 0x180127300)
Resolve each by find_all(b"RS4:"+name) then xrefs_to(hit-4) -> factory -> the
.rdata vtable it installs -> slot +0x08. Decompile all three deserialisers and
every virtual they invoke on the FUT manager singleton, so the wallet field and
its writers are visible. Then enumerate every writer of that field.

CONTROL: the discard chain must resolve to 0x180127300, which is already known
independently (qword 0x180127300 sits at 0x180220490 = vtable+0x08). If the same
mechanism yields a plausible deser for the two credits classes, it is working.
"""
import traceback, os, struct

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/store/qs/"


def dump(tag, va, path=None):
    try:
        src = dec(va)
    except Exception as e:
        src = "// threw %r" % (e,)
    print("=" * 78)
    print("%s %#x fname=%s len(src)=%d (FULL)" % (tag, va, fname(va), len(src)))
    print("=" * 78)
    print(src)
    if path:
        open(OUT + path, "w").write(src)
    return src


def resolve(name):
    out = []
    for h in find_all(b"RS4:" + name.encode() + b"\x00"):
        for frm, typ, fn, ent in xrefs_to(h - 4):
            if ent:
                out.append((h, frm, ent, fn))
    return out


try:
    for nm in ("FutDiscardCardServerResponse", "FutUserCreditsServerResponse",
               "FutUpdateCreditsServerResponse"):
        print("##### %s #####" % nm)
        for h, frm, ent, fn in resolve(nm):
            print("  literal %#x  factory %#x %s (ref at %#x)" % (h, ent, fn, frm))
            src = dump("factory", ent, "qs_r_fac_%x.txt" % ent)
            # find the PTR_FUN_ vtable it installs
            import re
            for m in re.finditer(r"PTR_FUN_([0-9a-f]+)", src):
                vt = int(m.group(1), 16)
                print("    vtable %#x, slot+0x08 = %#x %s"
                      % (vt, qword(vt + 8), fname(qword(vt + 8))))
                dump("deser", qword(vt + 8), "qs_r_deser_%x.txt" % qword(vt + 8))
                for off, tgt, fnm in vtable(vt, 24):
                    print("      +%#05x %#x %s" % (off, tgt, fnm))
        print()

    print("##### the FUT manager slots used by the discard deser #####")
    # the deser calls (**(code**)(*plVar4 + 0xa30))(plVar4, id) after parsing an id
    # find every function that calls a virtual at +0xa30 / +0xa08 / +0xa48 and the
    # ones that call slots near them, so a credit setter can be spotted by name.
    it = fm.getFunctions(True)
    want = ("0xa08", "0xa30", "0xa48", "0x9f8", "0xa00", "0xa10", "0xa18", "0xa20",
            "0xa28", "0xa38", "0xa40", "0xa50", "0xa58", "0xa60")
    tally = {}
    while it.hasNext():
        f = it.next()
        ii = listing.getInstructions(f.getBody(), True)
        got = []
        while ii.hasNext():
            i = ii.next()
            t = str(i)
            if t.startswith("CALL qword ptr [") and any(w in t for w in want):
                got.append((int(i.getAddress().getOffset()), t))
        if got:
            tally[int(f.getEntryPoint().getOffset())] = (f.getName(), got)
    print("  %d functions call one of those slots" % len(tally))
    for e in sorted(tally):
        nm, got = tally[e]
        print("   %#x %s" % (e, nm))
        for a, t in got:
            print("        %#x %s" % (a, t))

except Exception:
    traceback.print_exc()
