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
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
{
"20801": 27,
"41236": 25,
"48717": 0,
"48940": 0,
"52091": 7,
"53612": 5,
"53914": 5,
"106231": 25,
"108080": 3,
"112253": 10,
"120533": 5,
"121944": 14,
"137186": 5,
"138956": 5,
"139668": 0,
"139720": 5,
"139968": 0,
"142780": 5,
"142784": 3,
"143745": 14,
"146530": 3,
"146562": 18,
"146954": 14,
"150724": 0,
"152729": 5,
"153244": 25,
"156616": 16,
"157481": 5,
"158121": 0,
"159147": 5,
"161648": 18,
"162240": 14,
"162895": 14,
"163711": 10,
"165153": 25,
"167431": 14,
"168354": 0,
"168651": 14,
"171877": 14,
"172879": 5,
"175943": 27,
"176769": 25,
"177413": 14,
"177610": 5,
"178509": 25,
"179783": 0,
"179944": 5,
"181458": 16,
"182494": 0,
"183497": 0,
"184144": 16,
"184432": 7,
"185239": 5,
"188152": 18,
"189125": 16,
"189461": 14,
"189560": 10,
"190547": 5,
"191740": 14
}
+19
View File
@@ -0,0 +1,19 @@
{
"155862": "CB",
"158023": "RW",
"167495": "GK",
"176580": "ST",
"177003": "CM",
"182521": "CM",
"183277": "LM",
"183907": "CB",
"184941": "CB",
"188545": "ST",
"189332": "LB",
"190871": "LW",
"192985": "RM",
"197445": "LB",
"200389": "GK",
"202126": "ST",
"20801": "LW"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+171 -159
View File
@@ -1,175 +1,185 @@
#!/usr/bin/env python3
"""The card pool packs draw from.
# -*- coding: utf-8 -*-
"""The FUT card pool: the REAL FIFA 17 roster, 17,547 players.
Replaces the 18-entry curated list that lived in fut_store.py, whose own TODO asked
for "a full dbdata.dll extract (~18k players)".
WHAT CHANGED, AND WHY IT MATTERS
--------------------------------
This module used to hold 79 hand-written rows whose asset ids were mostly
invented, on the premise (from an older CARD_SYSTEM.md) that the client's card
map is empty offline so no id could ever render. That premise was wrong.
WHY THIS IS NOT THAT EXTRACT, and why it does not need to be yet
----------------------------------------------------------------
`dbdata.dll` is a real PE with a single export, `getTableData`, and its 2.5MB payload
sits in a section named `.xdata` that disassembles as obfuscated code rather than a
table directory. So the base player DB is not statically extractable without either
running that export under Wine or defeating the obfuscation. Neither is cheap.
Card identity does not come from us. The client inserts every item we serve into
its CardsDb map and, just before that, merges in its OWN local players table
keyed on `resourceId & 0xffffff`. An invented id renders as a blank generic card;
a real one renders as a real player. Proven live: a pack showed SILVA and NOWAK
with real names, badges and flags beside three blanks at rating 50.
More importantly it would not currently change anything on screen. Per
docs/CARD_SYSTEM.md, the card view-model at 0x1800d7920 reads EVERY rendered field
(rating +0xb4, position +0x146, nation +0x148, teamid +0x94, six attributes
+0x98..0xac, name +0xdd) from a player-definition record resolved at `item+0x10`, and
NEVER from our item JSON. That record comes from the client's own CardsDb map at
`CardsDb_obj+0x160c0`, keyed by resourceId, and OFFLINE THAT MAP IS EMPTY: every
lookup misses and a blank default record is emitted. So no assetId we send, real or
invented, can currently produce a named card. Fixing that is a separate and much
larger job (options A/B/C in CARD_SYSTEM.md) and it is about populating the client's
map, not about our pool.
So the pool is now built from the game's own roster, extracted from a running
FIFA17.exe by tools/dbdata_extract.py into data/roster.json. It was cross-checked
against a completely independent method -- the sweep oracle in
tools/sweep_collect.py, which reads back the identity the CLIENT resolved -- and
573 of 573 overlapping names agreed exactly.
WHAT THE POOL DOES CONTROL, and why widening it is worth doing now
------------------------------------------------------------------
Everything the SERVER is the source of truth for:
WHICH FIELDS ARE REAL AND WHICH ARE NOT. Be honest about this when reading a card:
* rating, which drives the gold/silver/bronze split. The old pool was 18 players
rated 85 to 94, so every pack produced gold rares and the bronze pack was a lie.
* leagueId / nation / teamid, which are exactly what the club-stats drill-down
screens read. Those screens now work (REBUILD_RESEARCH S20) and were being fed
from 5 leagues and 13 nations.
* preferredPosition, which decides whether a squad can actually be filled. A pool
with no goalkeepers below 87 makes a bronze squad impossible.
* the six attributes, which feed the market's attribute filters.
playerid REAL data/roster.json
rating REAL same
name REAL resolved by the client from the id; we never send a name
club REAL we send teamid 0 and the client fills its own value
nation REAL we send nation 0, same mechanism
league REAL the client always recomputes leagueid on a DB hit
position PARTLY 59 ids are known (data/positions.json); the rest are
SYNTHETIC, assigned deterministically per id
attributes SYNTH derived from rating and position
ON THE ASSET IDS
----------------
Ids marked (real) are genuine FIFA 17 player ids and were already in use here or are
well established. The remainder are structural: they occupy a valid-looking id space
so the pool has breadth. Because the CardsDb map is empty offline, an id being wrong
has NO user-visible effect today: every card renders generic either way. If the
identity work ever lands, this table is the thing to correct, and `verified` marks
which rows can be trusted. Do not present the unverified ones as real players.
The merge fills nation/teamid ONLY when they arrive as zero, and never touches
rating, position or attributes. That asymmetry is the whole design of this file:
send zero for everything the client knows better than us, and send our own value
only where the client has nothing.
Tuple layout matches fut_store._item:
(assetId, rating, position, nation, leagueId, teamid, [6 attributes])
THE MISSING COLUMNS, AND THE LEADS FOR THEM
-------------------------------------------
position / nationality / teamId / attributes are NOT in the rating index. Two
live sources exist and BOTH are per-materialised-card caches, not tables:
* the 0x180-stride resolved card records (attributes + names), and
* a 32-byte-stride keyed container, entries {playerId, position | hash<<32,
rating, ?}, found at 0x42e8dbe8 inside the 238 MB heap region.
data/positions.json comes from the second one. Sweeping 5,000 ids did NOT
populate it, so it caches what the game itself materialises rather than what we
ask about. 59 entries survived validation (each entry's rating had to match the
roster's). Anyone extending this: a full-roster position source has not been
found, and the FUT rating index does not contain one.
Position codes are the standard FIFA enum, decoded against our own club cards
read live: GK 0, CB 5, LB 7, CM 14, LM 16, RW 23, ST 25, LW 27.
"""
import json
import os
# League ids seen in live traffic and in the seed: 4, 13, 16, 19, 53.
LEAGUES = {
13: "Premier League",
16: "Ligue 1",
19: "Bundesliga",
31: "Serie A",
53: "LaLiga",
4: "Eredivisie",
14: "EFL Championship",
_HERE = os.path.dirname(os.path.abspath(__file__))
_DATA = os.path.join(_HERE, "..", "data")
# Position code -> the string the client's parser expects in preferredPosition.
POSITION_BY_CODE = {
0: "GK", 1: "SW", 2: "RWB", 3: "RB", 4: "CB", 5: "CB", 6: "CB",
7: "LB", 8: "LWB", 9: "CDM", 10: "CDM", 11: "CDM", 12: "RM",
13: "CM", 14: "CM", 15: "CM", 16: "LM", 17: "CAM", 18: "CAM",
19: "CAM", 20: "RF", 21: "CF", 22: "LF", 23: "RW", 24: "ST",
25: "ST", 26: "ST", 27: "LW",
}
# Nation ids used below, from the seed's own data.
NATIONS = {
7: "Belgium", 14: "England", 18: "France", 21: "Germany", 27: "Netherlands",
34: "Croatia", 37: "Poland", 38: "Portugal", 39: "Rep. Ireland", 40: "Austria",
44: "Slovenia", 45: "Spain", 52: "Argentina", 54: "Brazil", 60: "Uruguay",
95: "Colombia", 117: "Ghana", 25: "Italy", 36: "Norway", 46: "Sweden",
# Synthetic-position distribution, shaped like a real squad (one keeper, four at
# the back, four in midfield, three forward) so a random pack looks like a
# football team rather than eleven strikers.
_SYNTH_POSITIONS = (["GK"] +
["RB", "CB", "CB", "LB"] +
["CDM", "CM", "CM", "CAM"] +
["RW", "ST", "LW"])
# Attribute profiles: (pace, shooting, passing, dribbling, defending, physical)
# as multipliers on the rating. The GK profile stands in for the six keeper
# stats the card face shows in that slot instead.
_PROFILE = {
"GK": (0.68, 0.70, 0.40, 0.66, 0.20, 0.68),
"RB": (1.02, 0.72, 0.90, 0.92, 1.00, 0.95),
"LB": (1.02, 0.72, 0.90, 0.92, 1.00, 0.95),
"RWB": (1.05, 0.75, 0.92, 0.95, 0.96, 0.92),
"LWB": (1.05, 0.75, 0.92, 0.95, 0.96, 0.92),
"CB": (0.82, 0.55, 0.78, 0.75, 1.05, 1.05),
"SW": (0.82, 0.55, 0.78, 0.75, 1.05, 1.05),
"CDM": (0.85, 0.75, 0.98, 0.92, 1.00, 1.00),
"CM": (0.90, 0.85, 1.02, 0.98, 0.88, 0.92),
"RM": (1.05, 0.88, 0.98, 1.02, 0.72, 0.85),
"LM": (1.05, 0.88, 0.98, 1.02, 0.72, 0.85),
"CAM": (0.95, 0.92, 1.02, 1.04, 0.62, 0.82),
"RW": (1.08, 0.92, 0.95, 1.05, 0.60, 0.80),
"LW": (1.08, 0.92, 0.95, 1.05, 0.60, 0.80),
"RF": (1.02, 0.98, 0.95, 1.04, 0.58, 0.85),
"LF": (1.02, 0.98, 0.95, 1.04, 0.58, 0.85),
"CF": (1.00, 1.00, 0.95, 1.02, 0.58, 0.88),
"ST": (1.00, 1.05, 0.85, 0.98, 0.45, 0.95),
}
# --- GOLD, 75+ ---------------------------------------------------------------
# The eight below were in the previous pool and are (real).
GOLD = [
(20801, 94, "LW", 38, 53, 243, [90, 93, 82, 91, 33, 80]), # (real) Ronaldo
(158023, 93, "RW", 52, 53, 241, [89, 90, 86, 96, 26, 61]), # (real) Messi
(176580, 92, "ST", 60, 53, 241, [83, 90, 79, 87, 42, 80]), # (real) Suarez
(167495, 90, "GK", 21, 19, 21, [86, 88, 52, 88, 22, 88]), # (real) Neuer
(183907, 90, "CB", 21, 19, 21, [80, 58, 70, 71, 89, 85]), # (real) Boateng
(155862, 89, "CB", 45, 53, 243, [77, 62, 72, 74, 87, 84]), # (real) Ramos
(188545, 89, "ST", 37, 19, 21, [77, 88, 75, 82, 42, 82]), # (real) Lewandowski
(182521, 88, "CM", 21, 19, 21, [61, 79, 89, 82, 72, 74]), # (real) Kroos
(183277, 88, "LM", 7, 13, 5, [89, 80, 82, 92, 37, 62]), # (real) Hazard
(177003, 88, "CM", 34, 53, 243, [65, 78, 88, 79, 71, 66]), # (real) Modric
(192985, 88, "RM", 7, 13, 10, [80, 82, 85, 85, 63, 68]), # (real) De Bruyne
(190871, 87, "LW", 54, 16, 73, [90, 78, 80, 88, 36, 61]), # (real) Neymar
(200389, 87, "GK", 44, 53, 240, [86, 88, 47, 88, 25, 86]), # (real) Oblak
(197445, 87, "LB", 40, 19, 21, [86, 68, 79, 82, 82, 79]), # (real) Alaba
(202126, 86, "ST", 14, 13, 5, [79, 84, 74, 82, 45, 79]), # (real) Kane
(189332, 86, "LB", 45, 53, 241, [92, 66, 78, 84, 80, 71]), # (real) Alba
(169193, 87, "CDM", 45, 19, 21, [70, 66, 80, 78, 82, 84]), # (real) Alonso
(184941, 85, "CB", 14, 4, 5, [72, 40, 55, 60, 86, 85]), # (real)
# structural, gold tier, spread across the remaining leagues and positions
(201153, 85, "CAM", 18, 16, 73, [82, 79, 84, 86, 45, 62]),
(204485, 84, "RB", 45, 31, 44, [84, 55, 74, 76, 80, 74]),
(198710, 84, "CDM", 25, 31, 47, [66, 62, 79, 74, 83, 80]),
(193080, 83, "GK", 14, 13, 11, [82, 84, 45, 83, 24, 82]),
(192119, 83, "ST", 95, 13, 18, [83, 84, 71, 81, 38, 74]),
(186345, 82, "CM", 27, 4, 10, [70, 71, 82, 79, 70, 72]),
(207439, 82, "RW", 54, 31, 45, [88, 76, 77, 85, 34, 60]),
(178603, 81, "CB", 46, 19, 175, [70, 48, 62, 63, 82, 83]),
(199304, 81, "LM", 36, 13, 9, [85, 72, 76, 80, 45, 66]),
(210035, 80, "CAM", 39, 14, 15, [76, 74, 81, 80, 48, 63]),
(194765, 80, "ST", 117, 16, 66, [80, 81, 68, 76, 40, 78]),
(205452, 79, "RB", 34, 31, 48, [82, 52, 71, 73, 77, 72]),
(212188, 79, "GK", 18, 16, 73, [80, 82, 43, 80, 22, 79]),
(196889, 78, "CDM", 21, 19, 175, [64, 60, 76, 71, 79, 78]),
(203551, 78, "LW", 95, 53, 481, [86, 73, 74, 82, 32, 58]),
(209331, 77, "CB", 25, 31, 47, [66, 45, 58, 61, 79, 80]),
(191043, 77, "CM", 14, 13, 11, [68, 70, 78, 75, 68, 71]),
(213345, 76, "RM", 54, 4, 10, [84, 70, 73, 79, 40, 60]),
(188350, 76, "ST", 46, 14, 15, [74, 78, 65, 72, 36, 76]),
(206113, 75, "LB", 38, 53, 481, [83, 58, 70, 73, 74, 70]),
(215316, 75, "GK", 52, 31, 44, [78, 79, 41, 77, 21, 76]),
]
# --- SILVER, 65 to 74 --------------------------------------------------------
SILVER = [
(214119, 74, "ST", 14, 14, 15, [76, 74, 63, 70, 34, 74]),
(208618, 74, "CM", 18, 16, 66, [70, 66, 74, 72, 66, 68]),
(211029, 73, "CB", 27, 4, 10, [64, 44, 55, 58, 75, 76]),
(216543, 73, "RW", 95, 31, 48, [82, 68, 68, 75, 32, 58]),
(205781, 72, "GK", 46, 19, 175, [74, 75, 40, 73, 20, 73]),
(219877, 72, "LM", 39, 14, 15, [80, 65, 68, 73, 38, 60]),
(207112, 71, "CDM", 25, 31, 47, [62, 55, 70, 66, 73, 74]),
(218455, 71, "ST", 36, 13, 9, [73, 72, 60, 68, 32, 72]),
(210998, 70, "RB", 21, 19, 175, [76, 48, 65, 67, 70, 68]),
(222301, 70, "CAM", 54, 4, 10, [78, 66, 71, 74, 38, 58]),
(213776, 69, "CB", 45, 53, 481, [62, 42, 52, 56, 71, 73]),
(220144, 69, "LW", 18, 16, 66, [81, 64, 66, 72, 30, 56]),
(217650, 68, "CM", 14, 14, 15, [66, 62, 69, 67, 63, 65]),
(223089, 68, "GK", 34, 31, 48, [70, 71, 38, 69, 19, 70]),
(211934, 67, "ST", 117, 16, 66, [74, 70, 58, 66, 30, 71]),
(224512, 67, "LB", 46, 4, 10, [78, 50, 63, 66, 66, 64]),
(215008, 66, "RM", 39, 13, 9, [79, 62, 65, 70, 34, 57]),
(225367, 66, "CDM", 27, 31, 47, [60, 52, 66, 63, 68, 70]),
(218902, 65, "CB", 36, 19, 175, [60, 40, 50, 54, 68, 70]),
(226118, 65, "CAM", 95, 14, 15, [74, 63, 67, 70, 36, 55]),
]
def _load(name, default):
try:
with open(os.path.join(_DATA, name)) as f:
return json.load(f)
except (IOError, ValueError):
return default
# --- BRONZE, under 65 --------------------------------------------------------
BRONZE = [
(227403, 64, "ST", 14, 14, 15, [70, 66, 55, 62, 28, 68]),
(228156, 64, "GK", 39, 13, 9, [66, 67, 35, 65, 18, 66]),
(229011, 63, "CB", 46, 4, 10, [58, 38, 48, 52, 65, 67]),
(230245, 63, "LM", 95, 16, 66, [76, 58, 61, 66, 30, 52]),
(231088, 62, "CM", 27, 31, 47, [60, 56, 63, 61, 58, 60]),
(232517, 62, "RB", 36, 19, 175, [72, 44, 58, 60, 62, 61]),
(233402, 61, "ST", 117, 14, 15, [70, 64, 52, 59, 26, 66]),
(234199, 61, "CAM", 18, 16, 66, [70, 58, 63, 65, 32, 51]),
(235066, 60, "GK", 34, 31, 48, [62, 63, 33, 61, 17, 62]),
(236743, 60, "LB", 45, 53, 481, [72, 42, 56, 58, 60, 58]),
(237510, 59, "CB", 25, 4, 10, [56, 36, 46, 50, 62, 64]),
(238228, 59, "RW", 54, 13, 9, [76, 58, 58, 64, 26, 50]),
(239104, 58, "CDM", 21, 19, 175, [56, 48, 60, 57, 63, 65]),
(240377, 58, "ST", 36, 14, 15, [68, 62, 50, 57, 24, 64]),
(241255, 57, "RM", 39, 16, 66, [74, 55, 58, 62, 28, 49]),
(242018, 57, "CB", 46, 31, 47, [54, 34, 44, 48, 60, 62]),
(243390, 56, "GK", 27, 13, 9, [60, 61, 31, 59, 16, 60]),
(244107, 56, "CM", 95, 4, 10, [58, 52, 59, 57, 55, 57]),
(245266, 55, "LW", 117, 53, 481, [74, 54, 55, 60, 24, 48]),
(246013, 55, "LB", 25, 19, 175, [70, 40, 54, 56, 58, 56]),
]
POOL = GOLD + SILVER + BRONZE
ROSTER = _load("roster.json", [])
KNOWN_POSITIONS = {int(k): v for k, v in _load("positions.json", {}).items()}
# 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
# an unknown 62-rated defender and glaring on Neuer, and the famous players are
# precisely the ones a pack shows off.
CURATED_POSITIONS = {int(k): v for k, v in _load("positions_curated.json", {}).items()}
# Rows whose assetId is a genuine FIFA 17 player id. Everything else is structural,
# see the module docstring. Kept as data so a future dbdata extract can diff against it.
def _position(pid):
"""The real position where the game told us one, else curated, else synthetic.
Deterministic in the id, so a player never changes shape between packs or
between runs.
"""
code = KNOWN_POSITIONS.get(pid)
if code is not None:
return POSITION_BY_CODE.get(code, "ST")
if pid in CURATED_POSITIONS:
return CURATED_POSITIONS[pid]
return _SYNTH_POSITIONS[pid % len(_SYNTH_POSITIONS)]
def _attrs(rating, pos):
prof = _PROFILE.get(pos, _PROFILE["CM"])
out = []
for i, mult in enumerate(prof):
# A small, stable per-player wobble so two 82-rated strikers are not
# byte-identical. Seeded by rating and slot, never by wall-clock, so the
# pool is reproducible.
v = int(round(rating * mult)) + ((rating * 7 + i * 13) % 5) - 2
out.append(max(1, min(99, v)))
return out
def _build():
pool = []
for r in ROSTER:
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 item+0x10
# with no null check, so a zero id reaching the card UI is a crash.
continue
pos = _position(pid)
# nation / league / team are ZERO on purpose -- that is what makes the
# client fill in the real ones. Do not "improve" this by guessing them.
pool.append((pid, rating, pos, 0, 0, 0, _attrs(rating, pos)))
return pool
POOL = _build()
# Kept for the record: the old hand-written "verified" set. 169193 is in it and
# is NOT a real FIFA 17 player -- the client resolves it to the database's empty
# placeholder row, which reads as "Jamal Blackman" on every card. That is exactly
# the failure this rebuild removes, and two independent methods agreed on it. Do
# not restore this set as a source of truth; it is here so older notes stay
# traceable.
VERIFIED_ASSET_IDS = {
20801, 158023, 176580, 167495, 183907, 155862, 188545, 182521,
183277, 177003, 192985, 190871, 200389, 197445, 202126, 189332,
169193, 184941,
}
NAME_BY_ID = {r["id"]: (r["common"] or ("%s %s" % (r["first"], r["last"])).strip())
for r in ROSTER}
def tier(rating):
return "gold" if rating >= 75 else "silver" if rating >= 65 else "bronze"
@@ -181,17 +191,19 @@ def pool_for(tier_name):
return sel or POOL
def name_of(pid):
"""For LOGS only. The name a player actually sees comes from the client."""
return NAME_BY_ID.get(pid, "?")
if __name__ == "__main__":
import collections
print("pool: %d players (%d verified asset ids)" % (len(POOL), len(VERIFIED_ASSET_IDS)))
print("pool: %d players from the real FIFA 17 roster" % len(POOL))
for name in ("gold", "silver", "bronze"):
sel = pool_for(name)
print(" %-7s %3d ratings %d-%d" % (name, len(sel),
min(p[1] for p in sel), max(p[1] for p in sel)))
print(" leagues %2d %s" % (len({p[4] for p in POOL}),
sorted({p[4] for p in POOL})))
print(" nations %2d" % len({p[3] for p in POOL}))
print(" teams %2d" % len({p[5] for p in POOL}))
print(" positions %s" % sorted({p[2] for p in POOL}))
dupes = [a for a, n in collections.Counter(p[0] for p in POOL).items() if n > 1]
print(" duplicate asset ids: %s" % (dupes or "none"))
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))
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]))
+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