8f98e6adda
`card_identity_probe` reads the PLAYER slots (F_NATION 0x148, F_LEAGUE 0x154). A manager does not use those, so grading a manager with it reports nation=0 / leagueId=0 and reads like a server bug when it is only the wrong offsets. The manager layout, from the Ghidra reversal already recorded in fut_staff.py, is teamid rec+0x94, nation rec+0xde, leagueId rec+0xe0, talkrating rec+0xe2, negotiation rec+0xe3. The managercards merge (FUN_1801356c0) NEVER writes +0xde or +0xe0, which is exactly what makes them a clean test: whatever sits there came from our JSON and nowhere else. Read against the live client (pid 6580, 27 records in the CardsDb map): resource teamid nation league talkrating negot verdict 1000509 241 45 53 0 3 SERVER FIELDS LANDED So the manager's chemistry fields DO reach the record, and `negotiation=3` agrees with managercards row 1000509, i.e. the merge ran as well. That moves manager nation/league from INFERRED to PROVEN without a screenshot. Read-only: /proc/PID/mem is opened 'rb' and there is no write path.
101 lines
3.5 KiB
Python
Executable File
101 lines
3.5 KiB
Python
Executable File
#!/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())
|