"""Q: the client's fcc_discardcoins lookup does NOT miss (proved live). So why did
every card render "Quick Sell 0" when we omitted discardValue?

HYPOTHESIS H1: the consumer of the price reads item+0x38 (the wire discardValue)
only, and NEVER item+0x3c (where the client's own computation lands). That would
explain both halves of the observation: 0 with the field omitted, exact with it
sent.

RIVAL H2: the consumer reads +0x38 if non-zero else +0x3c. H2 predicts the
omitted-field test should have shown the right numbers, which it did not, but H2
survives if the failing observation was mis-made, so decide it on code.

METHOD: enumerate EVERY instruction in .text whose memory operand is a dword at
displacement 0x38 or 0x3c, tabulate by containing function, and then look at the
functions that touch BOTH (a candidate "sent else computed" selector) versus
functions that touch only one.

CONTROL: FUN_18013fe00 must appear in both tables (it provably reads +0x38 at
0x180141025 and writes +0x3c at 0x180141140). If the scan does not find those two
exact addresses, the scan is broken and its silence means nothing.

Also: FUN_180141660 in full, plus every string literal it references, for the
"does the miss degrade other screens" question.

Absence discipline: this scan enumerates operands, not immediates, so it is
immune to the ==/!=/switch/ladder trap; but it is scoped to CardsDLL only and
says nothing about FIFA17.exe.
"""
import traceback

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/cards/q2_out.txt"

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

    # ---------------------------------------------------------------- 1. operand scan
    from ghidra.program.model.lang import OperandType

    hits38, hits3c = {}, {}
    n_ins = 0
    it = listing.getInstructions(True)
    while it.hasNext():
        ins = it.next()
        n_ins += 1
        txt = str(ins)
        if "0x38]" not in txt and "0x3c]" not in txt:
            continue
        a = int(ins.getAddress().getOffset())
        f = fm.getFunctionContaining(ins.getAddress())
        key = (f.getName() if f else "?",
               int(f.getEntryPoint().getOffset()) if f else 0)
        if "0x38]" in txt:
            hits38.setdefault(key, []).append((a, txt))
        if "0x3c]" in txt:
            hits3c.setdefault(key, []).append((a, txt))

    P("instructions scanned: %d" % n_ins)
    P("functions touching a +0x38 operand: %d" % len(hits38))
    P("functions touching a +0x3c operand: %d" % len(hits3c))

    P("")
    P("=== CONTROL ===")
    ctl = [t for k, v in hits38.items() for t in v if t[0] == 0x180141025]
    P("read of +0x38 at 0x180141025 found: %s" % (ctl or "NO -- SCAN BROKEN"))
    ctl2 = [t for k, v in hits3c.items() for t in v if t[0] == 0x180141140]
    P("write of +0x3c at 0x180141140 found: %s" % (ctl2 or "NO -- SCAN BROKEN"))

    both = sorted(set(hits38) & set(hits3c), key=lambda k: k[1])
    P("")
    P("=== FUNCTIONS TOUCHING BOTH +0x38 AND +0x3c (%d) ===" % len(both))
    for k in both:
        P("  %s @ %#x" % k)
        for a, t in sorted(hits38[k]):
            P("      38  %#x  %s" % (a, t))
        for a, t in sorted(hits3c[k]):
            P("      3c  %#x  %s" % (a, t))

    only3c = sorted(set(hits3c) - set(hits38), key=lambda k: k[1])
    P("")
    P("=== FUNCTIONS TOUCHING +0x3c ONLY (%d) ===" % len(only3c))
    for k in only3c:
        P("  %s @ %#x" % k)
        for a, t in sorted(hits3c[k]):
            P("      %#x  %s" % (a, t))

    # ---------------------------------------------------------------- 2. tiny accessors
    P("")
    P("=== TINY ACCESSOR BYTE PATTERNS IN .text ===")
    pats = {
        "mov eax,[rcx+0x38]; ret": b"\x8b\x41\x38\xc3",
        "mov eax,[rcx+0x3c]; ret": b"\x8b\x41\x3c\xc3",
        "mov eax,[rcx+0x38]": b"\x8b\x41\x38",
        "mov eax,[rcx+0x3c]": b"\x8b\x41\x3c",
    }
    for name, pat in pats.items():
        hs = find_all(pat, blocks=(".text",))
        P("  %-26s %d hits" % (name, len(hs)))
        for h in hs[:40]:
            f = fm.getFunctionContaining(addr(h))
            P("      %#x  in %s" % (h, f.getName() if f else "?"))

    # ---------------------------------------------------------------- 3. FUN_180141660
    src = dec(0x180141660)
    P("")
    P("=== FUN_180141660 FULL DECOMPILE, len=%d ===" % len(src))
    P(src)

    P("")
    P("=== STRING LITERALS REFERENCED BY FUN_180141660 ===")
    f = func(0x180141660)
    seen = set()
    for ad in f.getBody().getAddresses(True):
        ins = listing.getInstructionAt(ad)
        if ins is None:
            continue
        for r in ins.getReferencesFrom():
            t = int(r.getToAddress().getOffset())
            if t in seen or not (0x1801E5000 <= t <= 0x180290000):
                continue
            seen.add(t)
            try:
                s = rd_str(t, 80)
            except Exception:
                continue
            if s and all(32 <= ord(c) < 127 for c in s) and len(s) >= 3:
                P("  %#x  from %#x  %r" % (t, int(ad.getOffset()), s))

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