//! 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}::"); } 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::" ); 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); } }