fifa17-recon: the card pool is now the REAL FIFA 17 roster, 17547 players

The old pool was 79 hand-written rows whose asset ids were mostly invented, on
the premise that the client's card map is empty offline so no id could render.
That premise was refuted by a live screenshot, and this replaces its consequence.

Source: tools/dbdata_extract.py reads FIFA's own rating-sorted index out of a
running process (0x40 stride, self-validating {begin,end,end+1} name-pointer
triple, anchored on 20801 = Ronaldo 94) -> data/roster.json. dbdata.dll was a
dead end and is documented as such: its single export getTableData is an
anti-tamper attestation routine, not a data accessor.

Cross-validated against a completely independent method. tools/sweep_collect.py
serves candidate ids as a synthetic club and reads back the identity the CLIENT
resolved through its own merge. 573 of 573 overlapping names agreed exactly, and
the single id present in one and not the other is 26501, the target of the
documented 22800..22879 Legends remap -- which is also what produced 'Alex Hunter
x80' in a sweep and had looked like a bug.

Field honesty, because half of these are real and half are not:
  playerid/rating/name  REAL   the roster
  club/nation/league    REAL   we send zeros and the CLIENT fills them (the merge
                               only fills those fields when they arrive as zero)
  position              PARTLY 59 from the game's own per-card cache, 17 curated
                               by hand, the rest synthetic but deterministic
  attributes            SYNTH  derived from rating and position

169193 is dropped from the curated set: it was in VERIFIED_ASSET_IDS and is not a
real player. The client resolves it to the database's empty placeholder row, which
renders as 'Jamal Blackman'. Two independent methods agreed.

NOTE BEFORE PUSHING ANYWHERE PUBLIC: data/roster.json is EA's player data,
extracted from your own installation. Fine locally; think twice about publishing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 21:08:52 -07:00
parent b0bbc2a07f
commit e9e6f203c2
8 changed files with 138898 additions and 160 deletions
+44
View File
@@ -113,14 +113,58 @@ def show(db):
r["teamid"], r["nation"], r["league"]))
def watch(db, interval, seconds):
"""Merge continuously while the game pages through an auto sweep.
THE MAP DOES NOT ACCUMULATE. Every club fetch wipes it and repopulates from
that response alone, so a single probe at the end of an auto sweep sees only
the LAST chunk -- which is how 34,000 candidates were nearly thrown away. A
pass costs 0.03s and chunks are ~12s apart, so polling catches all of them.
"""
import time
t0 = time.time()
last = -1
while time.time() - t0 < seconds:
cards = probe_quiet()
if cards:
added, _, _ = merge(db, cards)
if added:
save(db)
if len(db) != last:
last = len(db)
print("[%4ds] %d player(s)" % (time.time() - t0, len(db)), flush=True)
time.sleep(interval)
save(db)
return 0
def probe_quiet():
pid = W.find_pid()
if pid is None:
return None
base = W.dll_base(pid)
if not base:
return None
mem = W.Mem(pid)
obj = mem.q(base + (W.G_CARDSDB - W.IMG_BASE))
if not obj:
return None
return [c for c in (P.read_card(mem, n) for n in P.nodes(mem, obj)) if c]
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--show", action="store_true")
ap.add_argument("--watch", type=float, metavar="SECS",
help="poll continuously for SECS while an auto sweep runs")
ap.add_argument("--interval", type=float, default=1.0)
a = ap.parse_args()
db = load()
if a.show:
show(db)
return 0
if a.watch:
return watch(db, a.interval, a.watch)
cards = probe()
if cards is None:
return 1