Initial commit: OpenFUT Core + Bridge scaffold
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>
This commit is contained in:
@@ -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>`.
|
||||
Reference in New Issue
Block a user