# OpenFUT Core — Architecture ## Overview ``` HTTP Client (Bridge or direct) │ ▼ Axum Router │ ┌────┴─────┐ │ Routes │ ← thin handlers: extract state, call service, return JSON └────┬─────┘ │ ┌────┴──────┐ │ Services │ ← business logic, DB calls, data loading └────┬──────┘ │ ┌────┴──────┐ │ SQLite │ ← SQLx + migrations └───────────┘ │ ┌────┴──────┐ │ Data/ │ ← JSON files: cards, packs, objectives, SBCs └───────────┘ ``` ## Module Map | Path | Purpose | |---|---| | `src/main.rs` | Entry point: tracing, config, pool, migrations, seed, serve | | `src/lib.rs` | Library root: re-exports modules, exposes `build_app` for tests | | `src/app.rs` | Router construction, `AppState` definition | | `src/config.rs` | `Config` struct, loaded from env vars | | `src/db.rs` | Pool initialization and migration runner | | `src/error.rs` | `AppError` enum + `IntoResponse` impl | | `src/models/` | Pure data types (Serde + SQLx `FromRow`) | | `src/services/` | Business logic; all DB access lives here | | `src/routes/` | Axum handler functions; one file per domain | | `src/seed/` | First-run starter pack grant | | `src/modding/` | Generic JSON directory loader | | `data/` | Moddable JSON content: cards, packs, objectives, SBCs | | `migrations/` | SQLx SQL migrations | ## AppState `AppState` is cloned into every request handler via Axum's `State` extractor: ```rust pub struct AppState { pub pool: Pool, // SQLite connection pool pub card_db: Arc, // in-memory card registry pub pack_defs: Arc>, pub obj_defs: Arc>, pub sbc_defs: Arc>, } ``` All game-content data is loaded at startup from `data/` into `Arc`-wrapped collections. This avoids repeated disk I/O per request and keeps the data shared across the multi-threaded Tokio runtime without locking. ## Data Flow: Pack Open 1. `POST /packs/open/:pack_id` → `routes::packs::post_open_pack` 2. Fetch profile + club from DB 3. Call `services::pack::open_pack(pool, card_db, pack_defs, club_id, pack_id)` 4. Validate pack exists + not opened 5. For each slot in the pack definition, randomly select cards (synchronously — no rng held across await) 6. Insert `owned_cards` rows for each card 7. Mark pack as opened 8. Increment pack stats + objective progress 9. Return `PackOpenResult { pack_id, cards }` ## Data Flow: Match Result 1. `POST /matches/result` → `routes::matches::post_match_result` 2. Fetch profile + club 3. `services::match_service::process_match(...)` 4. Determine outcome (win/draw/loss), compute coins + XP 5. Insert match record 6. `club::add_coins`, `profile::add_xp` 7. `statistics::record_match` 8. `objective::increment_metric` for matches_played, matches_won, goals_scored, coins_earned 9. Return `MatchRewardResult` ## Single-Profile Design OpenFUT is single-player. Only one profile is allowed per database. All services fetch "the active profile" by selecting the first row. This is intentional and keeps the system simple. ## Modding All game content is data-driven. To add new cards: 1. Create a JSON file in `data/cards/` 2. The file must be an array of `CardDefinition` 3. Restart the server The `CardDb` struct loads all JSON files at startup and holds them in a `HashMap`.