# 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 | 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 }` = 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** also tagged `CONF`. Union of MEC + GW2 + ME4 keys (all three agree on the common subset): ``` associationListSkipInitialSet = "1" autoReconnectEnabled = "0" bytevaultHostname = # 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 # pingSiteInfoByAliasMap, alias -> {PSA,PSP,SNA} SVID = # 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--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 = ` | | 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 = `, `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=""`, `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=""` | | … | 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.