#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Turn the raw table dumps written by db_dump.py into ONE file the FUT card pool can consume: data/player_facts.json. Input data/tables/{players,teamplayerlinks,teams,leagues,nations, leagueteamlinks,playerattributesmapping}.json Output data/player_facts.json {"meta": {...}, "players": [ {"id":20801,"rating":94,"pos":27,"pos2":16,"pos3":25,"pos4":-1, "nation":38,"team":243,"league":53, "attrs":[90,93,82,91,33,80], # PAC SHO PAS DRI DEF PHY "gk":false}, ... ]} THE SIX CARD ATTRIBUTES ARE NOT COLUMNS. `players` stores the 29 base attributes; the six numbers a FUT card shows are a weighted sum of them. The weights are not guessed here -- they are read out of the game's own `playerattributesmapping` table, which maps each base attribute id to its percentage contribution to speed / shooting / passing / dribbling / defending / physical (and to the five gk* stats). Every column of that table sums to exactly 100. The one inference this file makes is attributeid -> players column name, and it is safe for this purpose: wherever two attributes could be swapped (marking vs standingtackle at 30 each, shotpower vs longshots at 20 each, ...) they carry EQUAL weight, so the computed six are identical either way. The assignments that actually move a number -- 45/55 acceleration/sprintspeed, 45 finishing, 50 dribbling, 35 shortpassing, 30 ballcontrol, 50 strength, 25 stamina, 20 aggression, 20 interceptions, 15 longpassing -- are each the unique attribute with that weight. VALIDATION (see the report): overallrating in `players` agrees with data/roster.json, extracted from a completely different memory structure, on 17547 of 17547 shared players. """ import json import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) TABLES = os.path.join(HERE, '..', 'data', 'tables') OUT = os.path.join(HERE, '..', 'data', 'player_facts.json') # FUT card slot -> [(players column, percent)], read off playerattributesmapping. OUTFIELD = [ ('pace', [('acceleration', 45), ('sprintspeed', 55)]), ('shooting', [('finishing', 45), ('shotpower', 20), ('longshots', 20), ('positioning', 5), ('volleys', 5), ('penalties', 5)]), ('passing', [('shortpassing', 35), ('vision', 20), ('crossing', 20), ('longpassing', 15), ('freekickaccuracy', 5), ('curve', 5)]), ('dribbling', [('dribbling', 50), ('ballcontrol', 30), ('agility', 10), ('balance', 5), ('reactions', 5)]), ('defending', [('marking', 30), ('standingtackle', 30), ('interceptions', 20), ('headingaccuracy', 10), ('slidingtackle', 10)]), ('physical', [('strength', 50), ('stamina', 25), ('aggression', 20), ('jumping', 5)]), ] # Keeper card face. The five gk* columns are used at 100% -- they ARE the card # numbers, no arithmetic. The speed slot is the only weighted one. GK = [ ('diving', [('gkdiving', 100)]), ('handling', [('gkhandling', 100)]), ('kicking', [('gkkicking', 100)]), ('reflexes', [('gkreflexes', 100)]), ('speed', [('acceleration', 60), ('sprintspeed', 40)]), ('positioning', [('gkpositioning', 100)]), ] POSITION_NAMES = ['GK', 'SW', 'RWB', 'RB', 'RCB', 'CB', 'LCB', 'LB', 'LWB', 'RDM', 'CDM', 'LDM', 'RM', 'RCM', 'CM', 'LCM', 'LM', 'RAM', 'CAM', 'LAM', 'RF', 'CF', 'LF', 'RW', 'RS', 'ST', 'LS', 'LW'] def load(name): with open(os.path.join(TABLES, name + '.json')) as fh: return json.load(fh) def weighted(row, spec): return int(round(sum(row[c] * w for c, w in spec) / 100.0)) def main(): players = load('players')['rows'] tpl = load('teamplayerlinks')['rows'] teams = {t['teamid']: t for t in load('teams')['rows']} nations = {n['nationid']: n['nationname'] for n in load('nations')['rows']} # A player appears in teamplayerlinks once per club AND once per national # side. The club is the row whose team is not a national team; nations # and teams share an id space only through teamnationlinks, so the cheap, # reliable discriminator is: the club link is the one with the lowest # teamid that is not the player's own nation-team. Keep every link too. nat_team = set() for t in load('teamnationlinks')['rows']: nat_team.add(t['teamid']) clubs = {} alllinks = {} for l in tpl: alllinks.setdefault(l['playerid'], []).append(l) if l['teamid'] in nat_team: continue prev = clubs.get(l['playerid']) if prev is None or l['teamid'] < prev['teamid']: clubs[l['playerid']] = l out = [] for p in players: pid = p['playerid'] gk = p['preferredposition1'] == 0 spec = GK if gk else OUTFIELD club = clubs.get(pid) out.append({ 'id': pid, 'rating': p['overallrating'], 'potential': p['potential'], 'pos': p['preferredposition1'], 'pos2': p['preferredposition2'], 'pos3': p['preferredposition3'], 'pos4': p['preferredposition4'], 'posname': POSITION_NAMES[p['preferredposition1']] if 0 <= p['preferredposition1'] < len(POSITION_NAMES) else None, 'nation': p['nationality'], 'nationname': nations.get(p['nationality']), 'team': club['teamid'] if club else 0, 'teamname': teams.get(club['teamid'], {}).get('teamname') if club else None, 'jersey': club['jerseynumber'] if club else 0, 'foot': p['preferredfoot'], 'skillmoves': p['skillmoves'], 'weakfoot': p['weakfootabilitytypecode'], 'height': p['height'], 'weight': p['weight'], 'gk': gk, 'attrs': [weighted(p, s) for _, s in spec], }) doc = { 'meta': { 'source': 'FIFA17.exe resident database, via tools/db_dump.py', 'players': len(out), 'attr_order_outfield': [k for k, _ in OUTFIELD], 'attr_order_gk': [k for k, _ in GK], 'weights_outfield': {k: dict(v) for k, v in OUTFIELD}, 'weights_gk': {k: dict(v) for k, v in GK}, 'position_enum': POSITION_NAMES, }, 'players': out, } with open(OUT, 'w') as fh: json.dump(doc, fh, ensure_ascii=False, separators=(',', ':')) sys.stderr.write("wrote %s: %d players (%d keepers)\n" % (OUT, len(out), sum(1 for x in out if x['gk']))) return 0 if __name__ == '__main__': sys.exit(main())