kits: canonical table-proven kit map, and category is the home/away key (not the id band)
Answers from the extracted client tables, before touching a binary. Every number is a count over the full table. fcc_kitcards 1482 rows, teamkits 2576 rows. CATEGORY -> ENGINE KIT TYPE, with a test that can actually fail. Asserting "category 3 means away" because away kits usually exist is not evidence: types 0/1/2 are present for most teams, so it is true by construction. The discriminating cases are the teams that LACK a type. category 2 -> type 0 HOME 54 keys lack type 0, 0 counterexamples category 3 -> type 1 AWAY 166 keys lack type 1, 0 counterexamples category 5 -> type 2 THIRD 145 keys lack type 2, 0 counterexamples and there is never more than one card per (team, year, category). THE ID BAND IS NOT HOME/AWAY. Band 6300000 holds 740 HOME cards AND 88 THIRD cards; band 6400000 holds the 654 AWAY cards. assetid is fully determined by the band (14 for 828/828 of 63xxxxx, 15 for 654/654 of 64xxxxx), so it carries no information the band does not. cardassetid is 35 on all 1482 rows - it is the FUT card frame, not the kit art. This matters for openfut-adapter-fifa17: KIT_AWAY_FLOOR splits home from away at 6_400_000, which is right for home vs away but silently classifies all 88 THIRD kits as HOME. Recorded here, not yet fixed - third kits are not currently ownable, so nothing observable depends on it. teamkits.islocked is 0 on all 2576 rows, so the DB lock flag is NOT what makes the pre-match selector call a kit locked. 6 rows are embargoed. Team 21, the staging club, resolves exactly: 6300006 cat 2 HOME year 0 assetid 14 -> teamkitid 1376 6400003 cat 3 AWAY year 0 assetid 15 -> teamkitid 1377 6300007 cat 2 HOME year 1972 assetid 14 -> teamkitid 5126 6300008 cat 5 THIRD year 0 assetid 14 -> teamkitid 1378 so the two kits OpenFUT serves are the correct home/away pair. NOT recoverable from data/tables: the kit's own name string. fcc_kitcards name/header/description/biodescription are byte OFFSETS into the table's string blob (583, 597, 608, 619 on one row), and that blob is not among the extracted tables. Adds audit_fifa17_kits.py (the tool, with the discriminating test inline) and fifa17-kit-map.json (its output) so this is reusable data rather than terminal scrollback.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Canonical FIFA 17 kit map, joined from the extracted client tables.
|
||||
|
||||
Authority for every kit question that a table can answer, so nobody has to
|
||||
reverse a binary for a fact that is sitting in a JSON row. Reads only:
|
||||
|
||||
fifa17-recon/data/tables/fcc_kitcards.json the FUT KIT CARD definitions
|
||||
fifa17-recon/data/tables/teamkits.json the ENGINE kit rows
|
||||
|
||||
Everything printed is TABLE_PROVEN unless the line says otherwise: it is a
|
||||
direct count over the full table, not a sample.
|
||||
|
||||
Usage:
|
||||
python3 audit_fifa17_kits.py human report
|
||||
python3 audit_fifa17_kits.py --json machine-readable, for tests/tools
|
||||
python3 audit_fifa17_kits.py --team 21 drill into one team
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
TABLES = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "data", "tables")
|
||||
|
||||
# TABLE_PROVEN, established by this script's own discriminating test (see
|
||||
# category_type_evidence): a kit CARD's `category` selects the engine kit ROW's
|
||||
# `teamkittypetechid` at the same (team, year).
|
||||
CATEGORY_TO_KIT_TYPE = {2: 0, 3: 1, 5: 2}
|
||||
KIT_TYPE_NAME = {0: "HOME", 1: "AWAY", 2: "THIRD", 3: "FOURTH", 5: "GK", 6: "SPECIAL6", 7: "SPECIAL7"}
|
||||
|
||||
|
||||
def load(name):
|
||||
with open(os.path.join(TABLES, name), "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
return data if isinstance(data, list) else data.get("rows", data)
|
||||
|
||||
|
||||
def band(carddbid: int) -> int:
|
||||
"""The 6_300_000 / 6_400_000 id band."""
|
||||
return (carddbid // 100_000) * 100_000
|
||||
|
||||
|
||||
def category_type_evidence(cards, kits):
|
||||
"""The DISCRIMINATING test behind CATEGORY_TO_KIT_TYPE.
|
||||
|
||||
Asserting "category 3 means away" because away kits usually exist is not
|
||||
evidence -- types 0/1/2 are present for most teams, so the claim is true by
|
||||
construction. What discriminates is the teams that LACK a type: if category 3
|
||||
really means type 1, then no category-3 card may exist for a (team, year)
|
||||
that has no type-1 row. Same for category 5 and type 2.
|
||||
"""
|
||||
kits_by = defaultdict(set)
|
||||
for r in kits:
|
||||
kits_by[(r["teamtechid"], r["year"])].add(r["teamkittypetechid"])
|
||||
cards_by = defaultdict(list)
|
||||
for r in cards:
|
||||
cards_by[(r["teamid"], r["year"])].append(r)
|
||||
|
||||
out = {}
|
||||
for cat, want in CATEGORY_TO_KIT_TYPE.items():
|
||||
# keys that HAVE teamkits rows but not the wanted type
|
||||
lacking = [k for k, t in kits_by.items() if t and want not in t]
|
||||
counterexamples = [
|
||||
r["carddbid"] for k in lacking for r in cards_by.get(k, []) if r["category"] == cat
|
||||
]
|
||||
out[cat] = {
|
||||
"kit_type": want,
|
||||
"name": KIT_TYPE_NAME[want],
|
||||
"keys_lacking_type": len(lacking),
|
||||
"counterexamples": counterexamples,
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def audit():
|
||||
cards = load("fcc_kitcards.json")
|
||||
kits = load("teamkits.json")
|
||||
|
||||
kits_by = defaultdict(list)
|
||||
for r in kits:
|
||||
kits_by[(r["teamtechid"], r["year"])].append(r)
|
||||
|
||||
rows = []
|
||||
for c in cards:
|
||||
key = (c["teamid"], c["year"])
|
||||
want = CATEGORY_TO_KIT_TYPE.get(c["category"])
|
||||
match = next((k for k in kits_by.get(key, []) if k["teamkittypetechid"] == want), None)
|
||||
rows.append(
|
||||
{
|
||||
"carddbid": c["carddbid"],
|
||||
"band": band(c["carddbid"]),
|
||||
"teamid": c["teamid"],
|
||||
"year": c["year"],
|
||||
"category": c["category"],
|
||||
"kit_type": want,
|
||||
"kit_type_name": KIT_TYPE_NAME.get(want, "?"),
|
||||
"assetid": c["assetid"],
|
||||
"cardassetid": c["cardassetid"],
|
||||
"value": c["value"],
|
||||
"weightrare": c["weightrare"],
|
||||
# These are BYTE OFFSETS into the table's string blob, not ids.
|
||||
# The blob is not among the extracted tables, so a kit's own
|
||||
# name string is NOT recoverable from data/tables alone.
|
||||
"name_offset": c["name"],
|
||||
"header_offset": c["header"],
|
||||
"description_offset": c["description"],
|
||||
"teamkitid": match["teamkitid"] if match else None,
|
||||
"teamkit_islocked": match["islocked"] if match else None,
|
||||
"teamkit_embargoed": match["isembargoed"] if match else None,
|
||||
}
|
||||
)
|
||||
|
||||
dupes = [k for k, n in Counter((r["teamid"], r["year"], r["category"]) for r in rows).items() if n > 1]
|
||||
|
||||
return {
|
||||
"counts": {"fcc_kitcards": len(cards), "teamkits": len(kits)},
|
||||
"bands": dict(sorted(Counter(r["band"] for r in rows).items())),
|
||||
"band_x_assetid": {f"{b}/{a}": n for (b, a), n in
|
||||
sorted(Counter((r["band"], r["assetid"]) for r in rows).items())},
|
||||
"band_x_category": {f"{b}/{c}": n for (b, c), n in
|
||||
sorted(Counter((r["band"], r["category"]) for r in rows).items())},
|
||||
"category_counts": dict(sorted(Counter(r["category"] for r in rows).items())),
|
||||
"cardassetid": sorted({r["cardassetid"] for r in rows}),
|
||||
"category_type_evidence": category_type_evidence(cards, kits),
|
||||
"unmatched": [r["carddbid"] for r in rows if r["teamkitid"] is None],
|
||||
"duplicate_team_year_category": dupes,
|
||||
"teamkits_islocked": dict(Counter(r["islocked"] for r in kits)),
|
||||
"teamkits_embargoed": dict(Counter(r["isembargoed"] for r in kits)),
|
||||
"teamkits_types": dict(sorted(Counter(r["teamkittypetechid"] for r in kits).items())),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--json", action="store_true")
|
||||
ap.add_argument("--team", type=int)
|
||||
args = ap.parse_args()
|
||||
|
||||
a = audit()
|
||||
if args.json:
|
||||
json.dump(a, sys.stdout, indent=2)
|
||||
return
|
||||
|
||||
print("FIFA 17 kit map — TABLE_PROVEN from the extracted client tables")
|
||||
print(f" fcc_kitcards rows : {a['counts']['fcc_kitcards']}")
|
||||
print(f" teamkits rows : {a['counts']['teamkits']}")
|
||||
|
||||
print("\nid bands")
|
||||
for b, n in a["bands"].items():
|
||||
print(f" {b}: {n}")
|
||||
print("\nband/assetid (assetid is fully determined by band)")
|
||||
for k, n in a["band_x_assetid"].items():
|
||||
print(f" {k}: {n}")
|
||||
print("\nband/category")
|
||||
for k, n in a["band_x_category"].items():
|
||||
print(f" {k}: {n}")
|
||||
print(f"\ncardassetid values: {a['cardassetid']} (the FUT card frame, not the kit art)")
|
||||
|
||||
print("\ncategory -> engine kit type, with the discriminating test")
|
||||
for cat, ev in a["category_type_evidence"].items():
|
||||
verdict = "HOLDS" if not ev["counterexamples"] else f"FAILS ({len(ev['counterexamples'])})"
|
||||
print(f" category {cat} -> type {ev['kit_type']} {ev['name']:6s} "
|
||||
f"| {ev['keys_lacking_type']:4d} (team,year) keys lack that type, "
|
||||
f"{len(ev['counterexamples'])} counterexample(s) -> {verdict}")
|
||||
|
||||
print("\nengine kit types present in teamkits")
|
||||
for t, n in a["teamkits_types"].items():
|
||||
print(f" type {t} {KIT_TYPE_NAME.get(t,'?'):8s}: {n}")
|
||||
|
||||
print(f"\nteamkits islocked : {a['teamkits_islocked']} <- every row, so NOT the selector lock")
|
||||
print(f"teamkits embargoed : {a['teamkits_embargoed']}")
|
||||
|
||||
print(f"\nanomalies")
|
||||
print(f" cards with no matching teamkits row : {len(a['unmatched'])}")
|
||||
print(f" duplicate (team,year,category) : {len(a['duplicate_team_year_category'])}")
|
||||
|
||||
if args.team is not None:
|
||||
print(f"\n=== team {args.team} ===")
|
||||
print(f" {'carddbid':10s} {'cat':4s} {'type':7s} {'year':6s} {'assetid':8s} {'teamkitid':10s} locked")
|
||||
for r in sorted((r for r in a["rows"] if r["teamid"] == args.team), key=lambda r: r["carddbid"]):
|
||||
print(f" {r['carddbid']:<10} {r['category']:<4} {r['kit_type_name']:<7} {r['year']:<6} "
|
||||
f"{r['assetid']:<8} {str(r['teamkitid']):<10} {r['teamkit_islocked']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user