fifa17-recon: real positions, clubs and the six card attributes for all 17,563 players

The pool now comes from the game's OWN resident database, not from a rating index
plus guesses. tools/db_dump.py walked the client's self-describing table catalog
read-only and wrote data/tables/ (149 tables, 55MB); tools/build_player_facts.py
turned it into data/player_facts.json; data/pool.json is the compact form fut_cards
loads.

MEASURED, per player: position (players.preferredposition1), nationality, teamid,
leagueid (via leagueteamlinks), and the six card attributes.

The six attributes are NOT columns -- they are a weighted sum of the 29 base
attributes, and the weights are read out of the game's own playerattributesmapping
table rather than from published formulas. Checked against real FIFA 17 cards:
Messi 89/90/86/96/26/61 and Ibrahimovic 72/90/81/85/31/86 are EXACT, Suarez is one
off on physical, Ronaldo within two on pace and shooting. Keepers come out directly
from the gk* columns.

What this fixes on screen: Kaka was a CDM, Bale a CM, Suarez a GK, and every
attribute was derived from the rating. Now Bale is RW, Boateng is a CB with 90
defending, De Gea is a GK, and a bronze pack deals real bronze players in real
positions.

REVERSAL, deliberate: nation/team/league were being sent as ZERO so the client would
fill its own values (the merge fills those three only when they arrive zero). Now
that we hold the game's own numbers there is nothing to gain from zeros, and they
actively hurt -- club-stats drill-downs bucket by the item's own nation and leagueId,
so a club full of zeros would have quietly emptied the per-nation and per-league
panels fixed the day before. Send the real values.

The old rating-index path is kept as a fallback so the pool still builds without
data/pool.json, and it now says out loud which of the two it used, because one is a
measurement and the other is a guess.

439 + 61 checks green.

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-05 09:08:29 -07:00
parent 49733f79d1
commit 9a0a76c9f4
156 changed files with 11878 additions and 1 deletions
+42 -1
View File
@@ -113,6 +113,27 @@ def _load(name, default):
ROSTER = _load("roster.json", [])
KNOWN_POSITIONS = {int(k): v for k, v in _load("positions.json", {}).items()}
# data/pool.json -- the REAL thing, and it supersedes everything below it.
#
# Built 2026-08-05 from the game's own resident database (data/tables/*.json, dumped
# read-only by tools/db_dump.py, then tools/build_player_facts.py). Per player it
# carries the MEASURED position (players.preferredposition1), nationality, teamid,
# leagueid (via leagueteamlinks) and the six card attributes.
#
# The six attributes are not columns: they are a weighted sum of the 29 base
# attributes, and the weights come from the game's OWN `playerattributesmapping`
# table rather than from published formulas. The result checks out against real FIFA
# 17 cards: Messi 89/90/86/96/26/61 and Ibrahimovic 72/90/81/85/31/86 are exact,
# Suarez is one off on physical, Ronaldo within two on pace and shooting.
#
# NOTE THE REVERSAL on nation/team/league. When those fields were unknown we sent
# ZERO so the client would fill its own values (the merge fills them only when they
# arrive zero). Now that we hold the game's own numbers there is nothing to gain, and
# zeros actively HURT: our club-stats drill-downs bucket by the item's own nation and
# leagueId, so a club full of zeros would have emptied the per-nation and per-league
# panels that were fixed yesterday. Send the real values.
POOL_FACTS = _load("pool.json", [])
# Hand-checked positions, carried over from the curated pool this file replaces.
# They are KNOWLEDGE, not measurement, which is why they rank below the codes the
# game itself supplied. They exist because a synthetic position is unnoticeable on
@@ -148,6 +169,21 @@ def _attrs(rating, pos):
def _build():
if POOL_FACTS:
pool = []
for r in POOL_FACTS:
pid, rating = r["id"], r["rating"]
if not pid or rating <= 0:
# A zero id is not a harmless skip: the registrar writes
# *(item+0x10) = 0 and the card view-model dereferences it with no
# null check, so a zero id reaching the card UI is a crash.
continue
pool.append((pid, rating, r["pos"], r["nation"], r["league"], r["team"],
list(r["attrs"])))
return pool
# Fallback: the rating-index roster, with synthetic positions and attributes.
# Kept so the pool still builds if data/pool.json is missing, but everything it
# produces below is a guess where the block above is a measurement.
pool = []
for r in ROSTER:
pid, rating = r["id"], r["rating"]
@@ -203,7 +239,12 @@ if __name__ == "__main__":
print(" %-7s %5d ratings %d-%d" % (name, len(sel),
min(p[1] for p in sel),
max(p[1] for p in sel)))
print("known positions: %d (the rest are synthetic)" % len(KNOWN_POSITIONS))
if POOL_FACTS:
print("source: data/pool.json -- positions, nation, club, league and all six "
"attributes MEASURED from the game's own database")
else:
print("source: data/roster.json FALLBACK -- %d real positions, the rest "
"synthetic, attributes derived from rating" % len(KNOWN_POSITIONS))
print("\ntop 10 by rating:")
for p in sorted(POOL, key=lambda x: -x[1])[:10]:
print(" %-7s %-3s %-4s %-26s %s" % (p[0], p[1], p[2], name_of(p[0]), p[6]))