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>
This commit is contained in:
funman300
2026-06-25 14:52:06 -07:00
commit 1ffe0ffa9f
60 changed files with 6152 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
# 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:
```rust
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_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<String, CardDefinition>`.