43917a0051
Wire the PRODUCTION constructor so the economy authority is not test-only. Server::from_config now builds one process-lifetime AsyncBridge, opens the durable MarketStore + PileStore (paths from config), shares one HttpCoreClient as both CoreAccess and CoreEconomy, builds the content pool from Core, and attaches EconomyServices via with_economy. Stores/bridge are host-lifetime, never per request. - config.rs: required OPENFUT_MARKET_DB / OPENFUT_PILE_DB (durable file paths; must survive host restart — no temp defaults). - Fail-closed startup: a bridge/store that cannot initialize returns Err from from_config (host refuses to start) — NEVER a silent omission or a Python economy fallback. Test: from_config_constructs_and_serves_economy — builds the Server via the REAL from_config (disposable config: temp market/pile/identity + a catalog file derived from seeded content + the real tables dir) against a live Core, drives credits / purchasegroup / Store BUY / market list-query-buy through it, then rebuilds from the SAME config after a Core restart and asserts the balance persisted. host 71 lib + 3 integration + 24 host_test green; clippy/fmt clean.
86 lines
3.7 KiB
Rust
86 lines
3.7 KiB
Rust
//! 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,
|
|
/// Durable FIFA17 transfer-market listing DB (host-owned SQLite). Required
|
|
/// for the economy cutover; must survive host restart (a real path, not a
|
|
/// temp file). Env `OPENFUT_MARKET_DB`.
|
|
pub market_db_path: String,
|
|
/// Durable FIFA17 item-pile metadata DB (host-owned SQLite). Required; must
|
|
/// survive host restart. Env `OPENFUT_PILE_DB`.
|
|
pub pile_db_path: String,
|
|
}
|
|
|
|
#[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<String, ConfigError> {
|
|
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<i64, ConfigError> {
|
|
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<Self, ConfigError> {
|
|
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")?,
|
|
market_db_path: required("OPENFUT_MARKET_DB")?,
|
|
pile_db_path: required("OPENFUT_PILE_DB")?,
|
|
})
|
|
}
|
|
}
|