edab23f04a
Emulates FIFA 17's full online + Ultimate Team stack against an offline,
clean-room backend (no EA servers). Proven end-to-end 2026-08-01:
Origin login -> Blaze login -> device-trust -> the FUT hub.
Package:
- tools/openfut-fut.sh one-command orchestrator (start/stop/status/restart)
- tools/root_arm.sh idempotent host arm (sysctls, DNAT, /etc/hosts easw)
- tools/{lsx_responder_v2,blaze_responder_v3b,roster_server,utas_server,autopatch}.py
the 5 servers (Origin LSX :4216, Blaze :42127/42130/42131, roster :8081,
FUT/UTAS :8099) + heat2.py (Fire2/Heat2 TDF codec)
- FUT-RUNBOOK.md runbook + gate-ladder troubleshooting
- docs/, tools/login_dump/*.md the reverse-engineering write-ups
All findings are clean-room, from binaries we own; nothing from any leak.
The wire protocol maps 1:1 to FIFA 23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
641 lines
37 KiB
Markdown
641 lines
37 KiB
Markdown
# FUT / UTAS (RS4) CONNECT PLAN — FIFA 17, clean-room
|
||
|
||
Synthesis of four independent reversals of `CardsDLL_Win64_retail.dll` (PE base `0x180000000`,
|
||
live-mapped at `0x6ffffc140000`, slide `0x6FFE7C140000`) plus live `/proc/932452/mem` reads and
|
||
`/tmp/blaze_responder.log` / `/tmp/roster_server.log`.
|
||
Scope: get FUT to **open a socket to us, authenticate, and stop showing "error connecting to
|
||
FIFA 17 Ultimate Team"**. Not full playable FUT.
|
||
|
||
**Headline correction to BRIEF8.** The brief's hypothesis ("we serve the base-URL keys EMPTY, so
|
||
CardsDLL can't build a URL") is **wrong**. The URL is built fine. CardsDLL falls back to a
|
||
**hardcoded default base URL** and that host is dead:
|
||
|
||
```
|
||
default base = "http://easw.easports.com:8099/" .rdata file 0x21d680 = VA 0x18021e280
|
||
default ptr = VA 0x1802caa08 -> live 0x6ffffc35e280 -> same string
|
||
live descriptors: Counter({(b'http://easw.easports.com:8099/', ks=0): 126}) # ALL 126 entries
|
||
$ getent hosts easw.easports.com -> rc=2 (NXDOMAIN), no /etc/hosts entry
|
||
```
|
||
|
||
So resolution succeeds, DNS fails, `connect()` is never reached, no SYN — exactly the observed
|
||
symptom. The in-process telemetry ring confirms the flow ran and died with no HTTP status at all:
|
||
`{"type":"utas","status":"start_flow"}` then `{"type":"utas","status":"error","status_code":"0"}`.
|
||
|
||
This changes the fix from "make a URL exist" to "point the URL at us" — and it opens a **zero-config
|
||
fallback** (§2.0): just make `easw.easports.com` resolve to `127.0.0.1` and listen on `:8099`.
|
||
|
||
---
|
||
|
||
## 1. THE URL + AUTH CHAIN
|
||
|
||
### 1.1 Two tables drive everything
|
||
|
||
**Module (group) table — 48 entries, VA `0x18021df80`, stride 0x10 = `{char* pathTemplate; char* NAME}`**
|
||
(dumped from the file; entries 45–47 have a NULL path):
|
||
|
||
| idx | path template | NAME | idx | path template | NAME |
|
||
|---|---|---|---|---|---|
|
||
|0|`ut/%s/auctionhouse`|AUCTIONHOUSE|24|`ut/%s/season/%%s/reset`|SEASONRESET|
|
||
|1|`ut/%s/clubUser`|CLUB_USER|25|`ut/%s/season/friendly`|FRIENDLYSEASON|
|
||
|2|`ut/%s/user/list`|CLUB_INFO|26|`ut/%s/purchased`|PURCHASED|
|
||
|3|`ut/%s/club`|CLUB|27|`ut/%s/store`|STORE|
|
||
|4|`ut/%s/defid`|DREAM|28|`ut/%s/watchList`|WATCHLIST|
|
||
|5|`ut/%s/squad`|SQUAD|29|`ut/delete/%s/watchList`|DELETEWATCHLIST|
|
||
|6|`ut/delete/%s/squad`|DELETE_SQUAD|30|`ut/%s/tradePile`|TRADEPILE|
|
||
|7|`ut/%s/leaderboards/options`|LBOPTIONS|31|`ut/%s/trade`|TRADE|
|
||
|8|`ut/%s/leaderboards`|LBDEFAULT|32|`ut/delete/%s/trade`|DELETETRADE|
|
||
|9|`ut/%s/activeMessage`|PAFPRACTICE|33|`ut/%s/marketdata`|MARKETDATA|
|
||
|10|`ut/%s`|**UT**|34|`ut/%s/clientdata`|CLIENTDATA|
|
||
|11|`ut/%s/user`|**USER**|35|`ut/auth`|**AUTH**|
|
||
|12|`ut/delete/%s/user`|DELETEUSER|36|`ut/delete/auth`|DELETE_AUTH|
|
||
|13|`ut/%s/item`|ITEMS|37|`ut/%s/phishing`|PHISHING|
|
||
|14|`ut/%s/item/resource`|ITEMS_BY_RES|38|`ut/%s/captcha`|CAPTCHA|
|
||
|15|`ut/delete/%s/item`|DELETEITEMS|39|`ut/%s/tfa`|TFA|
|
||
|16|`ut/%s/match`|MATCH|40|`ut/%s/squad/mode`|SQUADMODE|
|
||
|17|`ut/%s/sbs`|SBC|41|`ut/%s/draft/mode`|DRAFT|
|
||
|18|`ut/%s/tournament`|TOURNAMENT|42|`ut/%s/champion`|CHAMPIONS|
|
||
|19|`ut/%s/tournament/user`|TOURNAMENTUSER|43|`ut/v2/%s/store`|V2STORE|
|
||
|20|`ut/delete/%s/tournament/user`|TOURNAMENTQUIT|44|`ut/%s/livemessage`|LIVEMESSAGE|
|
||
|21|`ut/%s/season`|SEASON|45|*(null)*|ADMIN|
|
||
|22|`ut/%s/season/user`|SEASONUSER|46|*(null)*|DEBUG|
|
||
|23|`ut/%s/season/%%s/user`|SEASONUSER_ALTER|47|*(null)*|MAINTENANCE|
|
||
|
||
**Call (endpoint) descriptor table — 125 live entries + terminator, VA `0x1802caa20`, stride 0x30:**
|
||
|
||
```
|
||
+0x00 char* callName e.g. "GetSettings"
|
||
+0x08 u64 moduleIdx index into the 48-entry table above
|
||
+0x10 char* CALL_TAG e.g. "GETSETTINGS" (uppercase, used in the config key)
|
||
+0x18 char* baseUrl <-- what resolve() writes; live = the easw default for all 126
|
||
+0x20 u32 preAuthFlag 1 for GetSettings + the market/IS* calls, 0 otherwise
|
||
+0x21 u8 killSwitch live = 0 for all 126
|
||
+0x28 fnptr setKillSwitch one thunk per call, e.g. 0x180124020 writes 0x1802cae31
|
||
```
|
||
|
||
Verified entries relevant to boot: `21 GetSettings mod=10 UT flag=1`, `22 Authentication mod=35 AUTH`,
|
||
`23 Login mod=11 USER`, `24 Logout mod=36 DELETE_AUTH`, `27 CreateUser mod=11 USER`,
|
||
`28 GetUserInfo mod=11 USER`, `35 GetUserCredits mod=11`, `36 GetUserData mod=11`,
|
||
`14 LoadActiveSquad mod=5 SQUAD`, `54 KeepAlive mod=16 MATCH`, `100 GetHubData mod=10 UT`,
|
||
`101 GetUserMassInfo mod=10 UT`, `5 GetClubInfo mod=2 CLUB_INFO`.
|
||
|
||
### 1.2 Base-URL resolution — `RS4::ServerSettings::resolve` @ `0x180124270`
|
||
|
||
Runs **once, at EnterFUT**, after all `fetchClientConfig` traffic (blaze log: whole CFID sweep at
|
||
21:58:11, resolver object at `0x1802e6408` already populated). Three passes, **last non-empty wins**:
|
||
|
||
1. **Hardcoded default** — `mov r14,[rip+0x1a672c] # 0x1802caa08` @ `0x1801242d5`, splashed into every
|
||
descriptor's `+0x18` by the loop @ `0x180124350`.
|
||
2. **`FUT_RS4_APIURL_<MODULE_NAME>`** — loop @ `0x180124390`, `edi < 0x30` (48), `rbx` walks the module
|
||
table's NAME field, key format string `0x18021fa70`.
|
||
3. **`FUT_RS4_URL_<CALL_TAG>`** — loop @ `0x180124440`, `r12 = 0x87`, `rbx` walks descriptor `+0x10`,
|
||
key format string `0x18021faa0`.
|
||
|
||
Each pass requires **HasKey (`vt+0xe8`) == true AND GetString (`vt+0x30`) non-empty**
|
||
(`lea r8,[rip+0xc58bb] # 0x1801e9caf` is the default = a NUL byte; `cmp BYTE PTR [rbp-0x80],0x0; je skip`
|
||
@ `0x1801243ff`). Out buffer 0x200 bytes.
|
||
|
||
**`FUT/MODULE_BASEURL_%s` (`0x18021fa58`) and `FUT/SINGLE_BASEURL_%s` (`0x18021fa88`) are DEAD CODE**
|
||
in this build — independently confirmed by three reversers and re-verified here: both are `snprintf`'d
|
||
into `[rbp+0x180]` (`0x18012439f`, `0x18012444f`) and that buffer is **never** passed to `HasKey`/
|
||
`GetString`; the lookups take `lea rdx,[rsp+0x40]`, which holds the `FUT_RS4_*` form. Serving them is
|
||
harmless but useless.
|
||
|
||
**`FUT_TARGET_HOSTNAME` / `FUT_TARGET_PORT` are NOT the API base** — they belong to a separate
|
||
ICMP/traceroute latency probe (@`0x180180876`, siblings `FIFA_FUT_ALLOW_PING`, `FUT_DNS_TIMEOUT`,
|
||
`FUT_PING_TIMEOUT`, `FUT_MAX_HOPS`, `FUT_PUMP_RATE_MILLSEC`). **Do not serve `FUT_TARGET_PORT`**: a
|
||
copy/paste bug @`0x1801808e8` makes its `GetInt` read the key `FUT_MAX_HOPS` instead, so its presence
|
||
poisons the port with the hop count.
|
||
|
||
### 1.3 Value format and URL assembly (@ `0x180177a89`–`0x180177b62`, re-verified)
|
||
|
||
```
|
||
scan value for ':' cmp al,0x3a @0x180177aa0
|
||
bytes[colon+1] == '/' && [colon+2] == '/' -> strcpy_s verbatim @0x180177acf
|
||
else -> sprintf("http://%s") fmt @0x18022dc38
|
||
force trailing '/' mov BYTE PTR [rbp+rax-0x60],0x2f @0x180177b28
|
||
strcat_s(path) @0x180177b62
|
||
buffer 0x200 everywhere
|
||
```
|
||
|
||
So `http://127.0.0.1:8099/`, `http://127.0.0.1:8099` and `127.0.0.1:8099` all end up identical.
|
||
**Scheme: plain HTTP is native** (EA's own default is `http://…:8099/`); there is no scheme rewrite.
|
||
HTTPS would also be accepted verbatim (ProtoSSL verify is patched) but adds a variable — use http.
|
||
|
||
### 1.4 The platform token — `game/fifa17`
|
||
|
||
`BuildEndpointPath` @ `0x180123da0`:
|
||
|
||
```
|
||
movsxd rdi,[r14+rcx*8+0x2caa28] ; moduleIdx from the descriptor
|
||
call QWORD PTR [rax+0x250] ; engine/config service -> const char* token
|
||
lea r8,[rip+0xfbc7e] # 0x18021fac8 ; "game/%s" -> snprintf into a 0x40 buffer
|
||
mov r8,[r14+r8*8+0x21df80] ; moduleTable[moduleIdx].pathTemplate
|
||
snprintf(out, len, pathTemplate, "game/<token>")
|
||
```
|
||
|
||
The `%s` in `FUT_RS4_APIURL_%s` / `FUT_RS4_URL_%s` is a **module/call NAME, not a platform token** —
|
||
that was the brief's second wrong assumption. The only path-level token is this one, and the evidence
|
||
says `fifa17`: live FIFA17.exe `.rdata` `0x1438dac20 = b'fifa17\0\0 2017\0'`, adjacent
|
||
`0x1438dbe58 = 'ut/game/%s/'`, `0x1438dbe88 = 'FUT_RS4_BASE_URL'`; process env `GAMEID=fifa17`;
|
||
our own roster server is already serving `/fifa17/fut/rosterupdate.xml`. **Not proven** (the getter
|
||
lives in the encrypted exe) — mitigate by wildcarding the segment. Final shapes:
|
||
|
||
```
|
||
ut/auth (AUTH — no platform segment at all)
|
||
ut/delete/auth (LOGOUT)
|
||
ut/game/fifa17 (UT -> + "/settings", "/hub", "/userMassInfo")
|
||
ut/game/fifa17/user (USER -> + "/credits", …)
|
||
ut/game/fifa17/squad (SQUAD -> + "/active?active=true" | "/active/user/<personaId>")
|
||
ut/game/fifa17/user/list (CLUB_INFO -> + "?personaIdList=…")
|
||
ut/game/fifa17/match (MATCH -> + "/keepalive")
|
||
```
|
||
|
||
**In the JSON body the platform is the literal `"pc"`** (VA `0x18021fe9c`, the only `\0pc\0` in the
|
||
whole DLL) with `"sku":"FFA17PCC"` (VA `0x18021fe90`). No runtime platform formatting exists.
|
||
|
||
### 1.5 The auth handshake
|
||
|
||
**Request** — module 35 `ut/auth`, JSON body ⇒ POST (the literal method string is set through a
|
||
ProtoHttp enum, not a `"POST"` literal — treat method as unproven and accept anything).
|
||
|
||
Headers (template @ `0x180220150`, file 0x21f550): `Accept: application/json` + `Content-Type:
|
||
application/json` + **`Hash: %s` only when POW is on** (feature flag `"POW"` @ `0x180235ce0`, read via
|
||
config `vt+0x50`, cached to `0x1802ef5d0/…d8` — we simply never serve the key, so no POW).
|
||
**No `X-UT-SID` on this first request** (there is no sid yet).
|
||
|
||
Body, built by `0x180125900` in this exact key order, then `0x1801a26d0` appends the identification:
|
||
|
||
```json
|
||
{"isReadOnly":false,"priorityLevel":6,"sku":"FFA17PCC","nucleusPersonaPlatform":"pc",
|
||
"clientVersion":3,"nuc":33068179,"nucleusPersonaId":33068179,
|
||
"nucleusPersonaDisplayName":"CAGE","locale":"en-us","regionCode":"…",
|
||
"deviceId":"…","macAddress":"…",
|
||
"method":"authcode","identification":{"authCode":"OPENFUT-000…"}}
|
||
```
|
||
|
||
Key literals verified: `isReadOnly`0x1802201c0, `priorityLevel`0x1802201d0 (from global 0x1802cc528,
|
||
live 6), `sku`0x1802201e0, `nucleusPersonaPlatform`0x1802201e8, `clientVersion`0x180220200 (immediate 3),
|
||
`nuc`0x180220210, `nucleusPersonaId`0x180220218, `nucleusPersonaDisplayName`0x180220230 (fallback
|
||
literal `"mememe"`0x18022024c), `locale`0x180220254, `regionCode`0x1802201b0, `deviceId`0x180220260,
|
||
`macAddress`0x180220270, `method`0x18023641c = `authcode`0x180236428, `identification`0x180236438,
|
||
`authCode`0x180236448. The optional `"fat"`0x180236454 is **never** emitted (its gate `vt+0xb0` =
|
||
`0x1800cd820` = `xor al,al; ret`), and the derived-extra-fields hook `vt+0xa0` = `0x180122420` = `ret`.
|
||
Persona values come from FIFA17.exe via import thunks — with our Blaze LoginResponse they will be
|
||
`33068179` / `"CAGE"` (blaze log SESS.BUID / PDTL.DSNM).
|
||
|
||
**Response parser** @ `0x1801a2880` — a rapidjson member walk that recognises **exactly three keys**:
|
||
|
||
| key | VA | action |
|
||
|---|---|---|
|
||
|`sid`|0x180236458|`strlen` + string-assign into **ServerCall+0x1e8** (`0x1801a2ae8`) |
|
||
|`serverTime`|0x180236460|six `atoi` at char offsets 0,5,8,11,14,17 → `+0x208/+0x210` |
|
||
|`lastOnlineTime`|0x180236470|same → `+0x218/+0x220` |
|
||
|
||
Everything else is skipped. Return value is `setne` on the document terminating cleanly — **there is no
|
||
status-code check in the parser**, but the layer above requires a parseable body (see 1.7).
|
||
Datetime shape is `YYYY-MM-DD HH:MM:SS` (a `T` separator parses identically — `atoi` at fixed offsets).
|
||
|
||
**`sid` is the whole ballgame.** Header builder `0x180126080` (vtable slot 0x110) copies template
|
||
`0x180220100` = `"Accept: application/json\r\nContent-Type: application/json\r\nX-UT-SID: %s\r\n"`
|
||
(file 0x21f500 — one string; `strings(1)` split it, which is why the brief lists `X-UT-SID: %s` alone),
|
||
fills `%s` from `[this+0x1e8]` (`0x18012611b`) and appends it with fourCC `'apnd'` (`0x61706e64`)
|
||
via `[this+0x18]->vt[0x70]` (`0x180126231`). A getter for the same field is slot 0x108 = `0x180125520`.
|
||
The field is an eastl::string initialised empty in the ctor `0x1801a2210`.
|
||
|
||
**401/403 handling** @ `0x1801a33a0` / `0x1801a3447` — on 401 or 403 for a normal API call the client
|
||
**silently re-auths** (max 3, counter `[rcx+0x1e0]`); a 401 on `ut/auth` itself **truncates the stored
|
||
sid** and re-auths (max 2, counter `[rbx+0x350]`). Never answer 401/403 or we burn retries and then error.
|
||
|
||
`X-UT-VALIDATE: true` (VA `0x18022b398`) exists in the DLL but is not part of this path.
|
||
|
||
### 1.6 What the brief's other strings actually are (all four reversers agree)
|
||
|
||
* `%s/pow/mm/` (0x18021fab0) + `/fut/` (0x18021fabc) — a **content/CDN base** built at `0x18012450e`
|
||
from a *different* vtable slot (`vt+0x3f8`); live it is EMPTY (`obj+0x30 = 0`, `obj+0x38 = ""`) and
|
||
the code skips the whole block when empty. Production shape (leftover in memory):
|
||
`http://content.lt.easfc.ea.com:8080//fifa/fltOnlineAssets/2013/pow/mm/…`. Not the auth flow.
|
||
* `TRANSACTION_STATE_WAIT_FOR_AUTH_CODE` (0x1ef360) — the **FIFA-Points store purchase** state machine
|
||
(contiguous enum with `_WAIT_FOR_CART`, `_WAIT_FOR_CHECKOUT`, `_WAIT_FOR_CONSUME_SERVER`).
|
||
* `/user/hub`, `/user/registration` — **FUT Champions** suffixes (module CHAMPIONS →
|
||
`ut/game/fifa17/champion/user/hub`), not initial load.
|
||
* `/active/user/%lld`, `/active?active=true` — the **active-squad** loader (`0x18014DAD0`,
|
||
`RS4:FutSquadLoadServerResponse`).
|
||
* `http-rs4` (0x18022b4a8) — a **telemetry tag** passed next to `fut-rs4-server` (0x18022b4b8), not a scheme.
|
||
* There is a **second, exe-side FUT client** (`FutServerCall`, `FifaFutServiceImplementation`) with a
|
||
single key **`FUT_RS4_BASE_URL`** (no `%s`), path `ut/game/%s/`, sub-paths `users/club?sku=`,
|
||
`utStats?sku=`, `user/accountinfo`, and a *different* auth header:
|
||
`Easw-Session-Data-Nucleus-Id: %lld` (no X-UT-SID). Serve that key too.
|
||
|
||
### 1.7 Generic response pipeline and where "error connecting" is raised
|
||
|
||
`OnResponse` @ `0x18016D230`:
|
||
|
||
```
|
||
HTTP 204 -> skip parsing entirely (accepted)
|
||
empty body -> err = 0x63
|
||
JSON parse failure -> err = 0x3E6
|
||
else -> err = call->vt[0x60](resp, body) ; per-class error extractor
|
||
epilogue: [resp+0x1c] = err [resp+0x20] = httpStatus
|
||
```
|
||
|
||
`[resp+0x1c] == 0` is the universal success test.
|
||
|
||
The popup itself: **`FUT::WebServiceImpl` @ `0x180122440`** fires
|
||
`FireEvent(target="_global" (0x1801ed8a8), event="ServerFatalError" (0x1801ed890),
|
||
param="OSDK_LOST_CON_TO_EA" (0x18021dae0))` at `0x1801224d0` (alternate param
|
||
`"Unknown_FCC_Error"` 0x1801ed878 at `0x180122503`) — all four VAs verified by direct byte read.
|
||
Gate @ `0x1801224a7`: `cmp [rsp+0x58],0x2 / cmp eax,0x3` else fire.
|
||
It is **not** `FUTOnlineGameModeBase` (that string is a debug formatter, one xref, raises nothing).
|
||
|
||
The surrounding flow is a front-end `.nav` state machine, dispatched by string on event `0x3D`
|
||
(`0x180019BA0`, field `"StrParam"` 0x1801ed788): `beginFUTLogin` → `retrieveUserData` →
|
||
`CheckRewards` → `loginComplete` → `FinalizeLogin` → telemetry `('GAME','ACTN','MODE',"enter_hub")`
|
||
→ transition into the FUT hub. Live strings in the process confirm the flow files:
|
||
`loading nav/fut/futFlow.nav`, `/fut/futLogInFlow.nav`, `/fut/futGameHubFlow.nav`.
|
||
**A 404 on the first user lookup is an accepted, non-fatal answer** (`0x18007AF20`:
|
||
`[rdx+0x1c]==0 ? ok : [rdx+0x20]==0x194 ? ok : ServerFatalError`) — it is the new-user branch
|
||
that routes to createClub.
|
||
|
||
---
|
||
|
||
## 2. THE FIX (minimal)
|
||
|
||
### 2.0 Path B first — the zero-config, zero-relaunch experiment
|
||
|
||
Because every descriptor **already** holds `http://easw.easports.com:8099/`, the cheapest possible test
|
||
needs no responder change at all:
|
||
|
||
```bash
|
||
echo "127.0.0.1 easw.easports.com" | sudo tee -a /etc/hosts
|
||
python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/utas_server.py # binds 127.0.0.1:8099
|
||
# re-enter Ultimate Team (a game restart is safest — the resolver may have negatively cached)
|
||
```
|
||
|
||
Do this **in addition** to Path A below; it costs nothing and it removes the config store from the
|
||
equation (we have not proven CardsDLL reads the same merged `_all` section the exe uses).
|
||
|
||
### 2.1 (a) `blaze_responder_v3b.py` — fetchClientConfig additions
|
||
|
||
Insert after the `OSDK_ROSTER` block (~line 520) and change `client_config_for`:
|
||
|
||
```python
|
||
# --- FUT / UTAS (RS4) BASE URL --------------------------------------------
|
||
# CardsDLL RS4::ServerSettings::resolve @0x180124270 writes each of the 125
|
||
# endpoint descriptors' baseUrl (VA 0x1802caa20 + i*0x30 + 0x18) in 3 passes,
|
||
# last non-empty wins:
|
||
# 1. hardcoded default @0x1802caa08 = "http://easw.easports.com:8099/" (DEAD HOST)
|
||
# 2. cfg["FUT_RS4_APIURL_<MODULE_NAME>"] key fmt @0x18021fa70
|
||
# 3. cfg["FUT_RS4_URL_<CALL_TAG>"] key fmt @0x18021faa0
|
||
# A key only counts if HasKey(vt+0xe8) AND GetString(vt+0x30) is non-empty.
|
||
# The value is used verbatim when it contains "://" (@0x180177ab3), a trailing
|
||
# '/' is force-appended (@0x180177b28), then the endpoint path is strcat'd.
|
||
# FUT/MODULE_BASEURL_%s and FUT/SINGLE_BASEURL_%s are formatted but NEVER looked
|
||
# up (dead code). FUT_TARGET_PORT must NOT be served: bug @0x1801808e8 makes its
|
||
# GetInt read the key FUT_MAX_HOPS instead.
|
||
UTAS_BASE = "http://127.0.0.1:8099/"
|
||
|
||
FUT_RS4_MODULES = [
|
||
"AUCTIONHOUSE", "CLUB_USER", "CLUB_INFO", "CLUB", "DREAM", "SQUAD",
|
||
"DELETE_SQUAD", "LBOPTIONS", "LBDEFAULT", "PAFPRACTICE", "UT", "USER",
|
||
"DELETEUSER", "ITEMS", "ITEMS_BY_RES", "DELETEITEMS", "MATCH", "SBC",
|
||
"TOURNAMENT", "TOURNAMENTUSER", "TOURNAMENTQUIT", "SEASON", "SEASONUSER",
|
||
"SEASONUSER_ALTER", "SEASONRESET", "FRIENDLYSEASON", "PURCHASED", "STORE",
|
||
"WATCHLIST", "DELETEWATCHLIST", "TRADEPILE", "TRADE", "DELETETRADE",
|
||
"MARKETDATA", "CLIENTDATA", "AUTH", "DELETE_AUTH", "PHISHING", "CAPTCHA",
|
||
"TFA", "SQUADMODE", "DRAFT", "CHAMPIONS", "V2STORE", "LIVEMESSAGE",
|
||
"ADMIN", "DEBUG", "MAINTENANCE",
|
||
] # 48, VA 0x18021df80 NAME field
|
||
|
||
# Per-call overrides (pass 3, runs last and wins). Not required if pass 2 lands,
|
||
# but free insurance for the boot set.
|
||
FUT_RS4_CALLS_BOOT = [
|
||
"GETSETTINGS", "AUTHENTICATION", "LOGIN", "LOGOUT", "CREATEUSER",
|
||
"GETUSERINFO", "GETUSERDATA", "GETUSERCREDITS", "USERRELIABILITYINFO",
|
||
"GETHUBDATA", "GETUSERMASSINFO", "LOADACTIVESQUAD", "SQUADLIST",
|
||
"GETSQUADINFO", "GETCLUBINFO", "KEEPALIVE", "SEASONHISTORY",
|
||
]
|
||
|
||
FUT_RS4_CONFIG = (
|
||
[("FUT_RS4_APIURL_%s" % m, UTAS_BASE) for m in FUT_RS4_MODULES]
|
||
+ [("FUT_RS4_URL_%s" % c, UTAS_BASE) for c in FUT_RS4_CALLS_BOOT]
|
||
+ [("FUT_RS4_BASE_URL", UTAS_BASE)] # exe-side FutServerCall @0x1438dbe88
|
||
)
|
||
```
|
||
|
||
```python
|
||
def client_config_for(cfid: str) -> list:
|
||
"""-> sorted [(key, value)]. Unknown CFID -> [] ..."""
|
||
if cfid == "BlazeSDK":
|
||
return sorted(blazesdk_config() + FUT_RS4_CONFIG)
|
||
# FUT_RS4_* keys ride on EVERY CFID: we have not proven which section
|
||
# CardsDLL's accessor (engineTable[0x170] -> vt+0x118) reads.
|
||
return sorted((CLIENT_CONFIGS.get(cfid) or []) + FUT_RS4_CONFIG)
|
||
```
|
||
|
||
Timing is safe: the whole CFID sweep (OSDK_CORE … OSDK_ROSTER) completes at login
|
||
(`/tmp/blaze_responder.log` 21:58:11, lines 909-1338), long before EnterFUT runs the resolver.
|
||
|
||
**Do not add:** `FUT_TARGET_PORT` (poisons the port via the `FUT_MAX_HOPS` bug),
|
||
`FUT/MODULE_BASEURL_*`, `FUT/SINGLE_BASEURL_*` (dead), `POW` (leave absent so no `Hash:` header /
|
||
proof-of-work round-trip is required).
|
||
|
||
### 2.2 (b) Minimal UTAS server — `fifa17-recon/tools/utas_server.py`
|
||
|
||
Plain HTTP on `127.0.0.1:8099`. Rules distilled from §1.7:
|
||
|
||
* **Never 401/403** (triggers the silent re-auth storm).
|
||
* Every body must be **parseable JSON** (parse failure ⇒ err `0x3E6` ⇒ ServerFatalError).
|
||
* `204` with no body is explicitly accepted and skips parsing — good for fire-and-forget calls.
|
||
* `404` is accepted **only** on the first user lookup (new-user branch).
|
||
* Accept any HTTP method on any route (the method enum was not decoded to a literal).
|
||
* SKU segment is wildcarded (`/ut/game/<anything>/…`).
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""Minimal FIFA 17 UTAS/RS4 server (OpenFUT, clean-room).
|
||
|
||
CardsDLL resolves every RS4 endpoint to <base> + path, where <base> comes from
|
||
FUT_RS4_APIURL_<MODULE> / FUT_RS4_URL_<CALL> (blaze_responder_v3b.py) and path is
|
||
moduleTable[i] with %s -> "game/<sku>". Auth is POST <base>ut/auth; the response's
|
||
"sid" becomes the X-UT-SID header on every later call (CardsDLL @0x180126080).
|
||
|
||
Rules (from CardsDLL 0x18016D230 / 0x1801a33a0):
|
||
* NEVER 401/403 -> silent re-auth storm (3 retries) then ServerFatalError.
|
||
* 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
|
||
|
||
ADDR = ("127.0.0.1", 8099)
|
||
LOG = "/tmp/utas_server.log"
|
||
SID = "OPENFUT-SID-0000000000000001"
|
||
PERSONA_ID = 33068179 # blaze LoginResponse SESS.BUID / PDTL.PID
|
||
PERSONA_NAME = "CAGE" # PDTL.DSNM
|
||
# Flip to True once you want to exercise the create-club path instead.
|
||
NEW_USER = False
|
||
|
||
|
||
def now():
|
||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
|
||
def log(m):
|
||
line = "[%s] %s" % (datetime.datetime.now().strftime("%H:%M:%S"), m)
|
||
print(line, flush=True)
|
||
with open(LOG, "a") as f:
|
||
f.write(line + "\n")
|
||
|
||
|
||
# ---- payloads -------------------------------------------------------------
|
||
def auth_body():
|
||
# Only "sid" is load-bearing (parser 0x1801a2880 -> ServerCall+0x1e8).
|
||
# serverTime/lastOnlineTime are atoi'd at char offsets 0,5,8,11,14,17.
|
||
return {"protocol": 1, "sid": SID, "serverTime": now(), "lastOnlineTime": now()}
|
||
|
||
|
||
def user_info():
|
||
# Deserializer 0x18013EC10; every member optional (unknown key ids are
|
||
# skipped via 0x180135FF0), so {} also parses.
|
||
return {
|
||
"personaId": PERSONA_ID,
|
||
"clubName": "OpenFUT", "clubAbbr": "OFC", "established": "2026",
|
||
"clubNameChangeAllowed": True,
|
||
"currencies": [{"name": "coins", "value": 15000},
|
||
{"name": "points", "value": 0}],
|
||
"won": 0, "draw": 0, "loss": 0,
|
||
"divisionOffline": 10, "divisionOnline": 10,
|
||
"purchased": False,
|
||
"feature": {"trade": True},
|
||
"reliability": {"reliability": 100, "matchUnfinishedTime": 0},
|
||
"unopenedPacks": {"preOrderPacks": 0, "recoveredPacks": 0},
|
||
"bidTokens": {"count": 0, "updateTime": 0},
|
||
"trophies": 0, "sessionCoinsBankBalance": 0,
|
||
"actives": [], "squadList": [],
|
||
}
|
||
|
||
|
||
# GET ut/game/<sku>/user parser 0x180146970 does Parse + TWO NextToken calls
|
||
# before deserializing -> the object MUST be wrapped in one member. The member
|
||
# NAME is never compared, but the nesting level is required.
|
||
USER_GET = {"userInfo": user_info()}
|
||
# POST ut/game/<sku>/user (CreateUser, 0x18014CC60) recognises exactly:
|
||
# bonusPacks(0x5d) login(0x1a5) squad(0x2cd) starterPack(0x2e5) userData(0x36d).
|
||
USER_POST = {"login": True, "userData": user_info(),
|
||
"squad": {}, "starterPack": {}, "bonusPacks": []}
|
||
# GET ut/game/<sku>/settings (0x18013C6D0) recognises ONE key: configs (0xa2).
|
||
SETTINGS = {"configs": []}
|
||
|
||
G = r"/ut/game/[^/]+"
|
||
ROUTES = [
|
||
(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)),
|
||
(re.compile(G + r"/user/credits"), lambda m, h: (200, {"credits": 15000})),
|
||
(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"/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, {})),
|
||
(re.compile(G + r"/season"), lambda m, h: (200, {})),
|
||
(re.compile(G + r"/club"), lambda m, h: (200, {})),
|
||
]
|
||
|
||
|
||
def user_route(h):
|
||
if h.command == "POST":
|
||
return 200, USER_POST
|
||
if NEW_USER:
|
||
# accepted, non-fatal: 0x18007AF20 treats 404 as the new-user branch
|
||
return 404, {}
|
||
return 200, USER_GET
|
||
|
||
|
||
class H(http.server.BaseHTTPRequestHandler):
|
||
protocol_version = "HTTP/1.1"
|
||
|
||
def _handle(self):
|
||
n = int(self.headers.get("Content-Length", 0) or 0)
|
||
body = self.rfile.read(n) if n else b""
|
||
log("%s %s" % (self.command, self.path))
|
||
for k, v in self.headers.items():
|
||
log(" %s: %s" % (k, v))
|
||
if body:
|
||
log(" body: %s" % body[:1200].decode("utf-8", "replace"))
|
||
|
||
code, payload = 200, {}
|
||
for rx, fn in ROUTES:
|
||
if rx.search(self.path):
|
||
code, payload = fn(rx, self)
|
||
break
|
||
else:
|
||
log(" !! UNMAPPED PATH -> catch-all 200 {}")
|
||
|
||
raw = b"" if payload is None else json.dumps(payload).encode()
|
||
self.send_response(code)
|
||
if raw:
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Content-Length", str(len(raw)))
|
||
self.end_headers()
|
||
if self.command != "HEAD" and raw:
|
||
self.wfile.write(raw)
|
||
log(" -> %d %s" % (code, raw[:200].decode() if raw else "(no body)"))
|
||
|
||
do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = do_PATCH = _handle
|
||
|
||
def log_message(self, *a):
|
||
pass
|
||
|
||
|
||
if __name__ == "__main__":
|
||
open(LOG, "a").close()
|
||
log("=== utas_server http://%s:%d ===" % ADDR)
|
||
http.server.ThreadingHTTPServer(ADDR, H).serve_forever()
|
||
```
|
||
|
||
Exact wire responses for the two that matter:
|
||
|
||
```
|
||
POST /ut/auth
|
||
HTTP/1.1 200 OK
|
||
Content-Type: application/json
|
||
{"protocol":1,"sid":"OPENFUT-SID-0000000000000001",
|
||
"serverTime":"2026-07-31 22:40:00","lastOnlineTime":"2026-07-31 22:40:00"}
|
||
|
||
GET /ut/game/fifa17/settings
|
||
HTTP/1.1 200 OK
|
||
Content-Type: application/json
|
||
{"configs":[]}
|
||
```
|
||
|
||
Every subsequent request must be answered 2xx and must carry (from the client)
|
||
`X-UT-SID: OPENFUT-SID-0000000000000001`.
|
||
|
||
### 2.3 Fastest validation with no relaunch (live poke)
|
||
|
||
CardsDLL live base `0x6ffffc140000` (slide `0x6FFE7C140000`). Write a pointer to your own
|
||
NUL-terminated URL string into every descriptor's baseUrl field:
|
||
|
||
```
|
||
for i in 0..125: *(u64*)(0x1802caa20 + i*0x30 + 0x18 + slide) = <ptr to "http://127.0.0.1:8099/">
|
||
```
|
||
|
||
then re-enter Ultimate Team. `/proc/PID/mem` writes work under `ptrace_scope=1` (already proven by
|
||
`openfut-poke`). This bypasses both the responder and the DNS question in one step.
|
||
|
||
---
|
||
|
||
## 3. OBSERVABLE SUCCESS SIGNALS (strongest first)
|
||
|
||
1. **A 4th FIFA socket** — `ss -tnp | grep FIFA17` shows `SYN-SENT`/`ESTAB` to `127.0.0.1:8099`
|
||
(today there is none: three Blaze/roster sockets only). This alone proves URL resolution +
|
||
`connect()`, i.e. the whole of §1.2–1.4 landed.
|
||
2. **`/tmp/utas_server.log` shows a real request line** with the literal SKU:
|
||
`GET /ut/game/fifa17/settings` (or `POST /ut/auth`) plus headers
|
||
`Accept: application/json`, `Content-Type: application/json`, `User-Agent: ProtoHttp …`.
|
||
This is also how we *read* the SKU instead of guessing it.
|
||
3. **`POST /ut/auth`** with a body containing `"method":"authcode"` and our
|
||
`"identification":{"authCode":"OPENFUT-000…"}` — proves the Blaze authCode reached CardsDLL.
|
||
4. **Subsequent requests carry `X-UT-SID: OPENFUT-SID-0000000000000001`** — proves the sid was
|
||
stored at `ServerCall+0x1e8` and re-emitted by `0x180126080`; i.e. CardsDLL considers itself
|
||
authenticated. If a later request arrives with an *empty* `X-UT-SID:`, the sid is per-call and we
|
||
have another site to find.
|
||
5. **No re-auth storm** — the same `POST /ut/auth` must not repeat 2–3 times in a row (that pattern
|
||
means we answered 401/403 somewhere).
|
||
6. **In-game**: the "error connecting to FIFA 17 Ultimate Team" popup does not appear; the nav flow
|
||
advances `beginFUTLogin → retrieveUserData → loginComplete → FinalizeLogin` and emits the
|
||
telemetry `('GAME','ACTN','MODE',"enter_hub")`, landing on the FUT hub (or the starter-squad
|
||
screen if we answer 404 / `NEW_USER=True`).
|
||
7. **Live re-reads** (no relaunch needed):
|
||
* descriptor baseUrl at `0x1802caa20 + i*0x30 + 0x18 + slide` now reads `http://127.0.0.1:8099/`
|
||
instead of `http://easw.easports.com:8099/` → the config keys were consumed;
|
||
* `grep` process memory for `ut/game/` → currently **zero** hits; any hit proves
|
||
`BuildEndpointPath` @`0x180123da0` ran;
|
||
* the telemetry ring should now read `{"type":"utas","status":"start_flow"}` **without** the
|
||
following `{"type":"utas","status":"error","status_code":"0"}`.
|
||
|
||
---
|
||
|
||
## 4. WHAT STILL NEEDS A LIVE EXPERIMENT
|
||
|
||
| # | Unknown | Why it matters | Cheapest resolution |
|
||
|---|---|---|---|
|
||
|1|**The SKU token** returned by `engine->vt[0x250]` (feeds `game/%s`). `fifa17` is inferred from FIFA17.exe `0x1438dac20` + `GAMEID=fifa17`, never read directly (the getter lives in the encrypted exe).|Wrong token ⇒ our routes 404.|Wildcarded in the server (`/ut/game/[^/]+/…`); the first log line tells us the truth.|
|
||
|2|**Which config section CardsDLL reads** (`engineTable[0x170] → vt+0x118`, HasKey `+0xe8` / GetString `+0x30`) vs the exe's merged `_all` section (`getSection @0x14719e050`).|If it is a different store, the keys never land.|Already mitigated: keys attached to every CFID + the `/etc/hosts` Path B fallback.|
|
||
|3|**Report 3's finding that `ut/game/` has zero hits in 1.87 GB of process memory**, i.e. no RS4 path was ever built — which would put the block *upstream* of URL resolution (the nav flow never firing `beginFUTLogin`).|If true, config keys alone will not produce a socket.|Discriminator: after the fix, if the descriptors show our URL but still no SYN, instrument `0x180018F50` (`beginFUTLogin`) / `0x180122440` (the fatal-error site). Counter-evidence: the `utas start_flow`/`error status_code 0` telemetry pair says the flow *did* run and got no HTTP status — consistent with a DNS failure after the path was built into a since-freed heap buffer.|
|
||
|4|**HTTP method** for each call (the ProtoHttp method enum was not decoded to a literal `POST`/`GET`).|None if we accept all methods.|Server accepts every verb; log records the truth.|
|
||
|5|**Boot order** of the initial-load calls. `GetSettings` is the only non-market call with `preAuthFlag=1` (descriptor `+0x20`), which strongly implies it is first, but the driver (`CardsAdaptor vt[0x758]/[0x768]`) lives outside CardsDLL.|Determines which response must be right first.|Read the access log.|
|
||
|6|**Killswitch semantics** — `desc+0x21` is live 0 for all 126 (verified). If 0 means "disabled" rather than "not killed", `{"configs":[]}` would starve every call.|Could block everything after GetSettings.|If GetSettings returns and nothing else is ever requested, populate `configs` from the setter thunks (e.g. `0x180124020` → `0x1802cae31` = GetSettings' own byte) and the literal `FUT_INSTRUCTIONS_KILLSWITCH` (`0x180202FB0`).|
|
||
|7|**`Hash: %s`** value (auth-only header, from `svc->vt[0x420]`, hex digest — alphabet `0123456789abcdef` @0x21f598) and the **`POW`** feature flag.|Only if EA-side validation were mirrored; we control the flag.|Leave `POW` unserved; if a `Hash:` header shows up anyway, ignore it server-side.|
|
||
|8|**The per-class error extractor `call->vt[0x60]`** (`0x18016D47B`) that turns a parsed body into `[resp+0x1c]`. Key ids located: `code`(0x92), `reason`(0x279), `debug`(0xcb), `string`(0x2f6), `errorState`(0x10d); the function itself was not reversed.|If a 200 with a body still yields ServerFatalError, this is the next target — most likely our body contains a `code` member it reads as an error.|Keep bodies minimal (no `code`/`reason`/`debug` keys) — the supplied payloads already avoid them.|
|
||
|9|**The content/CDN base** (`vt+0x3f8` → `<X>/pow/mm/`, `<X>/fut/`), live EMPTY.|May cause a *secondary* failure after the API connects (LiveMessage / hub assets).|If a post-auth failure mentions pow/mm, add a config value shaped like `http://127.0.0.1:8099//fifa/fltOnlineAssets/2013`.|
|
||
|10|**The exe-side `FutServerCall`** (`user/accountinfo`, header `Easw-Session-Data-Nucleus-Id: %lld`, no X-UT-SID) — its response shape (`userAccountInfo`, `personas`, `userClubList`, `clubName`, `established`, `assetId`, `returningUser`) is read from live strings, not from a reversed parser.|A second, independent client that may error separately.|Route already stubbed to `200 {}`; iterate from its log line.|
|
||
|
||
### The next gate after this one
|
||
|
||
Assuming we clear auth, the flow reaches `retrieveUserData` → `FinalizeLogin`. Two outcomes:
|
||
|
||
* **Existing-club path** (`NEW_USER=False`): the hub loads against `{"userInfo":{…}}` with an **empty
|
||
club and empty squad** (`"actives":[]`, `"squadList":[]`). The hub will render, but the moment the
|
||
UI asks for real content — `LoadActiveSquad`, `SquadList`, `ViewCards` / `ut/game/fifa17/item`,
|
||
`GetUserMassInfo` — it needs an actual item set. Empty arrays should be *structurally* valid
|
||
(every member is optional, unknown key ids are skipped) but will likely surface as an empty club
|
||
screen or a "squad required" wall.
|
||
* **New-user path** (`NEW_USER=True`, or a 404 on the first GET): `GotoStarterSquad` /
|
||
`SetNewUserContextData` → `POST ut/game/fifa17/user` (CreateUser) with
|
||
`{"login":true,"userData":{…},"starterPack":{…},"squad":{…},"bonusPacks":[]}` — that is where a
|
||
**real starter pack + a legal 11-player squad** must be produced.
|
||
|
||
Either way, the next gate is **content, not protocol** — which is exactly `openfut-core`'s job
|
||
(`data/cards/`, `data/packs/`, the squad/club models). The clean handoff is: `utas_server.py` stays a
|
||
thin router that proxies `/ut/game/*/…` to `openfut-core` on `:8080`, keeping only `ut/auth` and the
|
||
X-UT-SID session locally. The item-JSON schema for that work is already recoverable — CardsDLL parses
|
||
by **hashed key ids** (FNV-1a `0x811c9dc5` @ `0x180180D00`, red-black map @ `0x1802E6598`, unknown ⇒
|
||
id `0x38C` and silently skipped) against a **907-entry key dictionary at VA `0x1802D2760`** (file
|
||
`0x2D0D60`, alphabetical: index 6 = `accountCreatedPlatformName` … `0x38a` = `yellowCards`). That
|
||
table is the complete FUT JSON vocabulary and should be dumped into `openfut-core` as the schema
|
||
reference for the content phase.
|
||
|
||
---
|
||
|
||
## Appendix A — disagreements between the four reversals, resolved
|
||
|
||
| Claim | Verdict |
|
||
|---|---|
|
||
|"Config keys are served empty ⇒ no URL" (report 2)|**Rejected.** Live: all 126 descriptors hold the easw default; DNS NXDOMAIN is the failure. Confirmed independently by reports 1, 3, 4 and re-verified here.|
|
||
|Descriptor table at `0x1802caa28` +0x10 (r1) vs `0x1802caa20` +0x18 (r3/r4)|**Same address** (`0x2caa38`). Normalised to base `0x1802caa20`, stride `0x30`, baseUrl at `+0x18`.|
|
||
|`FUT/MODULE_BASEURL_%s` live vs dead|**Dead** — verified in disasm here: formatted into `[rbp+0x180]`, lookups take `[rsp+0x40]`.|
|
||
|`%s` = platform token (brief, report 2) vs module/call NAME (r1, r3, r4)|**Module/call NAME.** The platform token appears only in `game/%s`.|
|
||
|`serverTime` `"YYYY-MM-DD HH:MM:SS"` (r2/r4) vs `"YYYY-MM-DDTHH:MM:SS"` (r3)|**Both parse** — six `atoi` at fixed char offsets 0,5,8,11,14,17. Use the space form.|
|
||
|Port 8082 (r3) vs 8099 (r1, r4)|**8099**, so the `easw.easports.com → 127.0.0.1` hosts trick works with zero config.|
|
||
|`/user/hub`, `/user/registration`, `/active/user/%lld`, `%s/pow/mm/` as auth/initial-load (brief)|**All misattributed** — FUT Champions, active-squad loader, and the FIFA-Points store respectively.|
|
||
|125 vs 126 endpoint descriptors|**125 real** (idx 0–124) + one terminator at idx 125 (`moduleIdx = 48`, empty name).|
|
||
|
||
## Appendix B — the 125 call tags (for `FUT_RS4_URL_<TAG>` if ever needed)
|
||
|
||
ISSEARCH, ISOFFERTRADE, ISSTART, RELISTALL, GETCLUBUSERS, GETCLUBINFO, CLUBSEARCH, CLUBSTATS,
|
||
STAFFSTATS, CONSUMABLESSEARCH, DREAMSQUADSEARCH, GETSQUADINFO, UPDATESQUADNAME, RETRIEVESQUAD,
|
||
LOADACTIVESQUAD, DELETESQUAD, SAVESQUAD, SQUADLIST, GETLBOPTIONS, GETLBENTRIES, GETLBENTRYDATA,
|
||
GETSETTINGS, AUTHENTICATION, LOGIN, LOGOUT, RESETUSER, USERRELIABILITYINFO, CREATEUSER, GETUSERINFO,
|
||
SETUSERINFO, GETHISTORICAL, SETTUTDATA, GETTOWDATA, SETTOWDATA, SETFAVDATA, GETUSERCREDITS,
|
||
GETUSERDATA, VIEWCARDS, ASSIGNCARD, APPLYCARD, APPLYCARDBYRES, ACTIVATECARD, CONSUMECARD, DISCARDCARD,
|
||
DISCARDCARDBYRES, DISCARDACARD, MOVECARD, MOVECARDBYRES, SWAPCARD, CREATEMATCH, MATCHREADY,
|
||
DESTROYMATCH, PLAYGAME, RESETMATCH, KEEPALIVE, LOADCATEGORYDETAILS, LOADSETCHALLENGES, STARTCHALLENGE,
|
||
LOADSQUADCHALLENGE, SAVESQUADCHALLENGE, SUBMITCHALLENGE, TAGSETS, SETSBCDATA, TOURNAMENTLIST,
|
||
TOURNAMENTTEAMS, GETACTIVETOURNAMENTS, UPDATETOURNAMENT, TOURNAMENTLOADDATA, TOURNAMENTQUIT,
|
||
SEASONLIST, SEASONUPDATE, SEASONLOADDATA, SEASONQUIT, SEASONHISTORY, PURCHASEDITEMS, PURCHASEPACK,
|
||
PURCHASEITEMS, STOREPACKTYPES, STOREPACKQUANTITIES, ISREMOVEWATCH, ISWATCHTRADE, ISWATCHLIST,
|
||
ISVIEWTRADE, GETTRADEPILE, GETAUCTIONCOUNT, ISREMOVETRADE, GETSUGGESTEDPRICING, CHANGECLUBNAME,
|
||
GETPHISHINGQUESTION, SETPHISHINGANSWER, VALIDATEPHISHINGANSWER, GETTRUSTEDCONSOLELIST, GETCAPTCHA,
|
||
EXCHANGECAPTCHA, VALIDATECAPTCHA, VALIDATETFA, UPDATEUSERACTION, GETUSERACTION,
|
||
GETMANAGERQUESTREWARD, SETMANAGERQUESTCOMPLETE, GETHUBDATA, GETUSERMASSINFO, FIFAPOINTSTRANSFER,
|
||
FRIENDLYSEASONUPDATE, FRIENDLYSEASONLOAD, FRIENDLYSEASONHISTORY, GETAVAILABLELOANPLAYERS,
|
||
SIGNLOANPLAYER, GETCHEMISTRYATTR, GETDRAFTCURRENTSTATE, GETDRAFTCHOICES, GETDRAFTSTATS, GETDRAFTAWARD,
|
||
PURCHASEDRAFTMODE, PICKDRAFTCHOICE, PICKDRAFTAUTOCHOICE, GETSTORYMODEREWARD, CHAMPIONSHUB,
|
||
CHAMPIONSTOPX, CHAMPIONSRANK, CHAMPIONSFRIENDS, GRANTPRIZECHAMPIONUSER, REGISTERCHAMPIONSLEAGUE,
|
||
GetChampionsUserCountry, LIVEMESSAGE
|
||
|
||
*(Note: tag 124 is spelled `GetChampionsUserCountry` in mixed case in the binary — the key would be
|
||
`FUT_RS4_URL_GetChampionsUserCountry`, byte-for-byte.)*
|