20e281e0cf03e6b829b936e8da22308c91ba8e28
CI / Build, lint & test (push) Successful in 3m28s
`/squad/replace` is a full replacement: it deletes every assignment and
reinserts the supplied slots, and 9bdc163 correctly made it refuse a
replacement carrying no slots so a broken client cannot write its emptiness
back. That guard is load-bearing and is not touched here.
But FIFA 17 sends TWO operations down one wire path. Its captain/kick-taker
screen emits a body with no `players` at all -- `{id, custom, captain,
kicktakers}` -- and the host presented that to `/squad/replace` as a
replacement with zero slots. The guard did exactly its job and refused it, so
every captain/kick-taker change died with a 400 (surfaced to the client as a
502) and the user's edit was silently lost. Confirmed by bisect against the
captures: the same body returned 200 on 08-24 18:06:59 and 502 at 20:05:30,
either side of the Core deploy carrying the guard.
The operation was mis-described, so the fix is to stop mis-describing it, not
to relax the guard. `patch_squad_roles` updates only the captain flag and the
opaque extension, in one transaction, issuing no statement that can insert,
delete or reorder an assignment row -- player slots, the squad manager and club
actives are untouched by construction rather than by care.
Two details that matter:
* the captain is part of `squad_fingerprint`, so a captain move MUST
re-anchor the extension or every later read reports it stale;
* the captain is validated against THIS squad's assignments before any
write, so an invalid target leaves captain AND extension unapplied rather
than half-applying the patch.
Tests cover both halves: the captain moves without disturbing assignments and
re-anchors the fingerprint, and an unfielded captain is refused with the prior
captain and the prior extension payload both intact. The empty-replacement
guard regression test continues to pass unchanged.
OpenFUT Core
Offline Ultimate Team backend — game-independent.
OpenFUT Core is the heart of the OpenFUT project: a fully offline, single-player FUT-style backend written in Rust. It is deliberately decoupled from any specific game, though it is designed to power a FIFA 23 offline experience.
What it does
- Creates and manages local player profiles and clubs
- Manages coins, XP, and progression
- Generates packs from weighted JSON definitions
- Tracks your full card collection
- Squad builder with formations and chemistry (chemistry calculations: WIP)
- Objectives engine (daily, weekly, lifetime, milestone)
- SBC (Squad Building Challenge) engine with JSON-defined challenges
- Match result processing with coin and XP rewards
- NPC transfer market with daily refreshes
- Statistics tracking
- Fully moddable via JSON data files
Tech Stack
- Rust + Axum (HTTP framework)
- Tokio (async runtime)
- SQLite + SQLx (database + migrations)
- Serde (JSON data layer)
- tower-http (middleware: CORS, tracing)
Quick Start
# Build
cargo build --release
# Run (creates openfut.db in current directory)
./target/release/openfut-core
# Or with custom config
DATABASE_URL=sqlite://./myclub.db LISTEN_ADDR=127.0.0.1:8080 ./target/release/openfut-core
Environment Variables
| Variable | Default | Description |
|---|---|---|
LISTEN_ADDR |
127.0.0.1:8080 |
Address to listen on |
DATABASE_URL |
sqlite://openfut.db |
SQLite database path |
DATA_DIR |
data |
Path to JSON data files |
API Routes
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/auth/local |
Create first-run profile + club |
GET |
/profile |
Get active profile |
GET |
/club |
Get active club with coins |
GET |
/cards |
Browse all card definitions |
GET |
/collection |
Get owned cards |
GET |
/packs |
List unopened packs |
POST |
/packs/open/:pack_id |
Open a pack |
GET |
/squad |
Get active squad |
POST |
/squad |
Save squad |
GET |
/objectives |
List objectives with progress |
POST |
/matches/complete |
Complete a match exactly once + receive rewards |
GET |
/sbc |
List SBC definitions |
POST |
/sbc/submit |
Submit SBC solution |
GET |
/market |
Browse NPC transfer market |
POST |
/market/buy |
Buy listing |
POST |
/market/sell |
Quick-sell card |
POST |
/market/refresh |
Refresh NPC listings |
GET |
/statistics |
Get match/pack/SBC stats |
First Run
# Create your profile
curl -X POST http://localhost:8080/auth/local \
-H 'Content-Type: application/json' \
-d '{"username": "Player 1"}'
# Check your club (5000 coins + a gold pack waiting)
curl http://localhost:8080/club
# Open your starter pack
curl -X POST http://localhost:8080/packs/open/<pack_id>
# Submit a match win. `match_identity` keys exactly-once economy: resubmitting the
# same identity echoes the first result and grants nothing twice.
curl -X POST http://localhost:8080/matches/complete \
-H 'Content-Type: application/json' \
-d '{"match_identity":"match-1","result":"win","squad_id":"any","opponent_name":"Beginner AI","goals_for":3,"goals_against":0,"mode":"squad_battles"}'
Modding
All game content lives in data/. Drop JSON files into the appropriate folder and restart.
data/
cards/ ← CardDefinition[]
packs/ ← PackDefinition[]
objectives/ ← ObjectiveDefinition[]
sbcs/ ← SbcDefinition[]
events/ ← (future)
See docs/modding.md for schema reference.
Development
cargo fmt
cargo clippy -- -D warnings
cargo test
License
MIT — see LICENSE
Description
Languages
Rust
100%