#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Read back the MANAGER-ONLY chemistry slots the client resolved, and prove whether the server's `nation`/`leagueId` actually land in the record. READ-ONLY. /proc/PID/mem is opened 'rb'; there is no write path in this file. WHY THIS EXISTS --------------- `card_identity_probe` reads the PLAYER slots (F_NATION = 0x148, F_LEAGUE = 0x154). A manager does not use those, so grading a manager with that tool reports nation=0 / leagueId=0 and looks like a server bug when it is only the wrong offsets. `fifa17-recon/tools/fut_staff.py` records the manager layout from Ghidra: rec+0x94 teamid (read by the card view-model) rec+0xde nation MANAGER-ONLY slot, u16 rec+0xe0 leagueId MANAGER-ONLY slot, u16 rec+0xe2 talkrating written by the managercards merge rec+0xe3 negotiation written by the managercards merge The merge (FUN_1801356c0) NEVER writes +0xde or +0xe0, so whatever sits there came from OUR JSON and nowhere else. That makes those two u16s a direct, unambiguous test of the server's manager chemistry fields: if they read back as the values we served, the wire contract is PROVEN rather than inferred; if they read zero, the client discarded them and manager chemistry cannot be rendering. Usage: python3 manager_chem_probe.py # grade every manager in the map """ import sys import watch_club_model as W import card_identity_probe as P MANAGER_CARDTYPE = 2 # FUN_1800d8330: cardsubtypeid 4 -> cardtype 2 F_CARDTYPE = 0x4C F_RESOURCE = 0x18 F_TEAMID = 0x94 F_NATION_MGR = 0xDE F_LEAGUE_MGR = 0xE0 F_TALKRATING = 0xE2 F_NEGOTIATION = 0xE3 REC_SIZE = 0x158 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" % (pid, obj, len(ns))) print() print("%-10s %-8s %-8s %-8s %-10s %-10s %s" % ("resource", "teamid", "nation", "league", "talkrating", "negot", "verdict")) found = 0 for n in ns: rec = n + 0x28 buf = mem.read(rec, REC_SIZE) if not buf or len(buf) < REC_SIZE: continue if P.u8(buf, F_CARDTYPE) != MANAGER_CARDTYPE: continue found += 1 resource = P.u32(buf, F_RESOURCE) teamid = P.u32(buf, F_TEAMID) nation = P.u16(buf, F_NATION_MGR) league = P.u16(buf, F_LEAGUE_MGR) talk = P.u8(buf, F_TALKRATING) negot = P.u8(buf, F_NEGOTIATION) # +0xde and +0xe0 are never written by the merge, so a non-zero value # can only have come from the server's JSON. if nation and league: verdict = "SERVER FIELDS LANDED" elif nation or league: verdict = "PARTIAL -- one slot empty" else: verdict = "EMPTY -- client kept nothing we sent" print("%-10d %-8d %-8d %-8d %-10d %-10d %s" % (resource, teamid, nation, league, talk, negot, verdict)) if not found: print("(no manager record in the map -- the client has not been served one)") return 0 if __name__ == "__main__": sys.exit(main())