"""D3 Q2/Q3/Q4: who READS the pack record's packContentInfo slots, `start` and `unopened`, and does anything count the delivered itemList against them? ESTABLISHED IN RUN 1 (q_pack_content_1.py, d3_pack_elem_deser.asm), twice over -- once from the decompiler's frame locals and once from raw disassembly: pack element deser 0x18013af30, record base = RSP+0x50 = RBP-0xB0, record size 0x158 itemQuantity 0x170 -> [RBP+0x94] -> rec +0x144 goldQuantity 0x149 -> [RBP+0x98] -> rec +0x148 silverQuantity 0x2c6 -> [RBP+0x9c] -> rec +0x14c bronzeQuantity 0x63 -> [RBP+0xa0] -> rec +0x150 rareQuantity 0x273 -> [RBP+0xa4] -> rec +0x154 state 0x2eb -> [RBP+0x00] -> rec +0x0b0 start 0x2e3 -> [RBP+0x04] -> rec +0x0b4 (INT via 0x1800d7b30) useDefaultImage0x36a -> [RBP+0x1c] -> rec +0x0cc (inverted) unopened 0x35d -> [RBP+0x1d] -> rec +0x0cd (BOOL, stored raw) `start` and `unopened` are TOP-LEVEL pack keys, NOT packContentInfo children. HYPOTHESIS: nothing in CardsDLL reads +0x144..+0x154 back. WHY A BYTE SCAN IS SOUND HERE: every offset of interest is >= 0x80, so x86 cannot encode it as a signed disp8. Any instruction touching one of these slots must carry the literal disp32 little-endian bytes. So a raw .text scan for those 4 bytes is an EXHAUSTIVE upper bound on the set of candidate accesses; each hit is then confirmed by asking Ghidra for the instruction containing it and checking the scalar. POSITIVE CONTROL FOR THE SCAN: the record copy-assign 0x1801340e0 and the push_back 0x180132180 must move all 0x158 bytes. If they copy field-by-field the scan MUST list them; if the scan returns nothing at all for every offset including theirs, the scan is broken, not the binary. Second control: the stride 0x158 must be found in 0x18013af30 itself (the /0x158 count check) and in 0x180132180. """ import traceback, sys, os OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres/" os.makedirs(OUT, exist_ok=True) REC = {0x144: "itemQuantity", 0x148: "goldQuantity", 0x14c: "silverQuantity", 0x150: "bronzeQuantity", 0x154: "rareQuantity", 0x0b0: "state", 0x0b4: "start", 0x0cc: "useDefaultImage", 0x0cd: "unopened", 0x158: "STRIDE/record-size"} def dump(tag, va, path, echo=True): src = dec(va) hdr = ("%s %#x fname=%s len(src)=%d (FULL, NOT TRUNCATED)" % (tag, va, fname(va), len(src))) if echo: print("=" * 78) print(hdr) print("=" * 78) print(src) with open(path, "w") as fh: fh.write("// " + hdr + "\n" + src) return src def scan_disp(val): """every .text instruction carrying `val` as a literal 4-byte scalar""" pat = bytes([val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF]) seen = {} for h in find_all(pat, blocks=(".text",)): ins = None for back in range(0, 12): try: i2 = listing.getInstructionContaining(addr(h - back)) except Exception: i2 = None if i2 is not None: ins = i2 break if ins is None: continue ok = False for i in range(ins.getNumOperands()): for o in ins.getOpObjects(i): try: if (int(o.getValue()) & 0xFFFFFFFF) == val: ok = True except Exception: pass if not ok: continue a = int(ins.getAddress().getOffset()) seen[a] = (fname(a), str(ins)) return seen try: print("### CONTROL A: does the RS4 machinery work in THIS project copy?") for nm in ("FutSquadSaveServerResponse", "FutStoreGetPackTypesServerResponse"): hits = find_all(b"RS4:" + nm.encode()) print(" RS4:%-38s literal hits=%s" % (nm, [hex(x) for x in hits])) for h in hits: xs = xrefs_to(h) print(" xrefs to literal %#x: %s" % (h, [(hex(f), t, n) for f, t, n, e in xs])) for c, expect in (("FutSquadSave", 0x180171a60), ("FutSquadList", 0x180172140), ("FutCreateMatch", 0x180120380)): r = class_deser(c) print(" class_deser(%-16s) -> %s expect %#x %s" % (c, [hex(x[0]) for x in r], expect, "PASS" if any(x[0] == expect for x in r) else "FAIL/UNKNOWN")) print("\n### RECORD LIFECYCLE FUNCTIONS (full decompiles -> files)") for va, tag in ((0x1801342d0, "record ctor"), (0x1801340e0, "record copy-assign"), (0x180132180, "vector push_back/grow"), (0x1801232a0, "record dtor"), (0x1800d7af0, "int conv A (quantities)"), (0x1800d7b30, "int conv B (start,bonus)"), (0x1800d7b10, "int conv C (id, 16-bit)")): s = dump(tag, va, OUT + "d3_life_%x.txt" % va, echo=False) print(" %#x %-26s len=%d -> d3_life_%x.txt" % (va, tag, len(s), va)) print("\n### EXHAUSTIVE disp32 SCAN OF .text") allhits = {} for off in sorted(REC): s = scan_disp(off) allhits[off] = s print("\n --- offset %#05x (%s): %d confirmed instruction(s)" % (off, REC[off], len(s))) byfn = {} for a, (fn, txt) in sorted(s.items()): byfn.setdefault(fn, []).append((a, txt)) for fn in sorted(byfn): print(" %s" % fn) for a, txt in byfn[fn]: print(" %#x %s" % (a, txt)) print("\n### VERDICT INPUT: functions touching ANY quantity slot") q = set() for off in (0x144, 0x148, 0x14c, 0x150, 0x154): for a, (fn, txt) in allhits[off].items(): q.add(fn) print(" ", sorted(q) if q else "NONE") print("\n### STORE ROOT DESER 0x1801234e0 AND ITS CALLERS") dump("store root deser", 0x1801234e0, OUT + "d3_store_root.txt", echo=True) for a, n in callers(0x1801234e0): print(" CALLER %#x %s" % (a, n)) dump("caller of store root", a, OUT + "d3_storeroot_caller_%x.txt" % a, echo=True) print("\n### FutCreatePackServerResponse deser 0x180162880 (itemList / numberItems)") dump("createpack deser", 0x180162880, OUT + "d3_createpack_deser.txt", echo=True) for a, n in callers(0x180162880): print(" CALLER %#x %s" % (a, n)) except Exception: traceback.print_exc() sys.stdout.flush()