6ef4c40e29
Two-repo monorepo for an offline FUT backend inspired by SPT. openfut-core: game-independent REST API backend (Axum, SQLite, SQLx) - 19 API endpoints: auth, profiles, clubs, cards, packs, squads, objectives, SBCs, match rewards, NPC market, statistics - JSON-driven card/pack/objective/SBC data (fully moddable) - SQLx migrations, weighted pack generator, SBC validation engine - 5 integration tests passing openfut-bridge: FIFA 23 traffic proxy + reverse-engineering scaffold - Catch-all HTTP proxy with request capture to captures/ - Known-route mapper (FUT paths → Core API calls) - Placeholder responses for unknown endpoints - Admin endpoints: captures, unknown endpoint list - 4 unit tests passing cargo fmt ✓ cargo clippy -D warnings ✓ cargo test ✓ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.6 KiB
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
POST /packs/open/:pack_id→routes::packs::post_open_pack- Fetch profile + club from DB
- Call
services::pack::open_pack(pool, card_db, pack_defs, club_id, pack_id) - Validate pack exists + not opened
- For each slot in the pack definition, randomly select cards (synchronously — no rng held across await)
- Insert
owned_cardsrows for each card - Mark pack as opened
- Increment pack stats + objective progress
- Return
PackOpenResult { pack_id, cards }
Data Flow: Match Result
POST /matches/result→routes::matches::post_match_result- Fetch profile + club
services::match_service::process_match(...)- Determine outcome (win/draw/loss), compute coins + XP
- Insert match record
club::add_coins,profile::add_xpstatistics::record_matchobjective::increment_metricfor matches_played, matches_won, goals_scored, coins_earned- 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:
- Create a JSON file in
data/cards/ - The file must be an array of
CardDefinition - Restart the server
The CardDb struct loads all JSON files at startup and holds them in a HashMap<String, CardDefinition>.