"""The hub response class exists after all. Resolve it and settle the clubPlayers question. q_hub_1.py searched for RS4:FutGetHubServerResponse and found nothing, and the pack-opening doc recorded that as "there is no hub class". Wrong search, not absent class: the literal scan turned up `tHubDataServerResponse` at 0x18022ce61, i.e. the class is FutGetHubData..., not FutGetHub.... This is the same absence-from-a-bad-search trap the project has hit before, so this file re-derives it from the literal outward. q_hub_1 also found ZERO direct `== 0x90` comparisons for clubPlayers across 152 deserializers, with a passing control (itemList in 0x180162880). That is suggestive but it is NOT a proven absence, because dispatch here is frequently a running-sum sub/dec ladder, which a source grep for `== 0x90` cannot see. Settle it by reading the actual hub deserializer instead of by scanning. QUESTIONS Q1 exact class literal, its vtable, its deserializer (RS4 rule: the literal is "RS4:", so search the full string and take vtable slot +0x08) Q2 how many tokens the deserializer spends before its first key read, which decides whether the flat two-key body we serve loses its first pair Q3 which atoms its ladder actually handles, read from the decompile IN FULL rather than grepped, so clubPlayers 0x90 and auctionCount 0x33 get a real answer CONTROL: resolve FutSquadSave the same way in the same run. Its deserializer is known to be 0x180171a60. If that does not come back correct, nothing else here is trustworthy. """ import traceback CANDIDATES = [b"RS4:FutGetHubDataServerResponse", b"RS4:FutSquadSaveServerResponse"] KNOWN = {"RS4:FutSquadSaveServerResponse": 0x180171A60} def resolve(lit): """literal -> [(deser, vtable, factory)] via the RS4 rule.""" out = [] for a in find_all(lit): print(" literal at %#x: %r" % (a, rd_str(a))) for frm, typ, fn, ent in xrefs_to(a): if not ent: continue f = func(ent) if f is None: continue for ad in f.getBody().getAddresses(True): ins = listing.getInstructionAt(ad) if ins is None: continue for r in ins.getReferencesFrom(): t = int(r.getToAddress().getOffset()) if not (0x1801E5000 <= t <= 0x1802891FF): continue try: v0, v1 = qword(t), qword(t + 8) except Exception: continue if fm.getFunctionAt(addr(v0)) and fm.getFunctionAt(addr(v1)): out.append((v1, t, ent)) return out try: found = {} for lit in CANDIDATES: name = lit.decode() print("=" * 78) print(name) print("=" * 78) res = resolve(lit) uniq = sorted({d for d, _, _ in res}) for d, vt, ent in res: print(" deser %#x vtable %#x factory %#x" % (d, vt, ent)) print(" -> distinct deserializers: %s" % (" ".join("%#x" % d for d in uniq) or "NONE")) found[name] = uniq if name in KNOWN: exp = KNOWN[name] print(" CONTROL: expected %#x, %s" % (exp, "MATCH" if exp in uniq else "*** MISMATCH, results are void ***")) print() for d in found.get("RS4:FutGetHubDataServerResponse", []): src = dec(d) f = func(d) print("=" * 78) print("HUB DESERIALIZER %#x body %d bytes / decompile %d chars (IN FULL)" % (d, f.getBody().getNumAddresses() if f else -1, len(src))) print("=" * 78) print(src) toks, begin, keys = [], [], [] for ad in f.getBody().getAddresses(True): ins = listing.getInstructionAt(ad) if ins is None: continue for r in ins.getReferencesFrom(): t = int(r.getToAddress().getOffset()) a = int(ad.getOffset()) if t == 0x1801C7F10: toks.append(a) elif t == 0x1801C8270: begin.append(a) elif t == 0x180141EE0: keys.append(a) print("\n tokenizer calls : %d at %s" % (len(toks), " ".join("%#x" % a for a in toks))) print(" begin-object : %s" % (" ".join("%#x" % a for a in begin) or "NONE")) print(" key-reader calls: %s" % (" ".join("%#x" % a for a in keys) or "NONE")) if begin and keys: b, k = min(begin), min(keys) print(" tokens before first key read: %d" % len([a for a in toks if b < a < k])) print("\n callers: %s" % " ".join("%#x %s" % (a, n) for a, n in callers(d)[:10])) except Exception: traceback.print_exc()