Files
OpenFUT/fifa17-recon/tools/preauth_refs.md
T
funman300 6ddd5e9d47 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
2026-08-01 20:24:30 -07:00

367 lines
23 KiB
Markdown

# Util::preAuth (component 0x0009 / command 0x0007) — PreAuthResponse + Fire2 reply rules
Research for the FIFA 17 offline Blaze emulator. **Clean-room.** Nothing below comes from
the 2021 EA source leak. Every item is tagged with a provenance class:
| Class | Meaning |
|---|---|
| **(a-obs)** | Observed directly in *our own* FIFA 17 client's traffic (`fifa17-recon/captures/blaze/`) |
| **(a-cr)** | Independent third-party clean-room reimplementation / packet capture (repos listed below) |
| **(a-conv)** | Converged: ≥3 independent (a-cr) sources agree byte-for-byte |
| **(b-?)** | Provenance unverified — treat as suspect, do not copy verbatim without an independent check |
---
## 0. Sources used (and their provenance)
| Ref | What | Game / Blaze ver | Provenance |
|---|---|---|---|
| **R1** | `scratchpad/grid-blaze/` = [grid-leak/blaze](https://github.com/grid-leak) (Rust) | Mirror's Edge Catalyst, server `Blaze 15.1.1.0.5` | (a-cr) README explicitly states "clean-room implementation based entirely on network analysis"; credits packet-capture contributors |
| **R2** | `scratchpad/pamplona-future/` = [ploxxxy/pamplona-future](https://github.com/ploxxxy/pamplona-future) (TS) | MEC, same | (a-cr) same lineage as R1 (R1 is its Rust successor) |
| **R3** | `scratchpad/tdf/` = [jacobtread/tdf](https://github.com/jacobtread/tdf) (Rust) | generic Heat2 codec | (a-cr) codec only, derived from PocketRelay network RE |
| **R4** | `scratchpad/catalyst-mitm/` = [ploxxxy/catalyst-mitm](https://github.com/ploxxxy/catalyst-mitm) | MEC MITM capture tool | (a-cr) capture tooling only, no schema |
| **R5** | [Khysnik/Z7](https://github.com/Khysnik/Z7) — `Research/data/Blaze/**/*.txt` **decoded live captures** + `MasterServer/src/**` (C++ server) | PvZ Garden Warfare 2, server `Blaze 15.1.1.4.6`, client BSDK `15.1.1.1.0` | (a-cr) repo self-describes as "a reverse-engineered blaze server"; the `Research/data` files are decoded wire captures with redactions |
| **R6** | [Khysnik/GW2BlazeServer](https://github.com/Khysnik/GW2BlazeServer) + [Khysnik/BlazeSDK](https://github.com/Khysnik/BlazeSDK) (Go) | GW2 | (a-cr) working Fire2 codec + server; `Fire2.go` header doc-comment is the clearest framing spec found |
| **R7** | [PocketRelay/PocketArk](https://github.com/PocketRelay/PocketArk) `src/blaze/models/util.rs` | Mass Effect (ME4/Andromeda-era), `INST="masseffect-4-pc"` | (a-cr) PocketRelay lineage = network RE |
| **R8** | [Aim4kill/BlazeSDK](https://github.com/Aim4kill/BlazeSDK) `Blaze3SDK/Blaze/Util/PreAuthResponse.cs`, `ProtoFire/Frames/*` | Blaze **3.x** SDK reimplementation | **(b-?)** Contains full EA class names, member names (`mAnonymousChildAccountsEnabled`), TDF member indices and tag hashes. That level of detail is consistent with *reflection extraction from a game binary* (same technique we use), but the repo carries **no provenance statement**. Used here **only** to confirm field *semantics* that are already independently confirmed by (a-cr) sources. Do not copy code from it. |
Local copies of everything fetched: `/tmp/claude-1000/-home-alex-Documents-OpenFUT/b89d9ca6-265d-4444-969c-6923501c168a/scratchpad/refs/`
Notable **negative result**: no FIFA-specific Blaze emulator exists publicly. GitHub code
search for `fifa-2017-pc`, `"-PC-SERVER-BLAZE" fifa`, `INST "fifa-2017" blaze` returns nothing.
We are the first. All schema below must be adapted from the MEC / GW2 / ME 15.x cousins.
---
## 1. ⚠️ CORRECTION to our previously assumed Fire2 header layout
The header layout in the task brief was **wrong** and would have produced replies the client
drops. What we read as `msgType` was actually the low byte of a 24-bit message number.
### Correct layout (16 bytes, big-endian) — **(a-conv)**: R1 `packet.rs:120-180`, R2 `blaze.ts:48-95`, R5 `packet.cpp:85-170`, R6 `Fire2.go` header comment + `Fire2Encoder.go:EncodePacket`
```
[0:4] u32 payload length (NOT counting header or metadata)
[4:6] u16 metadata length (bytes of extra TDF struct placed BETWEEN header and payload)
[6:8] u16 component id
[8:10] u16 command id
[10:13] u24 message number <-- 3 bytes, big-endian
[13] u8 (msgType << 5) | (userIndex & 0x1F)
[14] u8 options (OPTION_IMMEDIATE = 0x01)
[15] u8 reserved (0)
```
Wire order: `header(16) || metadata(metaLen) || payload(payloadLen)`.
There is **no error-code field in the Fire2 header** (that is Fire *v1*, a different 12-byte
frame with an error u16 at [6:8] — see R8 `ProtoFire/Frames/FireFrame.cs`). Do not echo an
"error/msgId" field; it does not exist here.
### Re-decode of our own captures with the corrected layout — **(a-obs)**
| file | payloadLen | comp | cmd | **msgNum** | byte13 | msgType |
|---|---|---|---|---|---|---|
| `blaze_fire2_46521.bin` | 203 | 0x0009 | 0x0007 | **0** | 0x00 | MESSAGE |
| `blaze_fire2_45833.bin` | 0 | 0x0009 | 0x0002 | **1** | 0x00 | MESSAGE |
| `blaze_fire2_37161.bin` | 203 | 0x0009 | 0x0007 | **2** | 0x00 | MESSAGE |
| `blaze_fire2_36227.bin` | 0 | 0x0009 | 0x0002 | **3** | 0x00 | MESSAGE |
| `blaze_fire2_40571.bin` | 203 | 0x0009 | 0x0007 | **4** | 0x00 | MESSAGE |
| `blaze_fire2_39309.bin` | 0 | 0x0009 | 0x0002 | **5** | 0x00 | MESSAGE |
| `blaze_fire2_33803.bin` | 203 | 0x0009 | 0x0007 | **6** | 0x00 | MESSAGE |
| `blaze_fire2_41609.bin` | 0 | 0x0009 | 0x0002 | **7** | 0x00 | MESSAGE |
So: FIFA 17 sends `preAuth` then immediately an **empty `Util::ping` (cmd 2) without waiting
for the preAuth reply**, on every connection attempt, and `msgNum` is a *process-global*
counter that keeps incrementing across reconnects. Every one of these frames is
`msgType = 0 (MESSAGE)`, `userIndex = 0`. This exactly matches GW2's observed order
(R5: msgNum 0 = preAuth, msgNum 1 = ping).
### MessageType enum — **(a-conv)**: R1 `packet.rs:11-20`, R2 `blaze.ts:5-12`, R6 `Types.go`, R8 `MessageType.cs`
```
MESSAGE = 0 (client request)
REPLY = 1 (<-- what we must send for PreAuthResponse)
NOTIFICATION = 2 (server-initiated, unsolicited)
ERROR_REPLY = 3
PING = 4
PING_REPLY = 5
```
Byte 13 of a reply is therefore `1 << 5 = 0x20` (with userIndex 0).
### Reply construction rules — **(a-conv)**
1. **Copy the whole request header**, then overwrite byte 13's top 3 bits with `REPLY`.
Component, command, **msgNum and userIndex are echoed verbatim**. (R5 `Packet::createReply()`
literally memcpy's the header; R6 `MsgNum(pkt.Header.MessageNumber)`; R1 `Fire2Frame::reply()`
keeps `..*self`.)
2. `metadataLen = 0` and `payloadLen = len(serialized TDF)` are recomputed.
3. **Notifications** use `msgType = 2` and `msgNum = 0` (R1 `Fire2Frame::notification`,
R2 `.encode(0)`), i.e. notifications are *not* correlated to a request.
4. **No qtail / seqno / context.** Context and jumbo-frame handling exist only in Fire v1
(R8 `FireFrame.cs` `Option.HAS_CONTEXT/JUMBO_*`). Fire2 has none of it — all four Fire2
emulators write a flat 16-byte header and nothing else.
5. **Ping**: FIFA 17's keep-alive is `Util::ping` as a normal `MESSAGE` (not msgType 4), so
reply with a normal `REPLY` on component 9 / command 2. The msgType 4/5 PING/PING_REPLY
pair is a separate transport-level heartbeat (R1 routes it as component 0 / command 0 and
answers with msgType 5 and an empty body) — implement that too as a cheap safety net.
### Error replies — **UNRESOLVED, three conflicting clean-room encodings**
| Source | Where the error code goes |
|---|---|
| R5 `packet.cpp:155` (working GW2 C++ server) | `msgType=3`, empty metadata+payload, u16 error written into header bytes **[14:16]** |
| R6 `Fire2.go` (working GW2 Go server) | `msgType=3`, `ERRC` read from the **metadata** TDF struct |
| R1 `packet.rs:103` | `msgType=1(!)` with `CNTX`/`ERRC` in the **payload** — code carries a `// TODO: move ErrorBody to metadata` |
Not on the preAuth critical path (we return a success REPLY). Flag for later; prefer R6
(metadata `ERRC`) since its decoder was validated against a real client, and cross-check
against FIFA 17's own reaction.
---
## 2. Heat2 (TDF) encoding rules — **(a-conv)** R3 + R6, validated byte-for-byte against our own capture
* **Tag**: 3 bytes. `packed = Σ_{i<4} ((upper(tag[i]) - 0x20) & 0x3F) << (26 - 6*i)`; emit the
top 3 bytes of that u32. Missing/short chars contribute 0 (decode to a trailing space, which
is why real tags are written `"PSA "`, `"LNP "`, `"SNA "`, `"UID "`, `"LOC "`).
Verified: `CDAT``8e 48 74`, exactly the bytes at offset 0x10 of our capture.
* **Field** = `tag(3) || type(1) || value`.
* **Types**: `0 int(varint)`, `1 string`, `2 blob`, `3 struct`, `4 list`, `5 map`, `6 union`,
`7 variable`, `8 objtype`, `9 objid`, `10 float(be f32)`, `11 timevalue(varint µs)`, `12 generic`.
* **Varint**: first byte = 6 data bits, **bit 0x40 = negative sign (NOT data)**, bit 0x80 = continue;
subsequent bytes 7 data bits + 0x80 continue. (Our existing `decode_fire2.py` masks `&0x3f`, so
it reads magnitudes correctly but silently drops the sign — fine for now, noted.)
* **String**: `varint(len+1) || bytes || 0x00` — the length **includes** the NUL terminator.
* **Struct**: nested fields, terminated by a `0x00` byte. **The root payload has NO terminator.**
(R6 `encodeStruct(fields, root)`.)
* **List**: `elemType(1) || varint(count) || elements`.
* **Map**: `keyType(1) || valType(1) || varint(count) || (key,value)*`.
* **Struct as a list/map element**: just `fields... || 0x00`, no prefix. *Except*: some
polymorphic struct lists take a leading arm byte (R3 `#[tdf(prefix_two)]` → a literal `0x02`;
R6 `ArmedStruct`). None of the preAuth fields need it.
* **Empty lists/maps are omitted entirely** rather than emitted with count 0 (R6 `isEmptyCollection`).
* **Member order**: EA emits members sorted by *packed tag value* ascending (≈alphabetical).
Decoders are tag-driven so this is cosmetic, but R8's member tables are in that order and every
capture obeys it — match it, it's free.
---
## 3. Our FIFA 17 preAuth REQUEST, re-decoded cleanly (203/203 bytes consumed) — **(a-obs)**
```
CDAT { IITO=0 LANG=1701729619 ('enUS') SVCN="fifa-2017-pc" TYPE=0 }
CINF { BSDK="15.1.1.3.0" BTIM="Jun 9 2017 16:15:40" CLNT="FIFA17" CPFT=4 (pc)
CSKU="FIFAPC" CVER="3175939" DSDK="15.1.2.1.0"
ENV="prod" LOC=1701729619 ('enUS') PTVR="1.1" }
FCCR { CFID="BlazeSDK" }
LADD = 1761610250
```
**Key insight:** `FCCR.CFID = "BlazeSDK"` is an embedded `FetchClientConfigRequest`. The
`CONF` block of the PreAuthResponse is the answer to it — i.e. `CONF.CONF` must be the
**`BlazeSDK` config section** (pingPeriod, connIdleTimeout, nucleus* URLs, …). That is exactly
what MEC/GW2/ME all put there. Confirmed independently by R5 `util.cpp`, whose
`fetchClientConfig` handler has a dedicated `section == "BlazeSDK"` branch returning
`{pingPeriod, defaultRequestTimeout, connIdleTimeout, autoReconnectEnabled, maxReconnectAttempts}`.
GW2's request for comparison (R5 `Blaze__Util__PreAuthRequest.txt`) is identical in shape;
FIFA 17 adds one extra field, `CINF.PTVR = "1.1"`.
---
## 4. PreAuthResponse — the concrete field list
### 4.1 Field-by-field, with semantic names — **(a-conv)** for tags, (b-?) only for the human-readable names
Tag names/semantics from R6 `types/UtilComponent.go` (a-cr), R1/R2 comments (a-cr), R5
`util.cpp` comments (a-cr); the EA-style long names in R8 (b-?) agree with all of them.
| Tag | Type | Meaning | MEC (R1/R2) | GW2 (R5/R6, real capture) | ME4 (R7) |
|---|---|---|---|---|---|
| `ASRC` | string | authenticationSource (numeric title/telemetry id) | `"308903"` | `"310695"` | `"310335"` |
| `CIDS` | list<int> | componentIds — components configured on the server | see below | see below | see below |
| `CLID` | string | clientId — Nucleus client id for this service | `"MirrorsEdgeCatalyst-SERVER-PC"` | `"PVZGW2-PC-SERVER-BLAZE"` | `"ME4-PC-SERVER-BLAZE"` |
| `CONF` | struct | config — a `FetchConfigResponse`, i.e. `{ CONF: map<string,string> }` = the `BlazeSDK` section | see 4.2 | see 4.2 | see 4.2 |
| `ESRC` | string | entitlementSource | `"308903"` | `"310695"` | `"310335"` |
| `INST` | string | **serviceName — must match the client's `CDAT.SVCN`** | `"mirrorsedgecatalyst-2016-pc"` | `"plantsvszombies-gw2-pc"` | `"masseffect-4-pc"` |
| `MAID` | int | machineId — uniquely identifies the server machine, arbitrary u32 | `1129238128` | `3310897674` | `2291763061` |
| `MINR` | int/bool | underageSupported | `0` | `1` | `0` |
| `NASP` | string | personaNamespace | `"cem_ea_id"` | `"cem_ea_id"` | `"cem_ea_id"` |
| `PILD` | string | legalDocGameIdentifier | `""` | `""` | `""` |
| `PLAT` | string | platform | `"pc"` | `"pc"` | `"pc"` |
| `QOSS` | struct | qosSettings (`QosConfigInfo`) | see 4.3 | see 4.3 | see 4.3 |
| `RSRC` | string | registrationSource | `"308903"` | `"310695"` | (const) |
| `SVER` | string | serverVersion | `"Blaze 15.1.1.0.5 (CL# 1893137)\n"` | `"Blaze 15.1.1.4.6 (CL# 2136954)\n"` | (const) |
Fields present in R8's Blaze **3.x** descriptor but **absent from every 15.x capture**:
`ANON` (anonymousChildAccountsEnabled), `CNGN` (parentalConsentEntitlementGroupName),
`PTAG` (parentalConsentEntitlementTag). Conversely `CLID`/`ESRC`/`MAID` are 15.x additions
not in the 3.x descriptor. **Do not emit ANON/CNGN/PTAG** — no 15.x server does.
Exact raw GW2 capture (R5 `Research/data/Blaze/Util/Blaze__Util__PreAuthResponse.txt`) is
saved verbatim at `refs/z7_preauth.txt`. Header line: `//comp=0x0009 cmd=0x0007 msgType=Reply msgNum=0`
— confirming reply msgType and msgNum echo.
### 4.2 `CONF` — the `BlazeSDK` config section
`CONF` is a **struct** containing a single **map<string,string>** also tagged `CONF`.
Union of MEC + GW2 + ME4 keys (all three agree on the common subset):
```
associationListSkipInitialSet = "1"
autoReconnectEnabled = "0"
bytevaultHostname = <host> # point at ourselves or leave EA's
bytevaultPort = "42210"
bytevaultSecure = "true"|"false"
cachedUserRefreshInterval = "1s" # GW2/ME4 only
connIdleTimeout = "40s"
defaultRequestTimeout = "20s"
maxReconnectAttempts = "30" # GW2/ME4 only
nucleusConnect = "https://accounts.ea.com"
nucleusConnectTrusted = "https://accounts2s.ea.com"
nucleusPortal = "https://signin.ea.com"
nucleusProxy = "https://gateway.ea.com"
pingPeriod = "20s"
userManagerMaxCachedUsers = "0"
voipHeadsetUpdateRate = "1000"
xblTokenUrn = "accounts.ea.com"
xboxOneStringValidationUri = "client-strings.xboxlive.com"
```
Game-specific extras seen: `Override_ProtoHttp_LoginStateMachine_DedicatedServer_vers`
(GW2), `arubaDisabled/arubaEndpoint/arubaHostname/riverEnv/riverHost/riverPort/
disableDisconnectOnOrbitError` (ME4), `bugSentry*`/`gateway*`/`npsWebUrlBase` (MEC — but those
live in the *game's own* config section, not `BlazeSDK`). Start with the common set only.
`pingPeriod` is what tells the client how often to send `Util::ping`. `connIdleTimeout` is
what our responder must not exceed before it drops the socket.
### 4.3 `QOSS` — QosConfigInfo (struct)
Tags from R6 `types/FrameworkTypes.go` (a-cr):
```
QOSS {
BWPS { # bandwidthPingSiteInfo (QosPingSiteInfo) — leave blank/zero
"PSA " = "" # address
"PSP " = 0 # port
"SNA " = "" # siteName (present in MEC; ABSENT in the GW2 capture)
}
"LNP " = 10 # numLatencyProbes
LTPS = map<string, struct QosPingSiteInfo> # pingSiteInfoByAliasMap, alias -> {PSA,PSP,SNA}
SVID = <u32> # serviceId (present in MEC; ABSENT in the GW2 capture)
TIME = 5000000 | 10000000 # timeout (µs)
}
```
Real EA aliases/hosts (all three sources agree): `bio-dub`, `bio-iad`, `bio-sjc`, `bio-syd`,
`m3d-brz`/`i3d-gru`, `m3d-nrt`/`i3d-nrt``qos-prod-<alias>-common-common.gos.ea.com` port
**17504**. (The GW2 dump prints `PSP = 34976` = 17504<<1, an artefact of that dumper's varint
printing; 17504 is the real port, confirmed by MEC + ME4 source.)
Both working emulators (R5, R7) replace the map with a **single entry pointing at localhost**
so the client's QoS probe fails fast locally instead of timing out against dead EA hosts.
Do the same. `SNA` is optional; include it (harmless) or drop it.
### 4.4 `CIDS` — component id list
This is a hint list of which components the server has configured. Observed values:
* MEC: `30728, 24, 1, 30729, 25, 30730, 27, 9, 10, 33, 63490, 15, 30720, 30722, 30723, 30724, 21, 30726, 2000, 30727`
* GW2: `61448, 1, 61449, 25, 61450, 27, 4, 7, 9, 10, 33, 126978, 15, 61440, 61441, 61442, 61443, 61444, 61445, 61446, 61447, 3984`
* ME4: `1, 4, 7, 9, 10, 11, 14, 15, 25, 2000, 27, 30720, 30721, 30722, 30723, 30724, 33, 30725, 30726, 30727, 30728, 30729, 30730, 63490`
Base component ids (R6 `Fire2.go` doc-comment, a-cr): 1 Authentication, 3 Example,
4 GameManager, 5 Redirector, 7 Stats, 9 Util, 10 CensusData, 11 Clubs, 15 Messaging,
25 AssociationLists, 27 GpsContentController, 28 GameReporting, 31 ByteVault,
33 Achievements, 1025 XBLSystemConfigs, 1031 Friends, **0x7802 = 30722 UserSessions**.
Note the `0x7800`-range (MEC/ME4) vs `0xF000`-range (GW2) discrepancy for the framework
components — version-dependent, and GW2 lists `0xF002` in CIDS while its notifications
genuinely arrive on `0x7802`. Treat CIDS as advisory.
**FIFA 17's real component set is unknown and is not recoverable from any public repo.**
Recommended: (i) first shot = the MEC list (closest structural analogue, same `0x7800` range,
Blaze 15.1.1.0.5 vs FIFA's client 15.1.1.3.0); (ii) recover the authoritative list from
FIFA17.exe via the reflection-descriptor technique we already used for the redirector schema —
BlazeSDK registers a component-id table and per-component RPC name tables (we already know
979 RPC names live in that binary).
---
## 5. What comes next: the preAuth → postAuth → login sequence
Exact msgNum ordering from R5's decoded GW2 session (a-cr), which matches FIFA 17's observed
opening two frames (a-obs):
| msgNum | Direction | Component/Command | Payload |
|---|---|---|---|
| 0 | C→S | `9/7` Util::preAuth | `CDAT/CINF/FCCR/LADD` |
| 0 | S→C | `9/7` **Reply** | **PreAuthResponse** (section 4) |
| 1 | C→S | `9/2` Util::ping (empty) | — |
| 1 | S→C | `9/2` Reply | `STIM = <unix seconds>` |
| 2 | C→S | `9/1` Util::fetchClientConfig | `CFID = "IdentityParams"` |
| 2 | S→C | `9/1` Reply | `CONF = { "display": "console2/welcome", "redirect_uri": "http://127.0.0.1/success" }` |
| — | (client) | opens the Nucleus/Origin login web flow using those params, obtains an auth code | |
| 3 | C→S | `1/10` Authentication::login | `AUTH = <nucleus auth code>`, `ACHT{SHID:[] SKID:[]}`, `EXTB:[]`, `EXTI=0` |
| — | S→C | `0x7802 / 8` **Notification** `UserSessions::UserAuthenticated` | `1CON ALOC BUID CGID DSNM FRST KEY LAST LLOG MAIL NASP PID PLAT UID USTP XREF` |
| 3 | S→C | `1/10` Reply | LoginResponse: `ANON=0`, `SESS{ 1CON BUID FRST KEY LLOG MAIL PDTL{DSNM LAST PID PLAT STAS XREF} UID }`, `SPAM=0`, `UNDR=0` |
| 4 | C→S | `9/8` Util::postAuth | `DSUI=0`, `MAC="<mac>"`, `UDID=""` |
| — | S→C | `0x7802 / 5` Notification `UpdateExtendedDataAttribute` (`FLGS`,`ID`) — MEC does this *before* the reply | |
| 4 | S→C | `9/8` Reply | PostAuthResponse: `TELE{ADRS ANON DISA EDCT FILT LOC MINR NOOK PORT SDLY SESS SKEY SPCT STIM SVNM}`, `TICK{ADRS PORT SKEY}`, `UROP{TMOP "UID "}` |
| — | S→C | `0x7802 / 1 or 2` Notification `UserSessionExtendedDataUpdate` / `UserAdded` | `DATA{ADDR BPS CTY CVAR DMAP HWFG ISP PSLM PSM QDAT{BWHR DBPS NAHR NATT UBPS} TZ UATT ULST USER{...} XPLT}`, `SUBS`, `USID` |
| … | C→S | `9/28` Util::setClientState | `MODE=1`, `STAT=0` |
| … | C→S | `1/0x24` Authentication::getAuthToken | (empty) → Reply `AUTH="<token>"` |
| … | C→S | `25/6` AssociationLists::getLists, `0x7802/20` UserSessions::updateNetworkInfo, `9/22` setClientMetrics, `1/29` listEntitlements … | |
Util command ids (a-conv, R1/R2/R6 identical): 1 fetchClientConfig, 2 ping, 3 setClientData,
4 localizeStrings, 5 getTelemetryServer, 6 getTickerServer, **7 preAuth**, **8 postAuth**,
10-15 userSettings*, 20 filterForProfanity, 21 fetchQosConfig, 22 setClientMetrics,
23 setConnectionState, 25/26 get/setUserOptions, 27 suspendUserPing, 28 setClientState.
### ⚠️ Ping reply tag conflict — resolve empirically
* `TIME` — MEC / pamplona (R1 `PingResponse{TIME}`, R2 `TDFInteger('TIME', …)`), Blaze 15.1.1.0.x
* `STIM` — GW2 (R5 real capture `STIM = 3557264654`; R5 C++ + R6 Go both emit `STIM`) and ME4 (R7), Blaze 15.1.1.1.0+
FIFA 17's client BSDK is `15.1.1.3.0`, i.e. **newer than GW2's 15.1.1.1.0 → `STIM` is the
likely one**. Unknown tags are skipped by the decoder, so **emit both `STIM` and `TIME`**
(sorted: STIM before TIME) and let the client pick.
### The real wall after preAuth
`Authentication::login` takes a **Nucleus auth code**, obtained by the client from
`accounts.ea.com` using the `IdentityParams` config. Both MEC and GW2 emulators still rely on
live EA OAuth (R2's README notes EA deleting their OAuth client ids as an existential threat;
R1 substitutes Discord OAuth entirely). For a fully offline FIFA 17 we will have to either
(i) redirect `nucleusConnect`/`nucleusPortal`/`nucleusProxy` in the `CONF` map at our own
local HTTP stub and mint our own code, or (ii) accept whatever `AUTH` string arrives and
reply with a canned LoginResponse (what R6 `components/authentication.go` does — it ignores
the token completely and loads a user from `config/user.json`). **(ii) is the right first
move.** Note our `CONF` map is where those three URLs are set, so preAuth is already the
lever for redirecting Nucleus.
---
## 6. Ready-to-use artefacts produced by this research
| File | What |
|---|---|
| `scratchpad/preauth_build.py` | Clean-room Heat2 **encoder** + a concrete FIFA 17 `PreAuthResponse` and `PingResponse` builder + `fire2()` framer |
| `scratchpad/fire2_full.py` | Full Heat2 **decoder** (all types, incl. list/map/union/objid) + Fire2 header parse with the corrected layout |
| `scratchpad/refs/` | Local copies of every third-party file cited above |
**Validation performed:** `fire2_full.py` parses our real FIFA 17 preAuth request with
`consumed 203/203 (CLEAN)`, and parses `preauth_build.py`'s 915-byte generated reply with
`consumed 899/899 (CLEAN)`. Generated reply header:
`00 00 03 83 | 00 00 | 00 09 | 00 07 | 00 00 00 | 20 | 00 | 00`
(payload 899, meta 0, comp 9, cmd 7, msgNum 0, msgType REPLY).
### Values that are still guesses for FIFA 17 (iterate on client reaction)
| Field | Placeholder used | How to resolve |
|---|---|---|
| `ASRC`/`ESRC`/`RSRC` | `"309111"` | EA numeric title id; grep FIFA17.exe strings for a 6-digit telemetry/project id near `river`/`telemetry` |
| `CLID` | `"FIFA17-PC-SERVER-BLAZE"` | grep FIFA17.exe for `-PC-SERVER-BLAZE` / `SERVER-BLAZE` / Nucleus client-id strings |
| `CIDS` | MEC list | recover from FIFA17.exe component registration table (reflection technique) |
| `SVER` | `"Blaze 15.1.1.3.0 (CL# 1234567)\n"` | matched to the client's own `CINF.BSDK`; trailing `\n` is what both real servers send |
| `MAID` | `1129238128` | arbitrary |
| ping tag | both `STIM`+`TIME` | observe which one stops the retry loop |
`INST = "fifa-2017-pc"` is **not** a guess — it must equal the client's `CDAT.SVCN`, which we
observed directly.