"""D3 Q5: the UI view-model accessors over the item record, and the enum tables for pile / tradeState / bidState. WHAT LED HERE. FUN_1801a8940 is a nine-byte getter `return *(*(this+0x18) + 0x49)` and FUN_1801a89f0 is `return *(*(this+0x18) + 0x48)`: there is a wrapper class holding the item record at +0x18 and exposing its fields one accessor at a time. FUN_1801a7260 is a PREDICATE in the same region that reads item+0x49, item+0x4c, item+0x50 and item+0x145 and returns 0 or 1 -- exactly the shape of a "is this menu entry enabled" test, which is the open live question (Place on Transfer List / List on Transfer Market greyed out). HYPOTHESIS: enumerating every small function of the form `*(*(param_1+0x18) + N)` gives the COMPLETE list of item fields the UI can see, and the predicates in the same region give the gating rules. If itemState (+0x5c) has an accessor, the lifecycle table is reachable; if it has none, itemState reaches the UI some other way and I must say so. CONTROL: the accessor enumeration must find offsets we already know the UI displays -- +0x38/+0x3c (discardValue, confirmed on screen today) and +0xb4 (rating). If those come back with no accessor the enumeration is looking at the wrong class and proves nothing. Also: pile string->enum FUN_180142650 (used by the PUT /item verdict deser), tradeState FUN_180166bd0, bidState FUN_180166380, with their tables walked. """ import traceback, os, re 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 + "q5_%s_%x.c" % (tag, va), "w") as fh: fh.write(src) return src def walk_table(base, tag, lo=-0x100, hi=0x200): print(" --- table %s at %#x" % (tag, base)) for off in range(lo, hi, 0x10): a = base + off try: p = qword(a) q = qword(a + 8) except Exception: continue s = rd_str(p, 64) if 0x180000000 <= p < 0x181000000 else "" print(" %#x (%+#5x) p=%#014x q=%#010x %r" % (a, off, p, q, s)) try: print("###### A. accessor enumeration over the whole DLL") print(" every function <= 0x40 bytes whose decompile is a single deref of") print(" *(param_1 + 0x18) or *(param_1 + 0x10) plus a constant offset") pat18 = re.compile(r"\(param_1 \+ 0x18\) \+ (0x[0-9a-f]+|\d+)\)") pat18b = re.compile(r"\*\(longlong \*\)\(param_1 \+ 0x18\)\)") pat10 = re.compile(r"\(param_1 \+ 0x10\) \+ (0x[0-9a-f]+|\d+)\)") acc18 = defaultdict(list) acc10 = defaultdict(list) n_small = 0 fit = fm.getFunctions(True) while fit.hasNext(): f = fit.next() sz = int(f.getBody().getNumAddresses()) if sz > 0x40: continue n_small += 1 ent = int(f.getEntryPoint().getOffset()) try: src = dec(ent, 30) except Exception: continue for m in pat18.finditer(src): acc18[int(m.group(1), 0)].append((ent, f.getName(), sz)) for m in pat10.finditer(src): acc10[int(m.group(1), 0)].append((ent, f.getName(), sz)) if pat18b.search(src) and "+ 0x18" in src and not pat18.search(src): acc18[0].append((ent, f.getName(), sz)) print(" scanned %d small functions" % n_small) print(" --- accessors on *(this+0x18) + N (N = item record offset)") for off in sorted(acc18): for ent, nm, sz in acc18[off]: print(" +%#-6x %s (%#x) size=%d" % (off, nm, ent, sz)) print(" CONTROL: accessors exist for +0x38/+0x3c/+0xb4? %s" % {hex(k): len(acc18.get(k, [])) for k in (0x38, 0x3c, 0xb4)}) print(" itemState +0x5c accessor count: %d" % len(acc18.get(0x5c, []))) print(" --- accessors on *(this+0x10) + N") for off in sorted(acc10): for ent, nm, sz in acc10[off]: print(" +%#-6x %s (%#x) size=%d" % (off, nm, ent, sz)) print() print("###### B. the gate predicate and its neighbours") for tag, va in (("gate_1801a7260", 0x1801a7260), ("helper_1801a8900", 0x1801a8900), ("get49_1801a8940", 0x1801a8940), ("get48_1801a89f0", 0x1801a89f0), ("f5c_1801a5a30", 0x1801a5a30), ("f5c_1801a5a50", 0x1801a5a50), ("f5c_1801a5aa0", 0x1801a5aa0), ("f5c_1801a5ac0", 0x1801a5ac0), ("f5c_1801a5ae0", 0x1801a5ae0), ("f5c_1801a7040", 0x1801a7040)): dump(tag, va) print() print("###### C. who references the gate predicate (vtable slot or direct call)") for va in (0x1801a7260, 0x1801a8940, 0x1801a89f0): print(" --- xrefs to %#x" % va) for frm, typ, fn, ent in xrefs_to(va): print(" %#x %s in %s (%#x)" % (frm, typ, fn, ent)) # vtable membership: any .rdata qword equal to va import struct hits = find_all(struct.pack("enum") for tag, va in (("pile_enum_180142650", 0x180142650), ("tradeState_180166bd0", 0x180166bd0), ("bidState_180166380", 0x180166380)): src = dump(tag, va) m = re.search(r"PTR_[A-Za-z_0-9]*_(1[0-9a-f]{8})", src or "") if m: walk_table(int(m.group(1), 16), tag, 0, 0x120) else: m2 = re.search(r"DAT_(1[0-9a-f]{8})", src or "") if m2: walk_table(int(m2.group(1), 16) - 8, tag, 0, 0x120) except Exception: traceback.print_exc()