"""D3 Q2/Q3/Q4: who READS the pack record's packContentInfo slots, `start` and
`unopened`, and does anything count the delivered itemList against them?

ESTABLISHED IN RUN 1 (q_pack_content_1.py, d3_pack_elem_deser.asm), twice over --
once from the decompiler's frame locals and once from raw disassembly:
  pack element deser 0x18013af30, record base = RSP+0x50 = RBP-0xB0, record size 0x158
  itemQuantity   0x170 -> [RBP+0x94] -> rec +0x144
  goldQuantity   0x149 -> [RBP+0x98] -> rec +0x148
  silverQuantity 0x2c6 -> [RBP+0x9c] -> rec +0x14c
  bronzeQuantity 0x63  -> [RBP+0xa0] -> rec +0x150
  rareQuantity   0x273 -> [RBP+0xa4] -> rec +0x154
  state          0x2eb -> [RBP+0x00] -> rec +0x0b0
  start          0x2e3 -> [RBP+0x04] -> rec +0x0b4   (INT via 0x1800d7b30)
  useDefaultImage0x36a -> [RBP+0x1c] -> rec +0x0cc   (inverted)
  unopened       0x35d -> [RBP+0x1d] -> rec +0x0cd   (BOOL, stored raw)
`start` and `unopened` are TOP-LEVEL pack keys, NOT packContentInfo children.

HYPOTHESIS: nothing in CardsDLL reads +0x144..+0x154 back.

WHY A BYTE SCAN IS SOUND HERE: every offset of interest is >= 0x80, so x86 cannot
encode it as a signed disp8. Any instruction touching one of these slots must carry
the literal disp32 little-endian bytes. So a raw .text scan for those 4 bytes is an
EXHAUSTIVE upper bound on the set of candidate accesses; each hit is then confirmed
by asking Ghidra for the instruction containing it and checking the scalar.

POSITIVE CONTROL FOR THE SCAN: the record copy-assign 0x1801340e0 and the
push_back 0x180132180 must move all 0x158 bytes. If they copy field-by-field the
scan MUST list them; if the scan returns nothing at all for every offset including
theirs, the scan is broken, not the binary. Second control: the stride 0x158 must be
found in 0x18013af30 itself (the /0x158 count check) and in 0x180132180.
"""
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)

REC = {0x144: "itemQuantity", 0x148: "goldQuantity", 0x14c: "silverQuantity",
       0x150: "bronzeQuantity", 0x154: "rareQuantity",
       0x0b0: "state", 0x0b4: "start", 0x0cc: "useDefaultImage", 0x0cd: "unopened",
       0x158: "STRIDE/record-size"}


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 scan_disp(val):
    """every .text instruction carrying `val` as a literal 4-byte scalar"""
    pat = bytes([val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF])
    seen = {}
    for h in find_all(pat, blocks=(".text",)):
        ins = None
        for back in range(0, 12):
            try:
                i2 = listing.getInstructionContaining(addr(h - back))
            except Exception:
                i2 = None
            if i2 is not None:
                ins = i2
                break
        if ins is None:
            continue
        ok = False
        for i in range(ins.getNumOperands()):
            for o in ins.getOpObjects(i):
                try:
                    if (int(o.getValue()) & 0xFFFFFFFF) == val:
                        ok = True
                except Exception:
                    pass
        if not ok:
            continue
        a = int(ins.getAddress().getOffset())
        seen[a] = (fname(a), str(ins))
    return seen


try:
    print("### CONTROL A: does the RS4 machinery work in THIS project copy?")
    for nm in ("FutSquadSaveServerResponse", "FutStoreGetPackTypesServerResponse"):
        hits = find_all(b"RS4:" + nm.encode())
        print("  RS4:%-38s literal hits=%s" % (nm, [hex(x) for x in hits]))
        for h in hits:
            xs = xrefs_to(h)
            print("      xrefs to literal %#x: %s" % (h, [(hex(f), t, n) for f, t, n, e in xs]))
    for c, expect in (("FutSquadSave", 0x180171a60), ("FutSquadList", 0x180172140),
                      ("FutCreateMatch", 0x180120380)):
        r = class_deser(c)
        print("  class_deser(%-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"))

    print("\n### RECORD LIFECYCLE FUNCTIONS (full decompiles -> files)")
    for va, tag in ((0x1801342d0, "record ctor"), (0x1801340e0, "record copy-assign"),
                    (0x180132180, "vector push_back/grow"), (0x1801232a0, "record dtor"),
                    (0x1800d7af0, "int conv A (quantities)"),
                    (0x1800d7b30, "int conv B (start,bonus)"),
                    (0x1800d7b10, "int conv C (id, 16-bit)")):
        s = dump(tag, va, OUT + "d3_life_%x.txt" % va, echo=False)
        print("  %#x  %-26s len=%d -> d3_life_%x.txt" % (va, tag, len(s), va))

    print("\n### EXHAUSTIVE disp32 SCAN OF .text")
    allhits = {}
    for off in sorted(REC):
        s = scan_disp(off)
        allhits[off] = s
        print("\n  --- offset %#05x  (%s):  %d confirmed instruction(s)"
              % (off, REC[off], len(s)))
        byfn = {}
        for a, (fn, txt) in sorted(s.items()):
            byfn.setdefault(fn, []).append((a, txt))
        for fn in sorted(byfn):
            print("      %s" % fn)
            for a, txt in byfn[fn]:
                print("        %#x  %s" % (a, txt))

    print("\n### VERDICT INPUT: functions touching ANY quantity slot")
    q = set()
    for off in (0x144, 0x148, 0x14c, 0x150, 0x154):
        for a, (fn, txt) in allhits[off].items():
            q.add(fn)
    print("  ", sorted(q) if q else "NONE")

    print("\n### STORE ROOT DESER 0x1801234e0 AND ITS CALLERS")
    dump("store root deser", 0x1801234e0, OUT + "d3_store_root.txt", echo=True)
    for a, n in callers(0x1801234e0):
        print("  CALLER %#x %s" % (a, n))
        dump("caller of store root", a, OUT + "d3_storeroot_caller_%x.txt" % a, echo=True)

    print("\n### FutCreatePackServerResponse deser 0x180162880 (itemList / numberItems)")
    dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=True)
    for a, n in callers(0x180162880):
        print("  CALLER %#x %s" % (a, n))
except Exception:
    traceback.print_exc()
    sys.stdout.flush()
