"""D3 run 4: close the consumer set for the pack record.

WHAT RUN 3 SETTLED
  Of everything in .text that touches +0x144..+0x154 as a memory displacement,
  only four functions belong to the 0x158-stride pack record:
    0x1801342d0 ctor (zeroes them)          0x18013af30 deser (writes them)
    0x180133210 uninitialised_copy (0x158)  0x1801340e0 copy-assign
  The rest were offset collisions on unrelated structs, proven by their stride or
  their size: 0x180133af0 iterates with stride 0x168, 0x180134b50 copies out to
  +0x163, 0x180173e00's object extends to +0x2f8 and sums 0x148+0x14c+0x150 as a
  win/draw/loss total.

WHAT THIS RUN DOES
  1. The 0x158 STRIDE CENSUS. Any loop over the pack vector must advance a pointer
     by 0x158 or multiply an index by it. Enumerate every instruction that uses
     0x158 in pointer arithmetic (ADD/LEA/IMUL on a register), not as a stack frame
     size. Control: 0x180133210 and 0x18013af30 must both appear.
  2. Locate the FutStoreGetPackTypesServerResponse vtable by searching .rdata for
     the deserializer pointer 0x1801234e0, dump it, and take xrefs to the vtable so
     the owner class and any accessor are visible. (The factory and the deser have
     zero direct callers, so they are dispatched through this vtable.)
  3. Complete caller closure over the record's lifecycle functions: anything that
     can own a pack record must construct, copy or destroy one.
  4. CreatePack side: numberItems store offset, and every reader of it, to answer
     whether the reveal is sized from a declared count or from the actual list.
"""
import traceback, sys, os

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres/"
os.makedirs(OUT, exist_ok=True)


def dump(tag, va, path, echo=True):
    src = dec(va)
    hdr = "%s  %#x  fname=%s  len(src)=%d  (FULL, NOT TRUNCATED)" % (
        tag, va, fname(va), len(src))
    if echo:
        print("=" * 78)
        print(hdr)
        print("=" * 78)
        print(src)
    with open(path, "w") as fh:
        fh.write("// " + hdr + "\n" + src)
    return src


def insn_at(h):
    for back in range(0, 14):
        i2 = listing.getInstructionContaining(addr(h - back))
        if i2 is not None:
            return i2
    return None


try:
    print("### 1. 0x158 STRIDE CENSUS (pointer arithmetic only, not frame sizes)")
    pat = bytes([0x58, 0x01, 0x00, 0x00])
    seen = set()
    keep = []
    for h in find_all(pat, blocks=(".text",)):
        ins = insn_at(h)
        if ins is None:
            continue
        a = int(ins.getAddress().getOffset())
        if a in seen:
            continue
        seen.add(a)
        t = str(ins)
        if "0x158" not in t:
            continue
        mn = t.split()[0]
        if mn in ("SUB", "ADD") and t.split()[1].startswith("RSP"):
            continue          # stack frame
        if mn in ("ADD", "LEA", "IMUL", "MOV", "CMP", "SHL"):
            keep.append((a, fname(a), t))
    byfn = {}
    for a, fn, t in keep:
        byfn.setdefault(fn, []).append((a, t))
    print("  %d instruction(s) in %d function(s)" % (len(keep), len(byfn)))
    for fn in sorted(byfn):
        print("    %s" % fn)
        for a, t in byfn[fn]:
            print("        %#x  %s" % (a, t))
    print("  CONTROL: 0x180133210 present=%s   0x18013af30 present=%s"
          % ("FUN_180133210" in byfn, "FUN_18013af30" in byfn))

    print("\n### 2. STORE RESPONSE VTABLE")
    dp = (0x1801234e0).to_bytes(8, "little")
    for h in find_all(dp, blocks=(".rdata", ".data")):
        print("  deser pointer 0x1801234e0 found in .rdata/.data at %#x" % h)
        for base in (h - 8, h - 0x10, h):
            print("    candidate vtable base %#x:" % base)
            for off, tgt, nm in vtable(base, 14):
                print("      +%#04x  %#018x  %s" % (off, tgt, nm))
            break
        for frm, typ, fn, ent in xrefs_to(h - 8):
            print("    xref to (vtbl base %#x): %#x %s %s" % (h - 8, frm, typ, fn))
        for frm, typ, fn, ent in xrefs_to(h):
            print("    xref to (slot itself %#x): %#x %s %s" % (h, frm, typ, fn))

    print("\n### 3. CALLER CLOSURE OVER PACK-RECORD LIFECYCLE")
    LIFE = {0x1801342d0: "record ctor", 0x1801340e0: "copy-assign",
            0x180133210: "uninit_copy(0x158)", 0x180132180: "vector grow",
            0x1801232a0: "record dtor", 0x18013af30: "element deser"}
    lvl1 = {}
    for va, tag in LIFE.items():
        cs = callers(va)
        print("  %#x %-20s callers: %s" % (va, tag, [(hex(a), n) for a, n in cs]))
        for a, n in cs:
            lvl1.setdefault(a, set()).add(tag)
    print("\n  level-2 (callers of those callers):")
    for a in sorted(lvl1):
        if a in LIFE:
            continue
        print("    %#x %-20s via %s ; its callers: %s"
              % (a, fname(a), sorted(lvl1[a]), [(hex(x), n) for x, n in callers(a)]))
    print("\n  full decompiles of every non-lifecycle caller:")
    for a in sorted(lvl1):
        if a in LIFE:
            continue
        dump("LIFECYCLE CALLER", a, OUT + "d3_life_caller_%x.txt" % a, echo=True)

    print("\n### 4. CREATEPACK numberItems")
    src = dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=True)
    for va, tag in ((0x180162880, "createpack deser"),):
        pass
    dpc = (0x180162880).to_bytes(8, "little")
    for h in find_all(dpc, blocks=(".rdata", ".data")):
        print("  createpack deser pointer at %#x (vtable slot)" % h)
        for off, tgt, nm in vtable(h - 8, 12):
            print("      +%#04x  %#018x  %s" % (off, tgt, nm))
        for frm, typ, fn, ent in xrefs_to(h - 8):
            print("    xref to vtbl base: %#x %s %s" % (frm, typ, fn))
except Exception:
    traceback.print_exc()
    sys.stdout.flush()
