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:
funman300
2026-06-25 14:42:16 -07:00
commit 6ef4c40e29
79 changed files with 9158 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
use crate::{
db::Pool,
error::AppResult,
models::objective::{ObjectiveDefinition, ObjectiveProgress, ObjectiveWithProgress},
};
use anyhow::Context;
use std::path::Path;
use uuid::Uuid;
pub fn load_objective_definitions(data_dir: &str) -> anyhow::Result<Vec<ObjectiveDefinition>> {
let dir = Path::new(data_dir).join("objectives");
let mut defs = Vec::new();
if !dir.exists() {
return Ok(defs);
}
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false) {
let content =
std::fs::read_to_string(&path).with_context(|| format!("reading {:?}", path))?;
let batch: Vec<ObjectiveDefinition> =
serde_json::from_str(&content).with_context(|| format!("parsing {:?}", path))?;
defs.extend(batch);
}
}
Ok(defs)
}
pub async fn get_objectives_with_progress(
pool: &Pool,
profile_id: &str,
defs: &[ObjectiveDefinition],
) -> AppResult<Vec<ObjectiveWithProgress>> {
let progress_rows = sqlx::query_as::<_, ObjectiveProgress>(
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ?"
)
.bind(profile_id)
.fetch_all(pool)
.await?;
let result = defs
.iter()
.map(|def| {
let prog = progress_rows.iter().find(|p| p.objective_id == def.id);
ObjectiveWithProgress {
definition: def.clone(),
current: prog.map(|p| p.current).unwrap_or(0),
completed: prog.map(|p| p.completed).unwrap_or(false),
claimed: prog.map(|p| p.claimed).unwrap_or(false),
}
})
.collect();
Ok(result)
}
/// Increment a metric for all objectives that track it.
pub async fn increment_metric(
pool: &Pool,
profile_id: &str,
defs: &[ObjectiveDefinition],
metric: &str,
amount: i64,
) -> AppResult<Vec<String>> {
let mut completed_ids = Vec::new();
for def in defs
.iter()
.filter(|d| format!("{:?}", d.metric).to_lowercase() == metric)
{
let existing = sqlx::query_as::<_, ObjectiveProgress>(
"SELECT id, profile_id, objective_id, current, completed, claimed, updated_at FROM objective_progress WHERE profile_id = ? AND objective_id = ?"
)
.bind(profile_id)
.bind(&def.id)
.fetch_optional(pool)
.await?;
let now = chrono::Utc::now().to_rfc3339();
if let Some(prog) = existing {
if prog.completed {
continue;
}
let new_val = (prog.current + amount).min(def.target);
let now_complete = new_val >= def.target;
sqlx::query(
"UPDATE objective_progress SET current = ?, completed = ?, updated_at = ? WHERE id = ?"
)
.bind(new_val)
.bind(now_complete)
.bind(&now)
.bind(&prog.id)
.execute(pool)
.await?;
if now_complete {
completed_ids.push(def.id.clone());
}
} else {
let new_val = amount.min(def.target);
let now_complete = new_val >= def.target;
let id = Uuid::new_v4().to_string();
sqlx::query(
"INSERT INTO objective_progress (id, profile_id, objective_id, current, completed, claimed, updated_at) VALUES (?, ?, ?, ?, ?, 0, ?)"
)
.bind(&id)
.bind(profile_id)
.bind(&def.id)
.bind(new_val)
.bind(now_complete)
.bind(&now)
.execute(pool)
.await?;
if now_complete {
completed_ids.push(def.id.clone());
}
}
}
Ok(completed_ids)
}