//! Environment → [`HostConfig`]. Client-visible bind and the Python upstream are //! REQUIRED with no default (host-family discipline: a defaulted port could //! collide with the live oracle). `core_url` defaults to Bridge's convention. use std::env; #[derive(Debug, Clone)] pub struct HostConfig { /// Where this host listens (the address FIFA reaches for UTAS). Required. pub listen_addr: String, /// Base URL of the Python UTAS oracle for fallback, e.g. /// `http://127.0.0.1:8199`. Required — must NOT be this host's own address. pub python_upstream: String, /// OpenFUT Core base URL. Default `http://127.0.0.1:8080` (Bridge convention). pub core_url: String, /// Directory holding `leagues.json`/`nations.json`/`teams.json`. pub tables_dir: String, /// FIFA 17 card-definition **identity catalog** (card id → FIFA asset id). /// Required production identity source: a `/club` item's `resourceId` comes /// from here. Startup fails if it cannot be loaded — never a placeholder. pub catalog_path: String, /// Persistent external-identity **store** file (owned-instance → stable wire /// id). Required: the wire `id` of every owned item is allocated/resolved /// here so it survives restart and reverses exactly. pub identity_store_path: String, /// The launcher-selected FIFA persona id, injected via `OPENFUT_PERSONA_ID`. /// Required, non-zero: it stamps `personaId` on the Core-backed /// `GET /squad/active`, and must match the persona LSX/Blaze/POW/UTAS use. pub persona_id: i64, } #[derive(Debug)] pub struct ConfigError(pub String); impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl std::error::Error for ConfigError {} fn required(key: &str) -> Result { match env::var(key) { Ok(v) if !v.is_empty() => Ok(v), _ => Err(ConfigError(format!("{key} is required (no default)"))), } } /// Parse a required, non-zero i64 env var. A zero identity id is invalid (the /// client rejects a zero persona), so unset/empty/non-integer/zero is a hard error. fn required_i64_nonzero(key: &str) -> Result { let raw = required(key)?; let val: i64 = raw .parse() .map_err(|_| ConfigError(format!("{key} must be an integer, got {raw:?}")))?; if val == 0 { return Err(ConfigError(format!("{key} must be non-zero"))); } Ok(val) } impl HostConfig { pub fn from_env() -> Result { Ok(HostConfig { listen_addr: required("OPENFUT_UTAS_HOST_ADDR")?, python_upstream: required("OPENFUT_UTAS_PYTHON_URL")?, core_url: env::var("OPENFUT_CORE_URL") .unwrap_or_else(|_| "http://127.0.0.1:8080".into()), tables_dir: env::var("OPENFUT_FIFA17_TABLES_DIR") .unwrap_or_else(|_| "fifa17-recon/data/tables".into()), catalog_path: required("OPENFUT_FIFA17_CATALOG")?, identity_store_path: required("OPENFUT_IDENTITY_STORE")?, persona_id: required_i64_nonzero("OPENFUT_PERSONA_ID")?, }) } }