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

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/Z7Research/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)

  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: CDAT8e 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<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-nrtqos-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.