"""D3 run 8: what actually sizes the pack reveal. FOUND IN RUN 7: the CreatePack deserializer 0x180162880 push_backs EVERY parsed item TWICE, once into the response object's own vector (obj+0x30/0x38/0x40) and once into a singleton's vector reached as mgr = FUN_18011a830() -> vtbl[0x160](mgr) (call it PACKMGR) PACKMGR+0x30 / +0x38 / +0x40 item vector, 0x18-byte elements PACKMGR+0x28 byte set to 1 after the whole body is parsed ("contents ready") vtbl[0x10](PACKMGR) called BEFORE parsing (presumably clear) numberItems (atom 0x1dd) is written to the RESPONSE at obj+0x28 and is never used to size either vector: both grow one element per item actually present in itemList. THIS RUN 1. Resolve FUN_18011a830 and the vtbl+0x160 accessor so PACKMGR's class is named. 2. Decompile vtbl+0x10 (the pre-parse call) to confirm it is a clear. 3. Find readers of PACKMGR's vector and of the +0x28 ready flag: that is the reveal. 4. Disassemble the unanalysed RPC handler thunks 0x180124810 (StorePackTypes), 0x180124800 (StorePackQuantities), 0x180124250 (PurchasePack) -- Ghidra created no functions there, so read the bytes directly. CONTROL: FUN_18011a830 must resolve to a singleton getter (a DAT_ load or a create-on-first-use), and slot 0x160 must be a plain accessor. If either decompiles to something unrelated the chain is misread. """ 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) 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 try: print("### 1. SINGLETON CHAIN") dump("FUN_18011a830", 0x18011a830, OUT + "d3_mgr_getter.txt", echo=True) print(" callers of 0x18011a830: %d" % len(callers(0x18011a830))) print("\n### 4. RPC HANDLER THUNK BYTES") for va, nm in ((0x180124810, "STOREPACKTYPES"), (0x180124800, "STOREPACKQUANTITIES"), (0x180124250, "PURCHASEPACK"), (0x180124260, "PURCHASEDITEMS"), (0x180124240, "PURCHASEITEMS")): b = read_bytes(va, 32) print(" %#x %-22s %s" % (va, nm, b.hex())) f = fm.getFunctionContaining(addr(va)) print(" containing function: %s" % (f.getName() if f else "NONE")) ins = listing.getInstructionContaining(addr(va)) print(" instruction: %s" % (str(ins) if ins else "NONE (undisassembled)")) # decode a rel32 jmp/call if present if b[0] == 0xE9: t = va + 5 + int.from_bytes(b[1:5], "little", signed=True) print(" JMP rel32 -> %#x %s" % (t, fname(t))) if b[0] == 0x48 and b[1] == 0xFF and b[2] == 0x25: t = va + 7 + int.from_bytes(b[3:7], "little", signed=True) print(" JMP [rip+..] -> slot %#x = %#x" % (t, qword(t))) print("\n### 5. READERS OF THE RESPONSE-SIDE numberItems obj+0x28") # obj+0x28 is disp8-encodable so a byte scan is useless; instead enumerate # everything that can hold a FutCreatePackServerResponse: only its ctor names the # vtable, and the factory has no callers, so the object is dispatched generically. for tgt in (0x180228260, 0x1802282f0, 0x180228268): print(" xrefs to %#x: %s" % (tgt, [(hex(f), t, n) for f, t, n, e in xrefs_to(tgt)])) print("\n### 6. duplicateItemIdList sub-parser 0x180138e10") dump("dupe id list parser", 0x180138e10, OUT + "d3_dupe_parser.txt", echo=True) except Exception: traceback.print_exc() sys.stdout.flush()