"""D3 Q1: where do the seven packContentInfo fields land, and what object owns them?

HYPOTHESIS: the pack element deser 0x18013af30 dispatches atom 0x20c
(packContentInfo) into a nested object sub-deser, which writes seven scalars into
a struct. A prior note (docs/plan-2026-08-04-blockers.md:201) claims the slots are
+0x144..+0x154 and that nothing in cardsdll reads them back. Verify the offsets
first-hand and find the sub-deser.

CONTROL: class_deser("FutSquadSave") must return 0x180171a60 and
class_deser("FutSquadList") must return 0x180172140. If those come back empty the
whole batch is suspect.

OUTPUT: full decompiles (len printed, never truncated) + raw disassembly of the
pack element deser and of EVERY callee, so the store offsets are read off
instructions, not off the decompiler's guessed structure. Also scores each callee
by how many of the seven packContentInfo atom immediates (and their sub-ladder
deltas) it contains, so the nested sub-deser is identified mechanically.
"""
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)

PCI_ATOMS = {0x63: "bronzeQuantity", 0x2c6: "silverQuantity", 0x149: "goldQuantity",
             0x273: "rareQuantity", 0x170: "itemQuantity", 0x2e3: "start",
             0x35d: "unopened"}
# running-sum sub/dec ladder deltas between consecutive sorted atoms
_s = sorted(PCI_ATOMS)
PCI_DELTAS = {_s[i + 1] - _s[i] for i in range(len(_s) - 1)}


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


def insns(va, limit=200000):
    f = func(va)
    out = []
    if f is None:
        return out
    it = listing.getInstructions(f.getBody(), True)
    n = 0
    while it.hasNext() and n < limit:
        ins = it.next()
        out.append((int(ins.getAddress().getOffset()), str(ins)))
        n += 1
    return out


def disasm(va, path):
    lines = ["%#x  %s" % (a, s) for a, s in insns(va)]
    with open(path, "w") as fh:
        fh.write("\n".join(lines))
    return lines


def scalars(va):
    """set of every scalar immediate appearing in the function's instructions"""
    out = set()
    f = func(va)
    if f is None:
        return out
    it = listing.getInstructions(f.getBody(), True)
    while it.hasNext():
        ins = it.next()
        for i in range(ins.getNumOperands()):
            for o in ins.getOpObjects(i):
                try:
                    out.add(int(o.getValue()) & 0xFFFFFFFF)
                except Exception:
                    pass
    return out


try:
    print("### CONTROLS")
    for c, expect in (("FutSquadSave", 0x180171a60), ("FutSquadList", 0x180172140),
                      ("FutCreateMatch", 0x180120380)):
        r = class_deser(c)
        print("  %-16s -> %s   expect %#x  %s"
              % (c, [hex(x[0]) for x in r], expect,
                 "PASS" if any(x[0] == expect for x in r) else "FAIL/UNKNOWN"))

    PACK_DESER = 0x18013AF30
    src = dump("PACK ELEMENT DESER", PACK_DESER, OUT + "d3_pack_elem_deser.txt")

    print("\n### CALLERS OF PACK ELEMENT DESER")
    for a, n in callers(PACK_DESER):
        print("  %#x  %s" % (a, n))

    dl = disasm(PACK_DESER, OUT + "d3_pack_elem_deser.asm")
    print("\n### DISASM %d instructions -> d3_pack_elem_deser.asm" % len(dl))

    print("\n### CALLEES OF PACK ELEMENT DESER, scored for packContentInfo atoms")
    cand = []
    for a, n in callees(PACK_DESER):
        sc = scalars(a)
        hit_atoms = sorted(x for x in sc if x in PCI_ATOMS)
        hit_delta = sorted(x for x in sc if x in PCI_DELTAS)
        score = len(hit_atoms) + len(hit_delta)
        print("  %#x  %-28s  natoms=%d %s  ndelta=%d %s"
              % (a, n, len(hit_atoms), [hex(x) for x in hit_atoms],
                 len(hit_delta), [hex(x) for x in hit_delta]))
        cand.append((score, a, n))
        dump("CALLEE", a, OUT + "d3_callee_%x.txt" % a, echo=False)
        disasm(a, OUT + "d3_callee_%x.asm" % a)
    cand.sort(reverse=True)

    print("\n### CALL SITES INSIDE PACK ELEM DESER (address -> target)")
    for ad, s in dl:
        if s.startswith("CALL"):
            t = s.split()[-1]
            try:
                tv = int(t, 16)
                print("  %#x  %s   -> %s" % (ad, s, fname(tv)))
            except Exception:
                print("  %#x  %s" % (ad, s))

    print("\n### TOP CANDIDATE SUB-DESERS")
    for score, a, n in cand[:3]:
        print("  score=%d  %#x  %s" % (score, a, n))
    if cand and cand[0][0] >= 3:
        best = cand[0][1]
        s2 = dump("PACKCONTENTINFO SUB-DESER (best candidate)", best,
                  OUT + "d3_pci_subdeser.txt")
        d2 = disasm(best, OUT + "d3_pci_subdeser.asm")
        print("\n### FULL DISASM OF %#x (%d instructions)" % (best, len(d2)))
        for ln in d2:
            print("  " + ln)
except Exception:
    traceback.print_exc()
    sys.stdout.flush()
