a45155e0c51dd4cf41df9295352db110044d225a
CI / Build, lint & test (push) Successful in 3m4s
FIFA 17 training cards boost ONE attribute of ONE owned player. Core gains
the state to hold that and the vocabulary to be asked for it, without
learning any FIFA rule.
`owned_card_training` (migration 0029) keys on (owned_card_id,
attribute_index), so a second training on a slot that already carries one is
a constraint violation rather than a silent choice between stacking and
replacing. Whether FIFA 17 stacks, replaces, merges or refuses is UNKNOWN --
no shipped table describes it and the client holds no consumable-effect
logic to reverse it from -- so the schema enforces the unknown and the apply
turns it into a refusal that consumes nothing. Relaxing that later is one
line; unpicking accumulated wrong state would not be.
`InstanceEffect::ApplyTraining { attribute_index, amount, max_amount }`
names a SLOT in Core's own six-attribute model, never a FIFA attribute: that
"GK speed is slot 4" is the adapter's reversed knowledge and stays there.
The caller declares its family's authored ceiling and Core holds it to it,
which is what stops a host describing a boost no card could grant through a
vocabulary that exists to prevent exactly that.
The immutable definition is never written. `/collection` gains
`effective_attributes` (base + training, clamped to the 1..=99 domain) and
the raw effects, loaded for the whole club in one query rather than the N+1
this projection has suffered before. The legacy `training_bonus` column --
an overall-rating upgrade written only by a non-transactional route no
adapter calls -- is deliberately not reused.
Tests cover the happy path, same-slot refusal leaving the card intact,
distinct slots coexisting, over-ceiling and out-of-range refusals, loan
refusal, replay, FK cascade, and two 12-round races: apply vs quick-sell on
one card, and two concurrent applies of one card. Both prove exactly one
winner, one effect, one audit row.
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%