"""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()