352ad11bc4
Production real-profile content is loaded via an explicit path, NOT the dev-only OPENFUT_DEV_CONTENT_GAMES gate: - Config.content_packs from env OPENFUT_CONTENT_PACKS (comma-sep file paths). - CardDb::load_pack(path): merge an explicit CardDefinition[] production pack. - app::build loads dev packs then production packs. Preflight (app::build, always on): every owned_cards.card_id MUST resolve to a loaded CardDefinition. A real profile with owned players but even ONE missing definition fails LOUDLY instead of silently serving an empty /collection; an empty owned_cards table (fresh DB / tests) passes. 2 preflight integration tests (missing def fails, loaded def passes). clippy -D warnings clean; full suite 151 tests green.
94 lines
3.6 KiB
Rust
94 lines
3.6 KiB
Rust
use crate::models::card::CardDefinition;
|
|
use anyhow::{Context, Result};
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
/// In-memory card registry loaded from data/cards/*.json
|
|
pub struct CardDb {
|
|
pub cards: HashMap<String, CardDefinition>,
|
|
}
|
|
|
|
impl CardDb {
|
|
pub fn load(data_dir: &str) -> Result<Self> {
|
|
let cards_dir = Path::new(data_dir).join("cards");
|
|
let mut cards = HashMap::new();
|
|
|
|
if !cards_dir.exists() {
|
|
tracing::warn!("Card data directory not found: {:?}", cards_dir);
|
|
return Ok(Self { cards });
|
|
}
|
|
|
|
for entry in std::fs::read_dir(&cards_dir)
|
|
.with_context(|| format!("reading cards dir {:?}", cards_dir))?
|
|
{
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
|
let content = std::fs::read_to_string(&path)
|
|
.with_context(|| format!("reading {:?}", path))?;
|
|
let batch: Vec<CardDefinition> = serde_json::from_str(&content)
|
|
.with_context(|| format!("parsing {:?}", path))?;
|
|
for card in batch {
|
|
cards.insert(card.id.clone(), card);
|
|
}
|
|
}
|
|
}
|
|
|
|
tracing::info!("Loaded {} card definitions", cards.len());
|
|
Ok(Self { cards })
|
|
}
|
|
|
|
/// Merge a game's **opt-in development content pack** from
|
|
/// `{data_dir}/games/{game}/dev/cards.json` (a single `CardDefinition[]`).
|
|
/// This is NOT read by [`CardDb::load`]; it is loaded only when a game is
|
|
/// explicitly named in `Config::dev_content_games`, so default content stays
|
|
/// untouched. Returns the number of definitions merged. A missing file is an
|
|
/// error (opt-in means the pack is expected to exist).
|
|
pub fn load_game_dev(&mut self, data_dir: &str, game: &str) -> Result<usize> {
|
|
let path = Path::new(data_dir)
|
|
.join("games")
|
|
.join(game)
|
|
.join("dev")
|
|
.join("cards.json");
|
|
let content = std::fs::read_to_string(&path)
|
|
.with_context(|| format!("reading dev content pack {path:?}"))?;
|
|
let batch: Vec<CardDefinition> =
|
|
serde_json::from_str(&content).with_context(|| format!("parsing {path:?}"))?;
|
|
let n = batch.len();
|
|
for card in batch {
|
|
self.cards.insert(card.id.clone(), card);
|
|
}
|
|
tracing::info!("Loaded {} dev card definitions for game '{}'", n, game);
|
|
Ok(n)
|
|
}
|
|
|
|
/// Merge an explicit PRODUCTION content pack file (a single
|
|
/// `CardDefinition[]`). Unlike [`CardDb::load_game_dev`] this takes a direct
|
|
/// path (the real-profile import emits one) and is the production content
|
|
/// path — not gated behind dev content. Returns the number merged.
|
|
pub fn load_pack(&mut self, path: &Path) -> Result<usize> {
|
|
let content = std::fs::read_to_string(path)
|
|
.with_context(|| format!("reading content pack {path:?}"))?;
|
|
let batch: Vec<CardDefinition> =
|
|
serde_json::from_str(&content).with_context(|| format!("parsing {path:?}"))?;
|
|
let n = batch.len();
|
|
for card in batch {
|
|
self.cards.insert(card.id.clone(), card);
|
|
}
|
|
tracing::info!("Loaded {} production card definitions from {:?}", n, path);
|
|
Ok(n)
|
|
}
|
|
|
|
pub fn get(&self, id: &str) -> Option<&CardDefinition> {
|
|
self.cards.get(id)
|
|
}
|
|
|
|
pub fn all(&self) -> Vec<&CardDefinition> {
|
|
self.cards.values().collect()
|
|
}
|
|
|
|
pub fn by_min_overall(&self, min: u8) -> Vec<&CardDefinition> {
|
|
self.cards.values().filter(|c| c.overall >= min).collect()
|
|
}
|
|
}
|