fifa17-recon: offline FUT squad-shell working + full card-system RE

Milestone: FIFA 17 Ultimate Team boots end-to-end on our offline backend
past every EA gate into the hub and a live Squads editor (correct 4-4-2,
5-star squad, no freezes).

Key findings this session:
- userMassInfo MUST stay {} (any content desyncs the massinfo parser
  0x180174630 -> tokenizer busy-loop freeze). Deliver the squad via
  GET /squad/0 (fetched on Squads-tab entry) instead.
- Player cards render generic because the card view-model (0x1800d7920)
  reads identity/rating/face from a resolved record at item+0x10, filled
  by a lookup (0x18011cca0) in the FUT item-definition std::map at
  CardsDb+0x160c0 -- which is EMPTY offline -> default blank record.
- Version advertising (itemDbVersion/checkServerDbVersion) is proven inert
  (JSON fields routed to the skip handler). Owned items don't auto-trigger
  a definition fetch. In-place map overwrite is dead (map stays empty).
- Definition-serving endpoints (item/resource, defid, item?idList) built +
  ready; the fetch trigger lives in the packed FIFA17.exe.

New: docs/CARD_SYSTEM.md (findings + ordered next-steps plan for real
player cards: patch-POC, dbdata extractor, drive FIFA17.exe fetch, or
live-memory store injection). Plus tools: fut_seed.py (squad ladder +
definition serving), fifadrive.sh, vgamepad.py, and the login-RE toolset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
funman300
2026-08-01 20:24:30 -07:00
parent edab23f04a
commit 6ddd5e9d47
36 changed files with 9421 additions and 5 deletions
+92 -5
View File
@@ -11,7 +11,10 @@ Rules (from CardsDLL 0x18016D230 / 0x1801a33a0):
* body must parse as JSON (else err 0x3E6); 204 + empty body is accepted.
* [resp+0x1c] == 0 is the success test; 404 is OK only on the first user GET.
"""
import datetime, json, os, re, http.server
import datetime, json, os, re, sys, http.server
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from fut_seed import CLUB, SQUAD, USER_LIST # forged starter squad (clean-room)
ADDR = ("127.0.0.1", 8099)
LOG = "/tmp/utas_server.log"
@@ -71,9 +74,82 @@ USER_POST = {"login": True, "userData": user_info(),
"squad": {}, "starterPack": {}, "bonusPacks": []}
# GET ut/game/<sku>/settings (0x18013C6D0) recognises ONE key: configs (0xa2).
SETTINGS = {"configs": []}
# GET ut/game/<sku>/userMassInfo (GetUserMassInfo, deser 0x180174630).
# ANY content here (userInfo AND/OR squad) DESYNCS CardsDLL's massinfo parser ->
# infinite tokenizer spin (busy-loop freeze at 0x1801c7f1a). Proven: {} reaches
# the hub; {userInfo,...} and {...,squad,...} both freeze. The userInfo sub-deser
# 0x18013ec10 mis-consumes some field in user_info(). So keep userMassInfo EMPTY
# (hub-reaching) and deliver club/squad via their OWN endpoints (/user, /club,
# /squad) whose parsers we know work. Select via env FUT_MASSINFO (empty|userinfo|full).
_MI = os.environ.get("FUT_MASSINFO", "empty")
if _MI == "full":
MASSINFO = {"userInfo": user_info(), "squad": SQUAD,
"settings": {"configs": []}, "userData": {}}
elif _MI == "userinfo":
MASSINFO = {"userInfo": user_info(), "settings": {"configs": []}, "userData": {}}
else:
MASSINFO = {} # proven hub-reaching
# ---- FUT item-definition serving (wf_e41070d8) -------------------------------
# The card view-model 0x1800d7920 renders identity/rating/face from a RESOLVED
# record at item+0x10, filled by looking the resourceId up in the FUT item-def
# store. That store is network-filled; empty offline => generic cards. FIFA
# fetches definitions from ut/<sku>/item/resource, ut/<sku>/defid, and batch
# ut/<sku>/item?idList=<ids>. We serve them here (deser 0x18013fe00, same as items).
# resourceId = playerId | version<<24 ; assetId = resourceId & 0xffffff.
PLAYER_DEFS = {
# assetId: (name, rating, position, nation, leagueId, teamid, [6 attrs])
20801: ("Ronaldo", 94, "ST", 38, 53, 243, [90, 93, 82, 91, 33, 80]),
}
def item_def(rid):
"""Build one FUT item-definition for a requested resourceId."""
asset = rid & 0xffffff
name, rating, pos, nation, league, team, attrs = PLAYER_DEFS.get(
asset, ("Player", 75, "CM", 0, 0, 0, [70, 70, 70, 70, 70, 70]))
return {
"id": rid,
"resourceId": rid,
"definitionId": rid,
"assetId": asset,
"cardassetid": asset,
"commodityId": asset,
"cardsubtypeid": 0,
"cardType": 0,
"itemType": "player",
"rareflag": 1,
"rating": rating,
"preferredPosition": pos,
"nation": nation,
"leagueId": league,
"teamid": team,
"playStyle": 250,
"attributeList": [{"index": i, "value": v} for i, v in enumerate(attrs)],
"name": name,
"commonName": name,
"lastName": name,
"itemState": "free",
"untradeable": True,
}
def defs_route(h):
# Parse every integer id out of the query string (idList=a,b,c / definitionId=x
# / resourceId=x) and return a definition for each.
q = h.path.split("?", 1)[1] if "?" in h.path else ""
ids = [int(n) for n in re.findall(r"\d{3,}", q)]
if not ids:
return 200, {"itemData": []}
return 200, {"itemData": [item_def(i) for i in ids]}
G = r"/ut/game/[^/]+"
ROUTES = [
# ---- FUT item-definition endpoints (must precede generic /item, /user) ----
(re.compile(G + r"/item/resource"), lambda m, h: defs_route(h)),
(re.compile(G + r"/defid"), lambda m, h: defs_route(h)),
(re.compile(G + r"/item(\?|$)"), lambda m, h: defs_route(h)),
(re.compile(r"^/ut/auth"), lambda m, h: (200, auth_body())),
(re.compile(r"^/ut/delete/auth"), lambda m, h: (200, {})),
(re.compile(G + r"/settings"), lambda m, h: (200, SETTINGS)),
@@ -84,15 +160,20 @@ ROUTES = [
(re.compile(G + r"/phishing/validate"), lambda m, h: (200, {"token": "OPENFUT-TRUST-0000000000000000"})),
(re.compile(G + r"/phishing/question"), lambda m, h: (200, {"question": 0, "answer": "", "attempts": 5})),
(re.compile(G + r"/user/credits"), lambda m, h: (200, {"credits": 15000})),
# ---- club/squad routes reverted to known-good {} stubs (2026-08-01) ----
# The forged squad in MASSINFO/SQUAD/CLUB HANGS CardsDLL's deserializer (hard
# freeze at boot). Re-enable only after the exact shape is reversed. The forged
# data still lives in fut_seed.py + MASSINFO/squad_route below (unrouted).
(re.compile(G + r"/user/list"), lambda m, h: (200, {})),
(re.compile(G + r"/user/accountinfo"), lambda m, h: (200, {})),
(re.compile(G + r"/user$|" + G + r"/user\?"), lambda m, h: user_route(h)),
(re.compile(G + r"/squad"), lambda m, h: (200, {})),
(re.compile(G + r"/squad"), lambda m, h: squad_route(h)),
(re.compile(G + r"/match/keepalive"), lambda m, h: (204, None)),
(re.compile(G + r"/hub"), lambda m, h: (200, {})),
(re.compile(G + r"/userMassInfo"), lambda m, h: (200, {})),
# STEP 1 (wf_0bc80ab3): zero-resolve squad in MASSINFO; club stays {}.
(re.compile(G + r"/userMassInfo"), lambda m, h: (200, MASSINFO)),
(re.compile(G + r"/season"), lambda m, h: (200, {})),
(re.compile(G + r"/club"), lambda m, h: (200, {})),
(re.compile(G + r"/club"), lambda m, h: (200, CLUB)),
]
@@ -105,6 +186,12 @@ def user_route(h):
return 200, USER_GET
def squad_route(h):
# GET = LoadActiveSquad, PUT = updateActiveSquad. Always echo the full canonical
# squad (never {} — an empty body resets the client's 23 slots, 0x18013d1f0).
return 200, SQUAD
class H(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
@@ -115,7 +202,7 @@ class H(http.server.BaseHTTPRequestHandler):
for k, v in self.headers.items():
log(" %s: %s" % (k, v))
if body:
log(" body: %s" % body[:1200].decode("utf-8", "replace"))
log(" body: %s" % body[:65536].decode("utf-8", "replace"))
code, payload = 200, {}
for rx, fn in ROUTES: