Files
OpenFUT/openfut-adapter-fifa17/src/blaze/ids.rs
T
funman300 cf961603fe openfut-adapter-fifa17: FIFA 17 Blaze adapter, oracle-tested
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.

  blaze/ids.rs           component/command/notification tables
  blaze/config.rs        injectable identity + endpoints, nothing hardcoded
  blaze/session.rs       per-connection state
  blaze/client_config.rs the fetchClientConfig tables
  blaze/responses.rs     16 Blaze::* response bodies
  blaze/dispatch.rs      (component, command) -> Vec<Frame>

Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.

98 tests green across both crates; clippy clean.

MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.

The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.

Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.

Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.

Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:18:29 +00:00

270 lines
11 KiB
Rust

//! FIFA 17 Blaze component, command and notification IDs.
//!
//! This is exactly the knowledge that must NOT live in
//! `openfut-protocol-blaze`: the generic layer routes on numbers, and what
//! those numbers mean is per-title.
//!
//! # Provenance
//!
//! The Util table was recovered from FIFA17.exe's own `getCommandName` switch
//! (jump table `0x141b17af4`). The Authentication table could not be recovered
//! statically — the name pool is Denuvo-mutated — so it was obtained by CALLING
//! the client's own `getCommandName` (`0x146e0d2a0`) in-process over ids 1..320,
//! validated by reproducing the known Util names, and cross-checked against a
//! static REST-binding struct (`0x143896a80` → `trustedLogin = 0x0B`).
//! UserSessions notification ids come from the clean, unmutated
//! `getNotificationName` jump table at `0x141b03f70`.
//!
//! Names are for diagnostics only. Dispatch matches on the numeric constants.
pub mod component {
pub const AUTHENTICATION: u16 = 0x0001;
pub const GAME_MANAGER: u16 = 0x0004;
pub const REDIRECTOR: u16 = 0x0005;
pub const STATS: u16 = 0x0007;
pub const UTIL: u16 = 0x0009;
pub const CENSUS_DATA: u16 = 0x000A;
pub const CLUBS: u16 = 0x000B;
pub const MESSAGING: u16 = 0x000F;
pub const ASSOCIATION_LISTS: u16 = 0x0019;
pub const GAME_REPORTING: u16 = 0x001C;
pub const SPONSORED_EVENTS: u16 = 0x081C;
pub const OSDK_SETTINGS: u16 = 0x08C9;
pub const USER_SESSIONS: u16 = 0x7802;
}
pub mod util {
pub const FETCH_CLIENT_CONFIG: u16 = 0x0001;
pub const PING: u16 = 0x0002;
pub const PRE_AUTH: u16 = 0x0007;
pub const POST_AUTH: u16 = 0x0008;
pub const USER_SETTINGS_LOAD: u16 = 0x000A;
pub const USER_SETTINGS_SAVE: u16 = 0x000B;
pub const FETCH_QOS_CONFIG: u16 = 0x0015;
pub const SET_CLIENT_METRICS: u16 = 0x0016;
pub const SET_CLIENT_STATE: u16 = 0x001C;
}
pub mod auth {
pub const LOGIN: u16 = 0x000A;
pub const TRUSTED_LOGIN: u16 = 0x000B;
pub const LIST_USER_ENTITLEMENTS2: u16 = 0x001D;
pub const GET_ACCOUNT: u16 = 0x001E;
pub const LIST_ENTITLEMENTS: u16 = 0x0020;
pub const GET_AUTH_TOKEN: u16 = 0x0024;
pub const GRANT_ENTITLEMENT2: u16 = 0x0027;
pub const LIST_PERSONA_ENTITLEMENTS2: u16 = 0x0030;
pub const EXPRESS_LOGIN: u16 = 0x003C;
/// Routine "drop any stale session" step before PCLogin, NOT a failure.
pub const LOGOUT: u16 = 0x0046;
pub const GET_PERSONA: u16 = 0x005A;
pub const LIST_PERSONAS: u16 = 0x0064;
}
pub mod user_sessions {
pub const UPDATE_NETWORK_INFO: u16 = 0x0014;
/// Notification ids live in a separate number space from commands.
pub mod notify {
pub const EXTENDED_DATA_UPDATE: u16 = 0x0001;
pub const USER_ADDED: u16 = 0x0002;
pub const USER_REMOVED: u16 = 0x0003;
pub const USER_UPDATED: u16 = 0x0005;
pub const USER_AUTHENTICATED: u16 = 0x0008;
pub const USER_UNAUTHENTICATED: u16 = 0x0009;
pub const SERVER_DRAINING: u16 = 0x000C;
}
}
pub mod association_lists {
pub const GET_LISTS: u16 = 0x0006;
}
pub mod census_data {
pub const SUBSCRIBE_TO_CENSUS_DATA_UPDATES: u16 = 0x0005;
}
/// Components advertised in `PreAuthResponse.CIDS`.
///
/// Order is the oracle's and is preserved: `CIDS` is a TDF list, and list
/// elements are NOT reordered by the encoder the way struct members are.
pub const ADVERTISED_COMPONENT_IDS: [i64; 9] = [
component::AUTHENTICATION as i64,
component::GAME_MANAGER as i64,
component::REDIRECTOR as i64,
component::STATS as i64,
component::UTIL as i64,
component::MESSAGING as i64,
component::ASSOCIATION_LISTS as i64,
component::GAME_REPORTING as i64,
component::USER_SESSIONS as i64,
];
pub fn component_name(component: u16) -> Option<&'static str> {
Some(match component {
component::AUTHENTICATION => "Authentication",
component::GAME_MANAGER => "GameManager",
component::REDIRECTOR => "Redirector",
component::STATS => "Stats",
component::UTIL => "Util",
component::CENSUS_DATA => "CensusData",
component::CLUBS => "Clubs",
component::MESSAGING => "Messaging",
component::ASSOCIATION_LISTS => "AssociationLists",
component::GAME_REPORTING => "GameReporting",
component::SPONSORED_EVENTS => "SponsoredEvents",
component::OSDK_SETTINGS => "OSDKSettings",
component::USER_SESSIONS => "UserSessions",
_ => return None,
})
}
pub fn command_name(component: u16, command: u16) -> Option<&'static str> {
Some(match (component, command) {
(component::UTIL, 0x01) => "fetchClientConfig",
(component::UTIL, 0x02) => "ping",
(component::UTIL, 0x03) => "setClientData",
(component::UTIL, 0x04) => "localizeStrings",
(component::UTIL, 0x05) => "getTelemetryServer",
(component::UTIL, 0x06) => "getTickerServer",
(component::UTIL, 0x07) => "preAuth",
(component::UTIL, 0x08) => "postAuth",
(component::UTIL, 0x0A) => "userSettingsLoad",
(component::UTIL, 0x0B) => "userSettingsSave",
(component::UTIL, 0x0C) => "userSettingsLoadAll",
(component::UTIL, 0x0E) => "userSettingsDelete",
(component::UTIL, 0x0F) => "userSettingsLoadAllForUser",
(component::UTIL, 0x14) => "filterForProfanity",
(component::UTIL, 0x15) => "fetchQosConfig",
(component::UTIL, 0x16) => "setClientMetrics",
(component::UTIL, 0x17) => "setConnectionState",
(component::UTIL, 0x19) => "getUserOptions",
(component::UTIL, 0x1A) => "setUserOptions",
(component::UTIL, 0x1B) => "suspendUserPing",
(component::UTIL, 0x1C) => "setClientState",
(component::AUTHENTICATION, 0x0A) => "login",
(component::AUTHENTICATION, 0x0B) => "trustedLogin",
(component::AUTHENTICATION, 0x14) => "updateAccount",
(component::AUTHENTICATION, 0x15) => "upgradeAccount",
(component::AUTHENTICATION, 0x1D) => "listUserEntitlements2",
(component::AUTHENTICATION, 0x1E) => "getAccount",
(component::AUTHENTICATION, 0x1F) => "grantEntitlement",
(component::AUTHENTICATION, 0x20) => "listEntitlements",
(component::AUTHENTICATION, 0x22) => "getUseCount",
(component::AUTHENTICATION, 0x23) => "decrementUseCount",
(component::AUTHENTICATION, 0x24) => "getAuthToken",
(component::AUTHENTICATION, 0x26) => "getPasswordRules",
(component::AUTHENTICATION, 0x27) => "grantEntitlement2",
(component::AUTHENTICATION, 0x2B) => "modifyEntitlement2",
(component::AUTHENTICATION, 0x2C) => "consumecode",
(component::AUTHENTICATION, 0x2D) => "passwordForgot",
(component::AUTHENTICATION, 0x2F) => "getPrivacyPolicyContent",
(component::AUTHENTICATION, 0x30) => "listPersonaEntitlements2",
(component::AUTHENTICATION, 0x33) => "checkAgeReq",
(component::AUTHENTICATION, 0x34) => "getOptIn",
(component::AUTHENTICATION, 0x35) => "enableOptIn",
(component::AUTHENTICATION, 0x36) => "disableOptIn",
(component::AUTHENTICATION, 0x3C) => "expressLogin",
(component::AUTHENTICATION, 0x46) => "logout",
(component::AUTHENTICATION, 0x5A) => "getPersona",
(component::AUTHENTICATION, 0x64) => "listPersonas",
(component::AUTHENTICATION, 0x65) => "expressCreateAccount",
(component::AUTHENTICATION, 0xE6) => "createWalUserSession",
(component::AUTHENTICATION, 0xF1) => "acceptLegalDocs",
(component::AUTHENTICATION, 0xF2) => "getEmailOptInSettings",
(component::AUTHENTICATION, 0xF6) => "getTermsOfServiceContent",
(component::AUTHENTICATION, 0x104) => "getOriginPersona",
(component::AUTHENTICATION, 0x10E) => "checkEmail",
(component::AUTHENTICATION, 0x118) => "getPersonaNameSuggestions",
(component::AUTHENTICATION, 0x122) => "guestLogin",
(component::CENSUS_DATA, 0x01) => "subscribeToCensusData",
(component::CENSUS_DATA, 0x02) => "unsubscribeFromCensusData",
(component::CENSUS_DATA, 0x03) => "getRegionCounts",
(component::CENSUS_DATA, 0x04) => "getLatestCensusData",
(component::CENSUS_DATA, 0x05) => "subscribeToCensusDataUpdates",
(component::USER_SESSIONS, 0x14) => "updateNetworkInfo",
(component::ASSOCIATION_LISTS, 0x06) => "getLists",
_ => return None,
})
}
pub fn notification_name(component: u16, notify_id: u16) -> Option<&'static str> {
if component != component::USER_SESSIONS {
return None;
}
Some(match notify_id {
0x01 => "UserSessionExtendedDataUpdate",
0x02 => "UserAdded",
0x03 => "UserRemoved",
0x05 => "UserUpdated",
0x08 => "UserAuthenticated",
0x09 => "UserUnauthenticated",
0x0C => "ServerDraining",
_ => return None,
})
}
/// Human-readable label for a route, for logs and captures.
pub fn describe(component: u16, command: u16, is_notification: bool) -> String {
let comp = component_name(component)
.map(str::to_string)
.unwrap_or_else(|| format!("Component:0x{component:04x}"));
if is_notification {
if let Some(n) = notification_name(component, command) {
return format!("{comp}::<{n}>");
}
return format!("{comp}::<notify:0x{command:04x}>");
}
match command_name(component, command) {
Some(name) => format!("{comp}::{name}"),
None => format!("{comp}::cmd:0x{command:04x}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describes_the_first_rpc_fifa_sends() {
assert_eq!(
describe(component::UTIL, util::PRE_AUTH, false),
"Util::preAuth"
);
}
#[test]
fn commands_and_notifications_are_separate_number_spaces() {
// 0x0002 is UserSessions "UserAdded" as a notification, but is not a
// known UserSessions *command*.
assert_eq!(
describe(component::USER_SESSIONS, 0x0002, true),
"UserSessions::<UserAdded>"
);
assert_eq!(
describe(component::USER_SESSIONS, 0x0002, false),
"UserSessions::cmd:0x0002"
);
}
#[test]
fn unknown_routes_degrade_to_numbers() {
assert_eq!(
describe(0x1234, 0x0001, false),
"Component:0x1234::cmd:0x0001"
);
}
#[test]
fn advertised_components_are_in_oracle_order() {
// A list, not a struct: the encoder will NOT sort these, so the order
// here is the order on the wire.
assert_eq!(ADVERTISED_COMPONENT_IDS[0], 0x0001);
assert_eq!(ADVERTISED_COMPONENT_IDS[8], 0x7802);
assert_eq!(ADVERTISED_COMPONENT_IDS.len(), 9);
}
}