diff --git a/fifa17-recon/tools/manager_chem_probe.py b/fifa17-recon/tools/manager_chem_probe.py new file mode 100755 index 0000000..518be7c --- /dev/null +++ b/fifa17-recon/tools/manager_chem_probe.py @@ -0,0 +1,100 @@ +#!/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())