//! The identity LSX reports, ported from `fut_account.py`'s tier-1/tier-2 fields. //! //! SOURCED FROM `fut_account.ACCOUNT`, shared with blaze_responder_v3b.py, //! fut_store.py, fut_seed.py and utas_server.py. //! //! THE CONSTRAINT IS CROSS-LAYER CONSISTENCY, NOT ANY PARTICULAR VALUE: what LSX //! reports here must equal what Blaze returns in LoginResponse.SESS.PDTL and what //! UTAS serves as userInfo.personaId. (An older comment blamed a mismatch for //! AUTH_ERR_INVALID_PERSONA / AUTH_ERR_USER_DOES_NOT_MATCH_PERSONA / //! AUTH_ERR_PERSONA_NOT_FOUND -- those are Blaze *server* error codes and we are //! the server. Neither "CAGE" nor "33068179" appears in FIFA17.exe, CardsDLL or //! dbdata.dll; 33068179 lives only in stp-origin_emu.dll's own ini default. They //! stay the defaults because they are what the working stack asserts.) //! //! PRECEDENCE: env var > built-in default. //! //! `fut_account.py` sits one tier deeper -- env > `fut_account.json` > default -- //! but the JSON is deliberately NOT read here: its persisted values for the three //! fields LSX uses (`persona_id`, `persona_name`; `locale` is not even stored) are //! identical to the built-in defaults, and the launcher always passes //! `FUT_PERSONA_ID`/`FUT_PERSONA_NAME` explicitly when it spawns us //! (openfut-launcher/src/local_services.rs), so env decides in every real run. //! Parsing a JSON file to reach the same answer would only add a failure mode -- //! and the launcher, not a file next to the Python tools, is the identity owner //! for this binary. use crate::{ConfigError, Env}; // ------------------------------------------------------------------ tier 1 // LOCKED WIRE CONSTANTS. No env override on purpose: these are not preferences. /// FIFA 17 EA offer id (retail). pub const CONTENT_ID: &str = "1027460"; /// `TRIAL_ONLINE_ACCESS` for FIFA17_Trial.exe; retail uses this. pub const ENTITLEMENT_TAG: &str = "ONLINE_ACCESS"; // ------------------------------------------------------------------ tier 2 pub const DEFAULT_PERSONA_ID: i64 = 33068179; pub const DEFAULT_PERSONA_NAME: &str = "CAGE"; pub const DEFAULT_LOCALE: &str = "en_US"; /// The identity fields LSX puts on the wire. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Identity { /// Blaze SESS.BUID / SESS.UID / PDTL.PID, LSX PersonaId/UserId, UTAS /// userInfo.personaId and squad.personaId. pub persona_id: i64, /// Blaze PDTL.DSNM / LSX GetProfileResponse Persona / UTAS sellerName. pub persona_name: String, /// LSX `GetSetting LANGUAGE`. pub locale: String, } impl Default for Identity { fn default() -> Self { Self { persona_id: DEFAULT_PERSONA_ID, persona_name: DEFAULT_PERSONA_NAME.to_string(), locale: DEFAULT_LOCALE.to_string(), } } } impl Identity { /// Read the process environment. pub fn from_env() -> Result { Self::from_vars(&crate::os_env) } /// A `FUT_PERSONA_ID` that `int()` would reject kills `fut_account.py` at /// import; we refuse to start for the same reason, rather than serve one /// layer of the stack a silently different persona. pub fn from_vars(env: Env<'_>) -> Result { let persona_id = match env("FUT_PERSONA_ID") { // `int()` tolerates surrounding whitespace and a sign. Some(v) => v.trim().parse::().map_err(|_| ConfigError { var: "FUT_PERSONA_ID", value: v.clone(), expected: "an integer", })?, None => DEFAULT_PERSONA_ID, }; Ok(Self { persona_id, persona_name: env("FUT_PERSONA_NAME").unwrap_or_else(|| DEFAULT_PERSONA_NAME.into()), locale: env("FUT_LOCALE").unwrap_or_else(|| DEFAULT_LOCALE.into()), }) } /// Blaze blazeId / userId (SESS.BUID, SESS.UID, AccountInfo.UID). /// /// DERIVED, read-only, and deliberately NOT an independent knob: the client /// sends both `nuc` and `nucleusPersonaId` and both came out equal, so the /// getter->field mapping is undetermined. Do not split them until a live test /// proves Blaze USER_ID may legitimately differ from PERSONA_ID. pub fn user_id(&self) -> i64 { self.persona_id } } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option { let map: HashMap = pairs .iter() .map(|(k, v)| ((*k).to_string(), (*v).to_string())) .collect(); move |k: &str| map.get(k).cloned() } #[test] fn defaults_are_the_working_stacks_values() { let id = Identity::from_vars(&env_of(&[])).unwrap(); assert_eq!(id.persona_id, 33068179); assert_eq!(id.persona_name, "CAGE"); assert_eq!(id.locale, "en_US"); assert_eq!(id.user_id(), id.persona_id); assert_eq!(id, Identity::default()); } #[test] fn env_wins_over_defaults() { let id = Identity::from_vars(&env_of(&[ ("FUT_PERSONA_ID", "1234567"), ("FUT_PERSONA_NAME", "OTHER"), ("FUT_LOCALE", "de_DE"), ])) .unwrap(); assert_eq!(id.persona_id, 1234567); assert_eq!(id.persona_name, "OTHER"); assert_eq!(id.locale, "de_DE"); assert_eq!(id.user_id(), 1234567); } #[test] fn empty_env_value_still_wins() { // `os.environ.get` returns "" for `FUT_PERSONA_NAME=`, and "" is not None. let id = Identity::from_vars(&env_of(&[("FUT_PERSONA_NAME", "")])).unwrap(); assert_eq!(id.persona_name, ""); } #[test] fn whitespace_around_the_persona_id_is_tolerated_like_int() { let id = Identity::from_vars(&env_of(&[("FUT_PERSONA_ID", " 42 ")])).unwrap(); assert_eq!(id.persona_id, 42); } #[test] fn a_non_numeric_persona_id_is_refused() { let err = Identity::from_vars(&env_of(&[("FUT_PERSONA_ID", "CAGE")])).unwrap_err(); assert_eq!(err.var, "FUT_PERSONA_ID"); assert_eq!(err.to_string(), r#"FUT_PERSONA_ID must be an integer (got "CAGE")"#); } }