feat(utas-host): real Fifa17IdentityResolver (catalog + store + policy), drop placeholders
This commit is contained in:
Generated
+1
@@ -3269,6 +3269,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-http",
|
||||
"openfut-identity",
|
||||
"parking_lot",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
|
||||
@@ -9,6 +9,7 @@ publish = false
|
||||
[dependencies]
|
||||
openfut-adapter-fifa17 = { path = "../openfut-adapter-fifa17" }
|
||||
openfut-http = { path = "../openfut-http" }
|
||||
openfut-identity = { path = "../openfut-identity" }
|
||||
serde_json = "1"
|
||||
# Plain-HTTP client for Core queries and Python passthrough. UTAS is plaintext
|
||||
# HTTP (worker D: no wrap_socket, no cert), so no TLS backend is linked.
|
||||
|
||||
+24
-15
@@ -34,28 +34,37 @@ the host holds the [`CoreAccess`] boundary (`GET {core_url}/collection?…` toda
|
||||
|---|---|---|---|
|
||||
| `OPENFUT_UTAS_HOST_ADDR` | yes | — | where this host listens (client-visible UTAS addr) |
|
||||
| `OPENFUT_UTAS_PYTHON_URL` | yes | — | Python UTAS oracle base URL for fallback (must differ from this host) |
|
||||
| `OPENFUT_FIFA17_CATALOG` | yes | — | FIFA 17 card-definition identity catalog (`Fifa17CardCatalog` JSON: card id → asset id) |
|
||||
| `OPENFUT_IDENTITY_STORE` | yes | — | persistent external-identity store file (owned instance → stable wire id) |
|
||||
| `OPENFUT_CORE_URL` | no | `http://127.0.0.1:8080` | OpenFUT Core base |
|
||||
| `OPENFUT_FIFA17_TABLES_DIR` | no | `fifa17-recon/data/tables` | `leagues/nations/teams.json` for id⇄name |
|
||||
| `OPENFUT_FIFA17_ASSET_MAP` | no | — | JSON `{ "<core_card_id>": <fifa_asset_id> }` (see the blocker) |
|
||||
|
||||
## KNOWN BLOCKER — retail rendering of Core inventory
|
||||
Startup **fails clearly** if the catalog or identity store cannot be loaded —
|
||||
there is no placeholder fallback (exactly one production identity path).
|
||||
|
||||
## Identity model (resolved)
|
||||
|
||||
FIFA renders an owned card by resolving `resourceId & 0xffffff` against the
|
||||
client's **own local players table**; an invented id renders a **blank generic
|
||||
card** (proven live — `fut_cards.py:11-21`). OpenFUT Core's catalogue is synthetic
|
||||
string-id cards (`card_pl_001`) with **no FIFA asset id**, and no committed
|
||||
card→asset mapping exists. So:
|
||||
card** (proven live — `fut_cards.py:11-21`). Two distinct identities, never
|
||||
conflated, are resolved by [`Fifa17IdentityResolver`] (the single production path):
|
||||
|
||||
- Without `OPENFUT_FIFA17_ASSET_MAP`, `/club` returns `{"itemData":[]}` — honest,
|
||||
never faked. The shaper drops any item lacking a **real** asset id.
|
||||
- Making Core inventory actually render in retail requires a Core-card→FIFA-asset
|
||||
identity decision (seed Core from FIFA assets, or a real mapping table). This is
|
||||
the "who owns FUT state" question and is the **prerequisite** for a rendering
|
||||
retail `/club`. Filtering, pagination, entity mapping, transport and fallback are
|
||||
all done and tested independently of it.
|
||||
- **Definition identity** (`resourceId`/`assetId`) — the card's real FIFA asset
|
||||
id, from the versioned `OPENFUT_FIFA17_CATALOG`. An unmapped definition is
|
||||
**dropped and counted**, never faked.
|
||||
- **Instance identity** (`id`) — a stable, persistent, reversible wire integer
|
||||
from the generic `openfut-identity` store under the FIFA 17 wire-id policy
|
||||
(monotonic from `100_000_001`). The same owned instance keeps its id across
|
||||
restart and reverses exactly; two copies of one definition share a
|
||||
`resourceId` but get distinct `id`s. The namespace is globally monotonic
|
||||
within `(fifa17, owned-item)` — no per-account column is needed because Core
|
||||
owned-instance ids are globally-unique UUIDs.
|
||||
|
||||
`rare=SP` ("Special") stays UNSUPPORTED (semantics unproven; parsed, reported, never
|
||||
guessed).
|
||||
**Remaining prerequisite for a *rendering* retail `/club`:** Core inventory must
|
||||
reference cards that exist in the catalog. The catalog + store + resolver are
|
||||
built and tested; wiring a controlled real FIFA 17 dev-content inventory (the
|
||||
curated per-game dev pack) is the next slice. `rare=SP` ("Special") stays
|
||||
UNSUPPORTED (semantics unproven; parsed, reported, never guessed).
|
||||
|
||||
## Retail A/B runbook (first `/club` gate)
|
||||
|
||||
@@ -70,7 +79,7 @@ Preconditions (mirror the proven blaze/roster switch discipline):
|
||||
Bring-up:
|
||||
1. Move Python UTAS to an alternate port (`FUT_PORT=8199` in the container/`openfut-fut.sh`); it keeps serving there.
|
||||
2. Start this host on the client-visible UTAS addr:
|
||||
`OPENFUT_UTAS_HOST_ADDR=<lan>:8099 OPENFUT_UTAS_PYTHON_URL=http://127.0.0.1:8199 OPENFUT_CORE_URL=http://127.0.0.1:8080 OPENFUT_FIFA17_ASSET_MAP=<map.json> openfut-utas-host`
|
||||
`OPENFUT_UTAS_HOST_ADDR=<lan>:8099 OPENFUT_UTAS_PYTHON_URL=http://127.0.0.1:8199 OPENFUT_CORE_URL=http://127.0.0.1:8080 OPENFUT_FIFA17_CATALOG=<catalog.json> OPENFUT_IDENTITY_STORE=<store.json> openfut-utas-host`
|
||||
3. Launch FIFA → FUT → **My Squad** player picker and exercise: no-filter, position, nation, league, league+team, Gold+position, then scroll beyond page one.
|
||||
|
||||
Evidence to capture (all six):
|
||||
|
||||
@@ -15,9 +15,14 @@ pub struct HostConfig {
|
||||
pub core_url: String,
|
||||
/// Directory holding `leagues.json`/`nations.json`/`teams.json`.
|
||||
pub tables_dir: String,
|
||||
/// Optional JSON file mapping Core card id → FIFA asset id. Absent = the
|
||||
/// current reality (no mapping) → Core items cannot render and are dropped.
|
||||
pub asset_map_path: Option<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,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -46,9 +51,8 @@ impl HostConfig {
|
||||
.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()),
|
||||
asset_map_path: env::var("OPENFUT_FIFA17_ASSET_MAP")
|
||||
.ok()
|
||||
.filter(|s| !s.is_empty()),
|
||||
catalog_path: required("OPENFUT_FIFA17_CATALOG")?,
|
||||
identity_store_path: required("OPENFUT_IDENTITY_STORE")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+187
-67
@@ -36,16 +36,17 @@
|
||||
|
||||
pub mod config;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::Arc;
|
||||
|
||||
use openfut_adapter_fifa17::fut::catalog::{Fifa17CardCatalog, Fifa17WireItemIdPolicy};
|
||||
use openfut_adapter_fifa17::fut::club_response::{
|
||||
shape_club_response, CoreOwnedItem, Fifa17Identity, ItemIdentityResolver, ShapeStats,
|
||||
};
|
||||
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
||||
use openfut_adapter_fifa17::fut::owned_query::{map_to_core, parse_club_query, MapError};
|
||||
use openfut_identity::ExternalIdentityStore;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use config::HostConfig;
|
||||
@@ -197,64 +198,77 @@ fn core_item_from_json(e: &Value) -> Option<CoreOwnedItem> {
|
||||
})
|
||||
}
|
||||
|
||||
// ───────────────────────────── Asset resolvers ──────────────────────────────
|
||||
// ───────────────────────────── Item identity resolver ───────────────────────
|
||||
|
||||
/// The current production reality: no Core-card→FIFA-asset mapping exists, so
|
||||
/// every item is dropped (rendered response is `{"itemData":[]}`). Honest, not
|
||||
/// faked.
|
||||
pub struct EmptyAssetResolver;
|
||||
/// The single production [`ItemIdentityResolver`]: it composes the two distinct
|
||||
/// FIFA 17 identities from real, persistent sources — no placeholder, no hash,
|
||||
/// no fabricated id.
|
||||
///
|
||||
/// * **Definition identity** (`resourceId`/`assetId`) comes from the
|
||||
/// [`Fifa17CardCatalog`]: `card_id` → real FIFA asset id. An unmapped
|
||||
/// definition resolves to `None` → the item is dropped and counted, never
|
||||
/// faked.
|
||||
/// * **Instance identity** (`item_id`) comes from the generic
|
||||
/// [`ExternalIdentityStore`] under the FIFA 17 wire-id policy: the same owned
|
||||
/// instance always resolves to the same monotonic wire id, it survives
|
||||
/// restart, and it reverses exactly. Two copies of the same definition share a
|
||||
/// `resourceId` but get distinct `item_id`s.
|
||||
///
|
||||
/// The wire-id namespace is **globally monotonic within `(game, "owned-item")`**,
|
||||
/// not per-account. Python restarts numbering per save file; Core owned-instance
|
||||
/// ids are globally-unique UUIDs, so a single monotonic sequence keeps every
|
||||
/// wire id unique and its reverse lookup unambiguous across all accounts —
|
||||
/// satisfying the client's only requirement (per-session unique/stable/
|
||||
/// reversible ids). An account column is therefore unnecessary.
|
||||
pub struct Fifa17IdentityResolver {
|
||||
catalog: Fifa17CardCatalog,
|
||||
store: Arc<dyn ExternalIdentityStore>,
|
||||
}
|
||||
|
||||
impl ItemIdentityResolver for EmptyAssetResolver {
|
||||
fn resolve(&self, _item: &CoreOwnedItem) -> Option<Fifa17Identity> {
|
||||
None
|
||||
impl Fifa17IdentityResolver {
|
||||
pub fn new(catalog: Fifa17CardCatalog, store: Arc<dyn ExternalIdentityStore>) -> Self {
|
||||
Fifa17IdentityResolver { catalog, store }
|
||||
}
|
||||
|
||||
/// Reverse an owned-item wire id back to its Core owned-instance id (used by
|
||||
/// later item-operation slices). `None` = unknown wire id, never a guess.
|
||||
pub fn owned_id_for_wire(&self, wire: i64) -> Option<String> {
|
||||
self.store
|
||||
.core_for(
|
||||
Fifa17WireItemIdPolicy::GAME,
|
||||
Fifa17WireItemIdPolicy::OWNED_ITEM_KIND,
|
||||
wire,
|
||||
)
|
||||
.unwrap_or(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Map-backed resolver (from a config file or tests): Core card id → FIFA asset
|
||||
/// id. The wire item id is derived stably from the owned-card id (adequate for a
|
||||
/// read-only search; item-operation identity is a later slice).
|
||||
pub struct MapAssetResolver {
|
||||
map: HashMap<String, u32>,
|
||||
}
|
||||
|
||||
impl MapAssetResolver {
|
||||
pub fn from_map(map: HashMap<String, u32>) -> Self {
|
||||
MapAssetResolver { map }
|
||||
}
|
||||
|
||||
/// Load `{ "card_id": assetId, … }` from a JSON file.
|
||||
pub fn from_json_file(path: &str) -> std::io::Result<Self> {
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
let v: Value = serde_json::from_str(&raw)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let mut map = HashMap::new();
|
||||
if let Some(obj) = v.as_object() {
|
||||
for (k, val) in obj {
|
||||
if let Some(id) = val.as_u64() {
|
||||
map.insert(k.clone(), id as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(MapAssetResolver { map })
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable wire item id in the 100_000_000+ space (FNV-1a of the owned id).
|
||||
fn stable_item_id(owned_card_id: &str) -> u32 {
|
||||
let mut h: u32 = 2_166_136_261;
|
||||
for b in owned_card_id.bytes() {
|
||||
h ^= b as u32;
|
||||
h = h.wrapping_mul(16_777_619);
|
||||
}
|
||||
100_000_000 + (h % 900_000_000)
|
||||
}
|
||||
|
||||
impl ItemIdentityResolver for MapAssetResolver {
|
||||
impl ItemIdentityResolver for Fifa17IdentityResolver {
|
||||
fn resolve(&self, item: &CoreOwnedItem) -> Option<Fifa17Identity> {
|
||||
let asset = *self.map.get(&item.card_id)?;
|
||||
// Definition identity first: an unmapped card is dropped (never faked).
|
||||
let ident = self.catalog.lookup(&item.card_id)?;
|
||||
// Instance identity: stable, persistent, reversible wire id.
|
||||
let wire = match self.store.resolve_or_allocate(
|
||||
Fifa17WireItemIdPolicy::GAME,
|
||||
Fifa17WireItemIdPolicy::OWNED_ITEM_KIND,
|
||||
&item.owned_card_id,
|
||||
Fifa17WireItemIdPolicy::owned_item_base_floor(),
|
||||
) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
// Infrastructure failure allocating a wire id: drop this item
|
||||
// (freeze-safe) and log — never emit an unstable/fake id.
|
||||
eprintln!(
|
||||
"utas-host ERROR identity store alloc failed for {}: {e}",
|
||||
item.owned_card_id
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(Fifa17Identity {
|
||||
item_id: stable_item_id(&item.owned_card_id),
|
||||
asset_id: asset,
|
||||
// Wire ids live in 1e8..9e8 (policy) — well within u32.
|
||||
item_id: wire as u32,
|
||||
asset_id: ident.asset_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -479,18 +493,19 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build from config: load entity tables, pick the asset resolver, wire the
|
||||
/// Core client and Python passthrough.
|
||||
/// Build from config: load entity tables + the FIFA 17 identity catalog, open
|
||||
/// the persistent identity store, and wire the Core client + Python
|
||||
/// passthrough. Fails clearly if a required production identity source cannot
|
||||
/// be loaded — there is no placeholder fallback.
|
||||
pub fn from_config(cfg: &HostConfig) -> Result<Self, String> {
|
||||
let entities = Fifa17Entities::from_tables_dir(std::path::Path::new(&cfg.tables_dir))
|
||||
.map_err(|e| format!("loading entity tables from {}: {e}", cfg.tables_dir))?;
|
||||
let assets: Arc<dyn ItemIdentityResolver + Send + Sync> = match &cfg.asset_map_path {
|
||||
Some(p) => Arc::new(
|
||||
MapAssetResolver::from_json_file(p)
|
||||
.map_err(|e| format!("loading asset map {p}: {e}"))?,
|
||||
),
|
||||
None => Arc::new(EmptyAssetResolver),
|
||||
};
|
||||
let catalog = Fifa17CardCatalog::from_file(std::path::Path::new(&cfg.catalog_path))
|
||||
.map_err(|e| format!("loading card-definition catalog {}: {e}", cfg.catalog_path))?;
|
||||
let store = openfut_identity::JsonIdentityStore::open(&cfg.identity_store_path)
|
||||
.map_err(|e| format!("opening identity store {}: {e}", cfg.identity_store_path))?;
|
||||
let assets: Arc<dyn ItemIdentityResolver + Send + Sync> =
|
||||
Arc::new(Fifa17IdentityResolver::new(catalog, Arc::new(store)));
|
||||
Ok(Server {
|
||||
core: Arc::new(HttpCoreClient::new(cfg.core_url.clone())),
|
||||
entities: Arc::new(entities),
|
||||
@@ -740,12 +755,117 @@ mod tests {
|
||||
assert_eq!(it.attributes, [80, 70, 75, 78, 84, 82]);
|
||||
}
|
||||
|
||||
// ── Fifa17IdentityResolver: the single production identity path ──────────
|
||||
|
||||
fn test_catalog(cards: &[(&str, u32)]) -> Fifa17CardCatalog {
|
||||
let entries: Vec<String> = cards
|
||||
.iter()
|
||||
.map(|(id, asset)| format!("\"{id}\":{{\"asset_id\":{asset}}}"))
|
||||
.collect();
|
||||
let doc = format!(
|
||||
"{{\"schema_version\":1,\"game\":\"fifa17\",\"cards\":{{{}}}}}",
|
||||
entries.join(",")
|
||||
);
|
||||
Fifa17CardCatalog::from_json_str(&doc).unwrap()
|
||||
}
|
||||
|
||||
fn temp_store_path(tag: &str) -> std::path::PathBuf {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static N: AtomicU64 = AtomicU64::new(0);
|
||||
let n = N.fetch_add(1, Ordering::SeqCst);
|
||||
std::env::temp_dir().join(format!(
|
||||
"ofut-resolver-{tag}-{}-{n}.json",
|
||||
std::process::id()
|
||||
))
|
||||
}
|
||||
|
||||
fn owned(owned_id: &str, card: &str) -> CoreOwnedItem {
|
||||
CoreOwnedItem {
|
||||
owned_card_id: owned_id.into(),
|
||||
card_id: card.into(),
|
||||
rating: 90,
|
||||
position: "ST".into(),
|
||||
nation: "Argentina".into(),
|
||||
league: "Premier League".into(),
|
||||
club: "Chelsea".into(),
|
||||
attributes: [90, 90, 80, 91, 33, 80],
|
||||
}
|
||||
}
|
||||
|
||||
fn resolver(path: &std::path::Path, cards: &[(&str, u32)]) -> Fifa17IdentityResolver {
|
||||
let store = openfut_identity::JsonIdentityStore::open(path).unwrap();
|
||||
Fifa17IdentityResolver::new(test_catalog(cards), Arc::new(store))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stable_item_id_is_deterministic_and_in_range() {
|
||||
let a = stable_item_id("oc1");
|
||||
let b = stable_item_id("oc1");
|
||||
assert_eq!(a, b);
|
||||
assert!((100_000_000..1_000_000_000).contains(&a));
|
||||
assert_ne!(stable_item_id("oc1"), stable_item_id("oc2"));
|
||||
fn resolver_maps_definition_and_allocates_wire_id() {
|
||||
let p = temp_store_path("map");
|
||||
let r = resolver(&p, &[("card_gold_001", 20801)]);
|
||||
let id = r.resolve(&owned("oc1", "card_gold_001")).unwrap();
|
||||
assert_eq!(id.asset_id, 20801, "real asset from the catalog");
|
||||
assert_eq!(
|
||||
id.item_id, 100_000_001,
|
||||
"first wire id from the policy floor"
|
||||
);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_drops_unmapped_definition_never_faking() {
|
||||
let p = temp_store_path("drop");
|
||||
let r = resolver(&p, &[("card_gold_001", 20801)]);
|
||||
assert!(
|
||||
r.resolve(&owned("oc1", "card_unknown")).is_none(),
|
||||
"no catalog entry => dropped, never a fabricated id"
|
||||
);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_copies_of_a_definition_share_resource_but_get_distinct_wire_ids() {
|
||||
let p = temp_store_path("copies");
|
||||
let r = resolver(&p, &[("card_gold_001", 20801)]);
|
||||
let a = r.resolve(&owned("oc1", "card_gold_001")).unwrap();
|
||||
let b = r.resolve(&owned("oc2", "card_gold_001")).unwrap();
|
||||
assert_eq!(a.asset_id, b.asset_id, "same definition => same resourceId");
|
||||
assert_ne!(a.item_id, b.item_id, "distinct copies => distinct wire ids");
|
||||
// Idempotent: the same owned instance re-resolves to the same wire id.
|
||||
assert_eq!(
|
||||
r.resolve(&owned("oc1", "card_gold_001")).unwrap().item_id,
|
||||
a.item_id
|
||||
);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wire_id_survives_restart_and_reverses_exactly() {
|
||||
let p = temp_store_path("restart");
|
||||
let first = {
|
||||
let r = resolver(&p, &[("card_gold_001", 20801)]);
|
||||
r.resolve(&owned("oc-stable", "card_gold_001"))
|
||||
.unwrap()
|
||||
.item_id
|
||||
};
|
||||
// Reopen the same store file (simulating a host restart).
|
||||
let r2 = resolver(&p, &[("card_gold_001", 20801)]);
|
||||
let again = r2
|
||||
.resolve(&owned("oc-stable", "card_gold_001"))
|
||||
.unwrap()
|
||||
.item_id;
|
||||
assert_eq!(
|
||||
first, again,
|
||||
"same owned instance keeps its wire id across restart"
|
||||
);
|
||||
assert_eq!(
|
||||
r2.owned_id_for_wire(again as i64).as_deref(),
|
||||
Some("oc-stable"),
|
||||
"reverse lookup returns the exact owned instance"
|
||||
);
|
||||
assert_eq!(
|
||||
r2.owned_id_for_wire(999_999_999),
|
||||
None,
|
||||
"unknown wire id => None"
|
||||
);
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ fn main() {
|
||||
}
|
||||
};
|
||||
eprintln!(
|
||||
"utas-host starting: listen={} python_upstream={} core_url={} tables_dir={} asset_map={:?}",
|
||||
cfg.listen_addr, cfg.python_upstream, cfg.core_url, cfg.tables_dir, cfg.asset_map_path
|
||||
"utas-host starting: listen={} python_upstream={} core_url={} tables_dir={} catalog={} identity_store={}",
|
||||
cfg.listen_addr, cfg.python_upstream, cfg.core_url, cfg.tables_dir, cfg.catalog_path, cfg.identity_store_path
|
||||
);
|
||||
let server = match Server::from_config(&cfg) {
|
||||
Ok(s) => s,
|
||||
|
||||
@@ -8,11 +8,13 @@ use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use openfut_adapter_fifa17::fut::club_response::CoreOwnedItem;
|
||||
use openfut_adapter_fifa17::fut::catalog::Fifa17CardCatalog;
|
||||
use openfut_adapter_fifa17::fut::club_response::{CoreOwnedItem, ItemIdentityResolver};
|
||||
use openfut_adapter_fifa17::fut::entities::Fifa17Entities;
|
||||
use openfut_identity::JsonIdentityStore;
|
||||
use openfut_utas_host::{
|
||||
classify, read_request, CoreAccess, CoreError, CorePage, EmptyAssetResolver, MapAssetResolver,
|
||||
PassClient, Route, Server,
|
||||
classify, read_request, CoreAccess, CoreError, CorePage, Fifa17IdentityResolver, PassClient,
|
||||
Route, Server,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::Value;
|
||||
@@ -149,17 +151,34 @@ fn spawn_mock_python() -> (String, Recorded) {
|
||||
(format!("http://{addr}"), rec)
|
||||
}
|
||||
|
||||
/// A unique temp path for a per-server identity store (integration tests run in
|
||||
/// the same process; each server gets its own store file).
|
||||
fn unique_store_path() -> std::path::PathBuf {
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, Ordering::SeqCst);
|
||||
std::env::temp_dir().join(format!("ofut-host-it-{}-{n}.json", std::process::id()))
|
||||
}
|
||||
|
||||
fn build_server(
|
||||
core: Arc<FakeCore>,
|
||||
upstream: &str,
|
||||
assets_map: Option<HashMap<String, u32>>,
|
||||
) -> Server {
|
||||
let assets: Arc<
|
||||
dyn openfut_adapter_fifa17::fut::club_response::ItemIdentityResolver + Send + Sync,
|
||||
> = match assets_map {
|
||||
Some(m) => Arc::new(MapAssetResolver::from_map(m)),
|
||||
None => Arc::new(EmptyAssetResolver),
|
||||
};
|
||||
// The map (card id → asset id) becomes a real identity catalog; an absent
|
||||
// map is an empty catalog (every item dropped — the honest no-mapping case).
|
||||
let cards = assets_map.unwrap_or_default();
|
||||
let entries: Vec<String> = cards
|
||||
.iter()
|
||||
.map(|(id, asset)| format!("\"{id}\":{{\"asset_id\":{asset}}}"))
|
||||
.collect();
|
||||
let doc = format!(
|
||||
"{{\"schema_version\":1,\"game\":\"fifa17\",\"cards\":{{{}}}}}",
|
||||
entries.join(",")
|
||||
);
|
||||
let catalog = Fifa17CardCatalog::from_json_str(&doc).unwrap();
|
||||
let store = JsonIdentityStore::open(unique_store_path()).unwrap();
|
||||
let assets: Arc<dyn ItemIdentityResolver + Send + Sync> =
|
||||
Arc::new(Fifa17IdentityResolver::new(catalog, Arc::new(store)));
|
||||
Server::new(
|
||||
core,
|
||||
Arc::new(entities()),
|
||||
|
||||
Reference in New Issue
Block a user