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
23 KiB
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 (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 (TS) |
MEC, same | (a-cr) same lineage as R1 (R1 is its Rust successor) |
| R3 | scratchpad/tdf/ = jacobtread/tdf (Rust) |
generic Heat2 codec | (a-cr) codec only, derived from PocketRelay network RE |
| R4 | scratchpad/catalyst-mitm/ = ploxxxy/catalyst-mitm |
MEC MITM capture tool | (a-cr) capture tooling only, no schema |
| R5 | 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 + Khysnik/BlazeSDK (Go) | GW2 | (a-cr) working Fire2 codec + server; Fire2.go header doc-comment is the clearest framing spec found |
| R7 | 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 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)
- Copy the whole request header, then overwrite byte 13's top 3 bits with
REPLY. Component, command, msgNum and userIndex are echoed verbatim. (R5Packet::createReply()literally memcpy's the header; R6MsgNum(pkt.Header.MessageNumber); R1Fire2Frame::reply()keeps..*self.) metadataLen = 0andpayloadLen = len(serialized TDF)are recomputed.- Notifications use
msgType = 2andmsgNum = 0(R1Fire2Frame::notification, R2.encode(0)), i.e. notifications are not correlated to a request. - No qtail / seqno / context. Context and jumbo-frame handling exist only in Fire v1
(R8
FireFrame.csOption.HAS_CONTEXT/JUMBO_*). Fire2 has none of it — all four Fire2 emulators write a flat 16-byte header and nothing else. - Ping: FIFA 17's keep-alive is
Util::pingas a normalMESSAGE(not msgType 4), so reply with a normalREPLYon 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.pymasks&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
0x00byte. The root payload has NO terminator. (R6encodeStruct(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 literal0x02; R6ArmedStruct). 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<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 (R1PingResponse{TIME}, R2TDFInteger('TIME', …)), Blaze 15.1.1.0.xSTIM— GW2 (R5 real captureSTIM = 3557264654; R5 C++ + R6 Go both emitSTIM) 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.