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
+74
View File
@@ -0,0 +1,74 @@
use crate::{
config::Config,
db::Pool,
models::{objective::ObjectiveDefinition, pack::PackDefinition, sbc::SbcDefinition},
routes,
services::{
card_db::CardDb, objective::load_objective_definitions, pack::load_pack_definitions,
sbc::load_sbc_definitions,
},
};
use anyhow::Result;
use axum::{
routing::{get, post},
Router,
};
use std::sync::Arc;
use tower_http::{cors::CorsLayer, trace::TraceLayer};
#[derive(Clone)]
pub struct AppState {
pub pool: Pool,
pub card_db: Arc<CardDb>,
pub pack_defs: Arc<Vec<PackDefinition>>,
pub obj_defs: Arc<Vec<ObjectiveDefinition>>,
pub sbc_defs: Arc<Vec<SbcDefinition>>,
}
pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
let card_db = Arc::new(CardDb::load(&cfg.data_dir)?);
let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?);
let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?);
let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?);
tracing::info!(
"Loaded {} packs, {} objectives, {} SBCs",
pack_defs.len(),
obj_defs.len(),
sbc_defs.len()
);
let state = AppState {
pool,
card_db,
pack_defs,
obj_defs,
sbc_defs,
};
let router = Router::new()
.route("/health", get(routes::health::get_health))
.route("/auth/local", post(routes::auth::post_auth_local))
.route("/profile", get(routes::profile::get_profile))
.route("/club", get(routes::club::get_club))
.route("/cards", get(routes::cards::get_cards))
.route("/collection", get(routes::cards::get_collection))
.route("/packs", get(routes::packs::get_packs))
.route("/packs/open/:pack_id", post(routes::packs::post_open_pack))
.route("/squad", get(routes::squad::get_squad))
.route("/squad", post(routes::squad::post_squad))
.route("/objectives", get(routes::objectives::get_objectives))
.route("/matches/result", post(routes::matches::post_match_result))
.route("/sbc", get(routes::sbc::get_sbcs))
.route("/sbc/submit", post(routes::sbc::post_sbc_submit))
.route("/market", get(routes::market::get_market))
.route("/market/buy", post(routes::market::post_market_buy))
.route("/market/sell", post(routes::market::post_market_sell))
.route("/market/refresh", post(routes::market::post_market_refresh))
.route("/statistics", get(routes::statistics::get_statistics))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.with_state(state);
Ok(router)
}