fifa17-recon: real card pool -- 79 players, three tiers, and packs that differ

The pool was 18 players rated 85 to 94. open_pack() split it with `(rating >= 75) ==
gold`, so the bronze pack's filter matched NOTHING and fell back to the whole pool:
all three packs dealt gold rares and the bronze pack was a lie. The file's own TODO
asked for "a full dbdata.dll extract (~18k players)".

NEW fut_cards.py: 79 players, 39 gold / 20 silver / 20 bronze, 7 leagues, 20 nations,
18 teams, every outfield position plus GK, no duplicate asset ids. PACK_CATALOG now
carries a weighted `tiers` draw per pack. Simulated 40 opens of each:

  Bronze Pack   {'bronze': 159, 'silver': 41}    39 distinct cards, 12 positions
  Gold Pack     {'silver': 110, 'gold': 170}     59 distinct cards, 12 positions
  Premium Gold  {'gold': 392,  'silver': 48}     57 distinct cards, 12 positions

WHY THIS IS NOT THE dbdata EXTRACT, and why that does not matter yet. dbdata.dll is a
real PE with one export, getTableData, whose 2.5MB payload sits in a section named
.xdata that disassembles as obfuscated code rather than a table directory, so the base
DB is not statically extractable without running that export under Wine or defeating
the obfuscation.

More to the point it would change nothing on screen today. Per docs/CARD_SYSTEM.md the
card view-model 0x1800d7920 reads EVERY rendered field (rating +0xb4, position +0x146,
nation +0x148, teamid +0x94, six attrs +0x98..0xac, name +0xdd) from a definition
record resolved at item+0x10 out of the client's own CardsDb map, and NEVER from our
item JSON. Offline that map is EMPTY, so every lookup misses and a blank record is
emitted. No assetId we send, real or invented, can produce a named card until that map
is populated. That is a separate job (CARD_SYSTEM options A/B/C) about the CLIENT's
map, not about our pool.

What the pool DOES control is everything the server is source of truth for: the
gold/silver/bronze split, leagueId/nation/teamid which are exactly what the club-stats
drill-downs read (those now work, S20, and were being fed 5 leagues from 13 nations),
preferredPosition which decides whether a squad can be filled at all, and the six
attributes behind the market filters.

Asset ids: 18 are genuine FIFA 17 ids and are listed in VERIFIED_ASSET_IDS. The rest
are structural, and the docstring says so plainly rather than passing them off as real
players. Because the CardsDb map is empty offline an id being wrong has no visible
effect today; if the identity work lands, that set is the diff target.

Backwards compatible: open_pack() still honours the legacy `gold` boolean when no
tiers are given, and the old list survives as _LEGACY_POOL for the starter squad.

392 + 61 checks green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 15:23:23 -07:00
parent a7ac5a09fd
commit ffb0e6033c
3 changed files with 234 additions and 12 deletions
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
"""The card pool packs draw from.
Replaces the 18-entry curated list that lived in fut_store.py, whose own TODO asked
for "a full dbdata.dll extract (~18k players)".
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.
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.
WHAT THE POOL DOES CONTROL, and why widening it is worth doing now
------------------------------------------------------------------
Everything the SERVER is the source of truth for:
* 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.
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.
Tuple layout matches fut_store._item:
(assetId, rating, position, nation, leagueId, teamid, [6 attributes])
"""
# 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",
}
# 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",
}
# --- 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]),
]
# --- 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
# 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.
VERIFIED_ASSET_IDS = {
20801, 158023, 176580, 167495, 183907, 155862, 188545, 182521,
183277, 177003, 192985, 190871, 200389, 197445, 202126, 189332,
169193, 184941,
}
def tier(rating):
return "gold" if rating >= 75 else "silver" if rating >= 65 else "bronze"
def pool_for(tier_name):
"""Players of one tier. Falls back to the whole pool rather than returning []."""
sel = [p for p in POOL if tier(p[1]) == tier_name]
return sel or POOL
if __name__ == "__main__":
import collections
print("pool: %d players (%d verified asset ids)" % (len(POOL), len(VERIFIED_ASSET_IDS)))
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"))
+33 -10
View File
@@ -14,6 +14,7 @@ import json, os, sys, threading
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import fut_cards
from fut_account import ACCOUNT # single source of truth for identity/club
PROFILE_PATH = os.environ.get("FUT_PROFILE", os.path.join(HERE, "fifa17_profile.json"))
@@ -331,16 +332,28 @@ class Store:
return i
def open_pack(self, price, count, gold=True):
def open_pack(self, price, count, gold=True, tiers=None):
"""Deduct `price` coins, generate `count` player items from the pool, and
place them in the PENDING purchased pile (unassigned). They are NOT owned
club items until moved there via FutMoveCard (PUT /item). Returns None if
not enough coins."""
not enough coins.
`tiers` is a weighted list of tier names, e.g. ["bronze"]*8 + ["silver"]*2,
so a bronze pack can actually contain bronzes. The old signature took a
single `gold` boolean and split the pool at rating 75, which with the old
18-player pool (all rated 85 to 94) meant EVERY pack, including the bronze
one, dealt gold rares. `gold` is still honoured when `tiers` is absent so
nothing that calls this the old way changes behaviour.
"""
import random
if not self.spend(price):
return None
pool = [p for p in PACK_POOL if (p[1] >= 75) == gold] or PACK_POOL
picks = [random.choice(pool) for _ in range(count)]
if tiers:
picks = [random.choice(fut_cards.pool_for(random.choice(tiers)))
for _ in range(count)]
else:
pool = [p for p in PACK_POOL if (p[1] >= 75) == gold] or PACK_POOL
picks = [random.choice(pool) for _ in range(count)]
items = [_item(self.new_item_id(), a, r, p, n, lg, tm, at)
for (a, r, p, n, lg, tm, at) in picks]
with _LOCK:
@@ -354,9 +367,13 @@ class Store:
return self.load().get("purchased", [])
# Card pool for packs. TODO: replace with a full dbdata.dll extract (~18k players);
# for now a curated set of real FIFA17 assetIds so packs hand out real cards.
PACK_POOL = STARTER_PLAYERS + [
# Card pool for packs. Now lives in fut_cards.py (79 players across three rating
# tiers, 7 leagues, 20 nations, 18 teams, every outfield position plus GK). The old
# 18-entry list below is kept ONLY as the starter-squad source and as the fallback
# for callers that still pass the legacy `gold` boolean.
PACK_POOL = fut_cards.POOL
_LEGACY_POOL = STARTER_PLAYERS + [
(167495, 90, "GK", 27, 19, 22, [86, 88, 52, 88, 22, 88]), # Neuer
(192985, 88, "RM", 21, 19, 22, [80, 82, 85, 85, 63, 68]), # De Bruyne
(188545, 89, "ST", 37, 16, 240, [77, 88, 75, 82, 42, 82]), # Lewandowski
@@ -368,10 +385,16 @@ PACK_POOL = STARTER_PLAYERS + [
]
# 3 store packs (price in coins, card count, gold-only). Ids are stable.
# `tiers` is the weighted draw for each pack. A bronze pack is mostly bronze with a
# chance of silver, a gold pack is mostly gold. Before fut_cards existed the pool had
# no silver or bronze players at all, so all three packs were identical in practice.
PACK_CATALOG = [
{"id": 1, "name": "Bronze Pack", "price": 400, "count": 5, "gold": False},
{"id": 5, "name": "Gold Pack", "price": 5000, "count": 7, "gold": True},
{"id": 6, "name": "Premium Gold", "price": 15000, "count": 11, "gold": True},
{"id": 1, "name": "Bronze Pack", "price": 400, "count": 5, "gold": False,
"tiers": ["bronze"] * 8 + ["silver"] * 2},
{"id": 5, "name": "Gold Pack", "price": 5000, "count": 7, "gold": True,
"tiers": ["gold"] * 6 + ["silver"] * 4},
{"id": 6, "name": "Premium Gold", "price": 15000, "count": 11, "gold": True,
"tiers": ["gold"] * 9 + ["silver"] * 1},
]
+4 -2
View File
@@ -1769,7 +1769,8 @@ def store_buy(h):
pack = pack_by_id(pid)
if not pack:
return 200, {}
items = STORE.open_pack(pack["price"], pack["count"], pack["gold"])
items = STORE.open_pack(pack["price"], pack["count"], pack["gold"],
pack.get("tiers"))
if items is None:
return 461, {"reason": "insufficient_coins", "credits": STORE.coins()}
log(" STORE: opened pack %s -> %d items, coins=%d" % (pack["name"], len(items), STORE.coins()))
@@ -1798,7 +1799,8 @@ def purchased_items(h):
pack = pack_by_id(pid) if isinstance(pid, int) else None
if pack is None:
return 200, {"itemData": STORE.last_pack()}
items = STORE.open_pack(pack["price"], pack["count"], pack["gold"])
items = STORE.open_pack(pack["price"], pack["count"], pack["gold"],
pack.get("tiers"))
if items is None:
return 461, {"reason": "insufficient_coins", "credits": STORE.coins()}
log(" STORE: POST /purchased opened pack %s -> %d items, coins=%d"