fifa17-python: commit working FUT backend deployment (client/server split)
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.
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user