"""D3 Q4: who CONSUMES the state slots, and the transfer/quick-sell vocabulary. The q3 publisher hunt had a control that came back empty for a reason I can state: FUN_18013fe00 builds the record on the STACK (RSP-relative), and I had excluded RSP/RBP bases, so the deser cannot appear. That control was therefore uninformative, not passed. Here the fingerprint is rebuilt around offsets that are only meaningful on a fully built item record and that a consumer must reach through a register: 0x3c discardValue-computed, 0xb4 rating, 0x146 preferredPosition, 0x148 nation, 0x154 leagueId, 0x94 teamid, 0x5c itemState, 0x49 tradeable-flag, 0x60 pile CONTROL for this scan: it must rediscover functions that touch 0xb4 AND 0x146 AND 0x148 together, because the UI certainly draws rating, position and nation from the same object. Zero such functions => the scan is broken and nothing below counts. Also decompiled in full: FUN_1800515e0 touches all five of {0x38,0x3c,0x49,0x5c,0x60} -- candidate item copy-constructor, which if true independently re-derives the layout FUN_180130d10 the untradeableCount deser that WRITES [reg+0x49] FUN_180128600 / FUN_180128e30 the only two deserializers that parse `pile` 0x226 FUN_18013e410 tradeId 0x331 + tradeState 0x335 FUN_180148b70 the second `untradeable` 0x361 arm FUN_180138e10 itemLoans 0x16f + duplicateItemLoans 0xed And a literal hunt for the transfer-market / quick-sell vocabulary with xrefs. """ import traceback, os from collections import defaultdict OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/cards/" os.makedirs(OUT, exist_ok=True) def dump(tag, va, echo=True): f = func(va) if f is None: print("%s %#x -> NO FUNCTION" % (tag, va)) return "" src = dec(va) print("=" * 78) print("%s %#x %s len(src)=%d (FULL)" % (tag, va, f.getName(), len(src))) print("=" * 78) if echo: print(src) with open(OUT + "q4_%s_%x.c" % (tag, va), "w") as fh: fh.write(src) return src try: print("###### A. item-record consumer fingerprint over the whole .text") disp_of = defaultdict(set) it = listing.getInstructions(True) tot = 0 while it.hasNext(): ins = it.next() tot += 1 f = None for i in range(ins.getNumOperands()): try: objs = ins.getOpObjects(i) except Exception: continue if len(objs) < 2: continue regs = [o for o in objs if hasattr(o, "getName")] if not regs or any(str(r) in ("RSP", "RBP", "ESP", "EBP") for r in regs): continue for o in objs: try: v = int(o.getValue()) except Exception: continue if 0 <= v <= 0x200: if f is None: ff = fm.getFunctionContaining(ins.getAddress()) if ff is None: break f = int(ff.getEntryPoint().getOffset()) disp_of[f].add(v) print(" scanned %d instructions" % tot) CTRL = {0xb4, 0x146, 0x148} ctrl_fns = [e for e, d in disp_of.items() if CTRL <= d] print(" CONTROL {0xb4,0x146,0x148} all present in %d functions: %s" % (len(ctrl_fns), [("%s(%#x)" % (fname(e), e)) for e in sorted(ctrl_fns)][:20])) ITEM = {0x3c, 0xb4, 0x146, 0x148, 0x154, 0x94, 0x5c, 0x49, 0x60, 0x38, 0x50, 0x58} scored = sorted(((len(ITEM & d), e, sorted(ITEM & d)) for e, d in disp_of.items()), reverse=True) print(" top item-record consumers by fingerprint overlap:") for k, e, s in scored[:40]: if k < 5: break print(" %2d/%d %#x %-20s %s" % (k, len(ITEM), e, fname(e), [hex(x) for x in s])) print(" of those, the ones that ALSO touch 0x5c or 0x49:") for k, e, s in scored[:200]: if k < 4: break if 0x5c in s or 0x49 in s: print(" %2d/%d %#x %-20s %s" % (k, len(ITEM), e, fname(e), [hex(x) for x in s])) print() print("###### B. full decompiles") for tag, va in (("copyctor_cand", 0x1800515e0), ("untradeableCount_deser", 0x180130d10), ("pile_deser_a", 0x180128600), ("pile_deser_b", 0x180128e30), ("tradeId_tradeState_deser", 0x18013e410), ("untradeable_2nd", 0x180148b70), ("itemLoans_deser", 0x180138e10)): dump(tag, va) print() print("###### C. transfer / sell / list vocabulary in .rdata, with xrefs") NEEDLES = [b"TRANSFER", b"Transfer", b"transfer", b"QUICK_SELL", b"QuickSell", b"quickSell", b"DISCARD", b"Discard", b"TRADEABLE", b"tradeable", b"Tradeable", b"UNTRADEABLE", b"LIST_ON", b"tradepile", b"TRADE_PILE", b"canBeSold", b"isTradeable", b"AUCTION", b"auctionhouse"] seen = set() for nd in NEEDLES: hits = find_all(nd, blocks=(".rdata", ".data")) print(" needle %-14s hits=%d" % (nd.decode(), len(hits))) for h in hits[:80]: # back up to the start of the C string st = h for _ in range(96): try: if mem.getByte(addr(st - 1)) == 0: break except Exception: break st -= 1 if st in seen: continue seen.add(st) s = rd_str(st, 120) xs = xrefs_to(st) fns = sorted({(fn, ent) for _, _, fn, ent in xs if ent}) print(" %#x %-58r xrefs=%d %s" % (st, s, len(xs), ["%s(%#x)" % (n, e) for n, e in fns][:6])) except Exception: traceback.print_exc()