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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user