diff --git a/fifa17-recon/tools/fut_cards.py b/fifa17-recon/tools/fut_cards.py new file mode 100644 index 0000000..bc72f5f --- /dev/null +++ b/fifa17-recon/tools/fut_cards.py @@ -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")) diff --git a/fifa17-recon/tools/fut_store.py b/fifa17-recon/tools/fut_store.py index 7f88ccf..455d48e 100644 --- a/fifa17-recon/tools/fut_store.py +++ b/fifa17-recon/tools/fut_store.py @@ -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}, ] diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index b946fa3..bc69b31 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -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"