70a64e3709
Freeze the running offline FUT backend into version control as fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a fresh checkout: * OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is required for remote mode (compose and entrypoint fail without it) * docker-compose.yml reproducing the frozen baseline container exactly (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart) * .env.example / .env for site config - the LAN IP is never hardcoded in source * tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10, verified byte-identical to the running container at freeze time * client_arm.sh (the 105 client-side arming counterpart) * Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying * docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record, restore instructions and rebuild-equivalence procedure Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored. The live container is untouched pending the .105 launcher audit.
162 lines
6.6 KiB
Python
162 lines
6.6 KiB
Python
#!/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())
|