#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Dump the CLASSIFICATION fields the client stored for every card it holds, so the subtype->cardtype map and the itemState runtime values are read from the running game instead of inferred. READ-ONLY. /proc/PID/mem is opened 'rb'; there is no write path in this file. WHY THIS EXISTS --------------- Two things this project has repeatedly had to treat as INFERRED: 1. `FUN_1800d8330`'s cardsubtypeid -> cardtype map. It is read out of Ghidra (0..3->1 players, 4->2 manager, 5->3 headcoach, 6->10 gkcoach, 7->5 physio, 8->4 fitnesscoach, 9..b->7), and the kit selector gate `FUN_1801c3480` branches on cardtype == 7. Serving a subtype whose cardtype we guessed wrong fails SILENTLY, because cardtype 9 has no arm in the merge. 2. The itemState enum. The table at 0x180229d20 gives the tokens; the RUNTIME values the strings deserialize to (notably activeHomeKit/activeAwayKit -> 101/102) have been carried as inferred. Both are directly observable: the parser writes cardsubtypeid to rec+0x50, the derived cardtype to rec+0x4c, and the decoded itemState to rec+0x5c. Reading those back for every record turns the pair into measurements. rec+0x18 resourceId rec+0x4c cardtype (derived by FUN_1800d8330 from cardsubtypeid) rec+0x50 cardsubtypeid (as sent) rec+0x5c itemState (decoded enum value) Usage: python3 record_vocab_probe.py """ import collections import sys import watch_club_model as W import card_identity_probe as P F_RESOURCE = 0x18 F_CARDTYPE = 0x4C F_SUBTYPE = 0x50 F_ITEMSTATE = 0x5C REC_SIZE = 0x158 # What the Ghidra read of FUN_1800d8330 predicts, so a disagreement is loud. EXPECTED_CARDTYPE = {0: 1, 1: 1, 2: 1, 3: 1, 4: 2, 5: 3, 6: 10, 7: 5, 8: 4, 9: 7, 10: 7, 11: 7} def main(): pid = W.find_pid() if pid is None: print("FIFA17.exe is not running.") return 1 base = W.dll_base(pid) if base is None: print("pid %d is up but %s is not mapped yet." % (pid, W.DLL)) return 1 mem = W.Mem(pid) obj = mem.q(base + (W.G_CARDSDB - W.IMG_BASE)) if not obj: print("CardsDb singleton is NULL (no FUT session loaded).") return 1 ns = W.nodes(mem, obj) if hasattr(W, "nodes") else P.nodes(mem, obj) print("pid=%d CardsDb=%#x walked=%d\n" % (pid, obj, len(ns))) pairs = collections.Counter() states = collections.Counter() rows = [] for n in ns: buf = mem.read(n + 0x28, REC_SIZE) if not buf or len(buf) < REC_SIZE: continue resource = P.u32(buf, F_RESOURCE) cardtype = P.u8(buf, F_CARDTYPE) subtype = P.u8(buf, F_SUBTYPE) state = P.u8(buf, F_ITEMSTATE) pairs[(subtype, cardtype)] += 1 states[state] += 1 rows.append((resource, subtype, cardtype, state)) print("%-12s %-9s %-9s %s" % ("resource", "subtype", "cardtype", "itemState")) for r in sorted(rows): print("%-12d %-9d %-9d %d" % r) print("\n--- MEASURED cardsubtypeid -> cardtype ---") for (sub, ct), n in sorted(pairs.items()): want = EXPECTED_CARDTYPE.get(sub) if want is None: verdict = "no Ghidra prediction for this subtype" elif want == ct: verdict = "agrees with FUN_1800d8330" else: verdict = "DISAGREES -- Ghidra said %d" % want print(" subtype %-4d -> cardtype %-4d (%d record(s)) %s" % (sub, ct, n, verdict)) print("\n--- MEASURED itemState runtime values ---") for st, n in sorted(states.items()): print(" %-5d %d record(s)" % (st, n)) print("\nNOTE: a runtime value only appears here if the client was actually") print("served an item in that state. Absence is not evidence of absence.") return 0 if __name__ == "__main__": sys.exit(main())