"""D3 run 3: TIGHT reader scan + who consumes the store response object.

Run 2's disp32 byte scan was correct but too permissive: it accepted any operand
whose scalar equalled the offset, so `SUB RSP,0x150` counted. This run requires the
offset to appear as a MEMORY-OPERAND DISPLACEMENT (the instruction text must contain
"+ 0xNNN]") which is the only form a struct field access can take.

ESTABLISHED SO FAR (run 1 + run 2, both derivations agreeing):
  pack record size 0x158, ctor 0x1801342d0 zeroes +0x144/+0x14c(qwords)/+0x154(dword)
  itemQuantity +0x144, goldQuantity +0x148, silverQuantity +0x14c,
  bronzeQuantity +0x150, rareQuantity +0x154, state +0xb0, start +0xb4,
  useDefaultImage +0xcc, unopened +0xcd
  store root deser 0x1801234e0 puts the pack vector at responseObject+0x28,
  timestamp at responseObject+0x5c.

POSITIVE CONTROL (already passing in run 2, re-asserted here): the copy-assign
0x1801340e0 must show up reading AND writing +0x144, and the ctor 0x1801342d0 must
show up writing +0x144. If they do not, the scan is broken.

HYPOTHESES UNDER TEST
  H1  No function other than the record's own ctor/copy/dtor touches +0x144..+0x154.
  H2  Nothing counts an item list against those numbers (no function reads a quantity
      slot and also walks an item vector).
  H3  `start` +0xb4 and `unopened` +0xcd are likewise unread inside CardsDLL.
"""
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)

QTY = {0x144: "itemQuantity", 0x148: "goldQuantity", 0x14c: "silverQuantity",
       0x150: "bronzeQuantity", 0x154: "rareQuantity"}
OTHER = {0x0b4: "start", 0x0cd: "unopened", 0x0b0: "state"}
LIFECYCLE = {0x1801342d0: "record ctor", 0x1801340e0: "record copy-assign",
             0x180132180: "vector grow", 0x1801232a0: "record dtor",
             0x18013af30: "pack element deser"}


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_mem(val):
    """{func_entry: [(addr, text)]} for MEMORY accesses at displacement val"""
    pat = bytes([val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF])
    tag = "+ %#x]" % val
    out = {}
    seen = set()
    for h in find_all(pat, blocks=(".text",)):
        ins = None
        for back in range(0, 12):
            i2 = listing.getInstructionContaining(addr(h - back))
            if i2 is not None:
                ins = i2
                break
        if ins is None:
            continue
        a = int(ins.getAddress().getOffset())
        if a in seen:
            continue
        seen.add(a)
        txt = str(ins)
        if tag not in txt:
            continue
        f = fm.getFunctionContaining(ins.getAddress())
        key = int(f.getEntryPoint().getOffset()) if f else 0
        out.setdefault(key, []).append((a, txt))
    return out


try:
    print("### TIGHT MEMORY-DISPLACEMENT SCAN, .text, quantity slots")
    per_off = {}
    fn_offs = {}
    for off in sorted(QTY) + sorted(OTHER):
        m = scan_mem(off)
        per_off[off] = m
        nm = QTY.get(off) or OTHER.get(off)
        tot = sum(len(v) for v in m.values())
        print("\n  --- +%#05x %-16s : %d instruction(s) in %d function(s)"
              % (off, nm, tot, len(m)))
        for k in sorted(m):
            fn_offs.setdefault(k, set()).add(off)
            print("      %#x %-20s" % (k, fname(k) if k else "?"))
            for a, t in m[k]:
                print("          %#x  %s" % (a, t))

    print("\n### CONTROL: lifecycle functions must appear for the quantity slots")
    for va, tag in LIFECYCLE.items():
        got = sorted(fn_offs.get(va, []))
        print("  %#x %-22s offsets seen: %s  %s"
              % (va, tag, [hex(x) for x in got],
                 "PASS" if got else "absent"))

    print("\n### FUNCTIONS TOUCHING >=2 DISTINCT QUANTITY SLOTS (candidate consumers)")
    cands = []
    for k, offs in sorted(fn_offs.items()):
        q = sorted(o for o in offs if o in QTY)
        if len(q) >= 2:
            cands.append((k, q))
            print("  %#x %-22s %s  %s"
                  % (k, fname(k), [hex(x) for x in q],
                     "(lifecycle)" if k in LIFECYCLE else "<== NON-LIFECYCLE"))

    print("\n### FUNCTIONS TOUCHING EXACTLY ONE QUANTITY SLOT")
    for k, offs in sorted(fn_offs.items()):
        q = sorted(o for o in offs if o in QTY)
        if len(q) == 1:
            print("  %#x %-22s %s %s" % (k, fname(k), [hex(x) for x in q],
                                         "(lifecycle)" if k in LIFECYCLE else ""))

    print("\n### DECOMPILE EVERY NON-LIFECYCLE FUNCTION THAT TOUCHES ANY QUANTITY SLOT")
    for k, offs in sorted(fn_offs.items()):
        if k in LIFECYCLE or k == 0:
            continue
        if not any(o in QTY for o in offs):
            continue
        dump("QTY TOUCHER offs=%s" % [hex(x) for x in sorted(offs)], k,
             OUT + "d3_qty_%x.txt" % k, echo=True)

    print("\n### WHO CONSUMES THE STORE RESPONSE OBJECT (vector at +0x28)")
    fac = 0x180123480
    dump("store response factory", fac, OUT + "d3_store_factory.txt", echo=True)
    print("  callers of factory:")
    for a, n in callers(fac):
        print("    %#x %s" % (a, n))
    print("  callers of deser 0x1801234e0:")
    for a, n in callers(0x1801234e0):
        print("    %#x %s" % (a, n))

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