"""Q: who READS the parsed item record's discard fields, item+0x38 (the wire
discardValue) and item+0x3c (the client's own fcc_discardcoins result)?

WHY q2's CONTROL FAILED, and why that was the control's fault: inside
FUN_18013fe00 the item is a STACK STRUCT at RBP+0x160, so the guard reads
[RBP+0x198] and the store writes [RBP+0x19c]. A scan for the displacements 0x38
and 0x3c can never see them. The control was invalid, not the scan.

NEW METHOD -- FINGERPRINT THE STRUCT, NOT THE OFFSET. The item record has several
displacements that are rare in general code: +0x146 (preferredPosition, u16),
+0x148 (nation), +0x154 (leagueId), +0x94 (teamid), +0xb4 (rating). Any function
that dereferences a pointer at two or more of those is handling an item record.
Collect the displacement set per function from the instruction text, select the
item handlers, and then report their +0x38 / +0x3c usage.

SECOND TEST, independent of the fingerprint: find every place in .text where a
dword is READ at [reg+0x38] and, within 0x40 bytes and off the SAME base
register, a dword is READ at [reg+0x3c]. That is the shape of a "server value
else computed value" selector. H1 (consumer reads +0x38 only) predicts no such
selector on an item; H2 predicts one.

CONTROL for this run: the fingerprint must select FUN_18013fe00 itself when the
frame register RBP is allowed, because that function demonstrably touches
RBP+0x2a6 (0x146+0x160), RBP+0x2a8, RBP+0x2b4 and RBP+0x214. I print the
frame-relative fingerprint hits separately for exactly that reason.
"""
import re
import traceback

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/cards/q3_out.txt"
RE_MEM = re.compile(r"\[(R[A-Z0-9]+) \+ (-?0x[0-9a-f]+)\]")

try:
    lines = []
    def P(*a):
        lines.append(" ".join(str(x) for x in a))

    ITEM_MARKS = (0x146, 0x148, 0x154, 0x94, 0xb4, 0x8c)
    FRAME_MARKS = tuple(m + 0x160 for m in ITEM_MARKS)

    per_fn = {}           # entry -> {'name', 'disp': {d: [(addr, base, txt)]}}
    reads38 = []          # (addr, base, fnentry, txt)
    reads3c = []
    n_ins = 0

    it = listing.getInstructions(True)
    while it.hasNext():
        ins = it.next()
        n_ins += 1
        txt = str(ins)
        if "[" not in txt:
            continue
        ms = RE_MEM.findall(txt)
        if not ms:
            continue
        a = int(ins.getAddress().getOffset())
        f = fm.getFunctionContaining(ins.getAddress())
        ent = int(f.getEntryPoint().getOffset()) if f else 0
        rec = per_fn.setdefault(ent, {"name": f.getName() if f else "?", "disp": {}})
        for base, dtxt in ms:
            d = int(dtxt, 16)
            rec["disp"].setdefault(d, []).append((a, base, txt))
            if d == 0x38 and txt.startswith("MOV E") and "dword ptr [" + base in txt:
                reads38.append((a, base, ent, txt))
            if d == 0x3c and txt.startswith("MOV E") and "dword ptr [" + base in txt:
                reads3c.append((a, base, ent, txt))

    P("instructions scanned: %d ; functions with memory operands: %d" % (n_ins, len(per_fn)))

    # ---- CONTROL: the frame-relative fingerprint must select FUN_18013fe00
    P("")
    P("=== CONTROL: frame-relative item fingerprint (marks + 0x160) ===")
    ctl = []
    for ent, rec in per_fn.items():
        got = [m for m in FRAME_MARKS if m in rec["disp"]]
        if len(got) >= 3:
            ctl.append((ent, rec["name"], [hex(g) for g in got]))
    for ent, nm, got in sorted(ctl):
        P("  %-18s %#x  marks %s  %s" % (nm, ent, got,
                                         "<== FUN_18013fe00" if ent == 0x18013FE00 else ""))
    P("  control %s" % ("PASS" if any(e == 0x18013FE00 for e, _, _ in ctl)
                        else "FAIL -- fingerprint cannot see the known item handler"))

    # ---- pointer-relative fingerprint: the real search
    P("")
    P("=== ITEM HANDLERS BY POINTER-RELATIVE FINGERPRINT (>=2 of %s) ==="
      % [hex(m) for m in ITEM_MARKS])
    cands = []
    for ent, rec in per_fn.items():
        got = []
        for m in ITEM_MARKS:
            for (a, base, txt) in rec["disp"].get(m, []):
                if base not in ("RSP", "RBP"):
                    got.append(m)
                    break
        if len(got) >= 2:
            cands.append((ent, rec["name"], got))
    P("candidates: %d" % len(cands))
    for ent, nm, got in sorted(cands):
        rec = per_fn[ent]
        h38 = [(a, b, t) for (a, b, t) in rec["disp"].get(0x38, []) if b not in ("RSP", "RBP")]
        h3c = [(a, b, t) for (a, b, t) in rec["disp"].get(0x3c, []) if b not in ("RSP", "RBP")]
        P("")
        P("  %-18s %#x  marks %s   +0x38:%d  +0x3c:%d"
          % (nm, ent, [hex(g) for g in got], len(h38), len(h3c)))
        for a, b, t in h38:
            P("        38  %#x  %s" % (a, t))
        for a, b, t in h3c:
            P("        3c  %#x  %s" % (a, t))

    # ---- selector shape
    P("")
    P("=== SELECTOR SHAPE: dword read [reg+0x38] then dword read [SAME reg+0x3c] within 0x40 ===")
    idx3c = {}
    for a, base, ent, txt in reads3c:
        idx3c.setdefault(base, []).append((a, ent, txt))
    nsel = 0
    for a, base, ent, txt in reads38:
        for a2, ent2, txt2 in idx3c.get(base, []):
            if 0 < a2 - a <= 0x40:
                nsel += 1
                nm = per_fn.get(ent, {}).get("name", "?")
                P("  %s @ %#x : %#x %s   ->   %#x %s" % (nm, ent, a, txt, a2, txt2))
    P("  selectors found: %d" % nsel)
    P("  (reads at +0x38: %d, reads at +0x3c: %d, over the whole .text)"
      % (len(reads38), len(reads3c)))

    with open(OUT, "w") as fh:
        fh.write("\n".join(lines))
    print("wrote %s (%d lines)" % (OUT, len(lines)))
except Exception:
    traceback.print_exc()
