"""D5 Q1/Q2/Q3/Q5: the store ServerCall classes, the currency element deser, the transaction state enum table, and the client-side purchase validators. HYPOTHESES H1 (fork): PurchasePack and PurchaseItems are two separate ServerCall classes, both POSTing to a "/transaction"-suffixed URL; the fork is decided client-side at construction time and the server has no say in it. H2 (price): the pack currency record is {std::string name; u32 funds; u32 finalFunds; u32 origExtPriceId; u32 finalExtPriceId} and FUN_180138bd0 is the element parser that fills name/funds/finalFunds. H3 (state enum): the 9-entry table at 0x1802d02c0 (u32 value, char* name) is the complete `state` vocabulary of FutPurchaseItemsServerResponse. CONTROL: dec(0x180171a60) must be the FutSquadSave deserializer (a big SAX loop calling FUN_1801c7f10); dec(0x180120380) the FutCreateMatch one. Both printed. """ import traceback, os OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres" def w(name, text): p = os.path.join(OUT, name) with open(p, "w") as f: f.write(text) print("WROTE %s (%d bytes)" % (p, len(text))) try: buf = [] def P(*a): s = " ".join(str(x) for x in a) print(s); buf.append(s) P("=== CONTROL: sizes of two known deserializers ===") for c in (0x180171a60, 0x180120380): s = dec(c) P(" %#x %-24s len=%d has_sax_loop=%s" % (c, fname(c), len(s), "FUN_1801c7f10" in s)) # ---- H3: transaction state enum table ---- P("") P("=== state enum table @0x1802d02c0 (u32 value, char* name) x12 ===") for i in range(12): base = 0x1802d02c0 + i * 16 try: val = dword(base) ptr = qword(base + 8) nm = rd_str(ptr) if 0x180000000 <= ptr < 0x181000000 else "?" except Exception as e: P(" [%d] ERR %s" % (i, e)); continue P(" [%2d] %#010x ptr=%#x %r" % (i, val, ptr, nm)) # ---- the atom name table, to prove the request key names live there ---- P("") P("=== atom-name pointer table around 0x1802d42a8 (useCredits) ===") for off in range(-6, 7): a = 0x1802d42a8 + off * 8 try: ptr = qword(a) nm = rd_str(ptr) if 0x180000000 <= ptr < 0x181000000 else "?" except Exception as e: nm = "ERR %s" % e; ptr = 0 P(" %#x -> %#x %r" % (a, ptr, nm)) # ---- who references FUN_180126720 (the /transaction URL builder) ---- P("") for tgt, tag in ((0x180126720, "url_builder_/transaction"), (0x1801269f0, "purchaseitems_deser"), (0x180162880, "createpack_deser"), (0x1801267b0, "http409_handler"), (0x1801669b0, "state_str_to_enum"), (0x180138bd0, "currency_elem_deser"), (0x1801234e0, "packtypes_deser")): try: xs = xrefs_to(tgt) except Exception as e: P("XREFS %s %#x ERR %s" % (tag, tgt, e)); continue P("XREFS to %s %#x : %d" % (tag, tgt, len(xs))) for frm, typ, fn, ent in xs: P(" from %#x %-12s in %s @ %#x" % (frm, typ, fn, ent)) # ---- vtables around the store server-call classes ---- P("") P("=== scan .rdata for qword == 0x180126720 / 0x1801269f0 / 0x180162880 (vtable slots) ===") import struct for tgt in (0x180126720, 0x1801269f0, 0x180162880, 0x1801267b0, 0x1801234e0, 0x1801758c0): pat = struct.pack(" target %#x" % (hit, tgt)) for j in range(-4, 10): a = hit + j * 8 try: q = qword(a) except Exception: continue f = fm.getFunctionAt(addr(q)) if 0x180000000 <= q < 0x181000000 else None s = "" if 0x180000000 <= q < 0x181000000 and f is None: try: t = rd_str(q, 60) if t and all(32 <= ord(c) < 127 for c in t): s = repr(t) except Exception: pass P(" %+3d %#x -> %#x %s %s" % (j, a, q, f.getName() if f else "", s)) w("d5_q2_notes.txt", "\n".join(buf) + "\n") # ---- decompiles ---- TARGETS = { "currency_elem_deser_180138bd0": 0x180138bd0, "script_PurchasePack_18003f010": 0x18003f010, "script_EnterStore_18003efd0": 0x18003efd0, "script_ExitStore_18003eff0": 0x18003eff0, "script_ValidateCoinPurchase_18003f060": 0x18003f060, "script_ValidatePointsPurchase_18003f090": 0x18003f090, "http409_1801267b0": 0x1801267b0, "urlbuild_180126720": 0x180126720, "state_enum_1801669b0": 0x1801669b0, } for tag, va in sorted(TARGETS.items()): try: f = func(va); src = dec(va) except Exception as e: f = None; src = "// ERR %s" % e w("d5_q2_dec_%s.txt" % tag, "// %s va=%#x entry=%s len=%d\n" % (tag, va, fname(va), len(src)) + src) # ---- dump every function in the store-service .text cluster ---- P("") P("=== functions in 0x180126400..0x180127400 ===") cluster = [] it = fm.getFunctions(addr(0x180126400), True) while it.hasNext(): f = it.next() e = int(f.getEntryPoint().getOffset()) if e > 0x180127400: break cluster.append(e) P(" %d funcs: %s" % (len(cluster), ", ".join("%#x" % c for c in cluster))) txt = [] for e in cluster: s = dec(e) txt.append("// ===== %#x %s len=%d\n%s" % (e, fname(e), len(s), s)) w("d5_q2_cluster_126400.txt", "\n".join(txt)) # createpack cluster cluster2 = [] it = fm.getFunctions(addr(0x180162400), True) while it.hasNext(): f = it.next() e = int(f.getEntryPoint().getOffset()) if e > 0x180162e00: break cluster2.append(e) txt = [] for e in cluster2: s = dec(e) txt.append("// ===== %#x %s len=%d\n%s" % (e, fname(e), len(s), s)) w("d5_q2_cluster_162400.txt", "\n".join(txt)) w("d5_q2_notes.txt", "\n".join(buf) + "\n") print("DONE") except Exception: traceback.print_exc()