Files
funman300 1ffe0ffa9f Initial commit: OpenFUT Core
Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:54:51 -07:00

3.6 KiB

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<AppState> extractor:

pub struct AppState {
    pub pool: Pool,                         // SQLite connection pool
    pub card_db: Arc<CardDb>,              // in-memory card registry
    pub pack_defs: Arc<Vec<PackDefinition>>,
    pub obj_defs: Arc<Vec<ObjectiveDefinition>>,
    pub sbc_defs: Arc<Vec<SbcDefinition>>,
}

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_idroutes::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/resultroutes::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<String, CardDefinition>.