funman300 e8be289660
CI / Build, lint & test (push) Successful in 3m16s
feat(consume): durable per-instance contract state + HTTP apply route
`consume_item` was a complete, tested, atomic apply transaction with zero
production callers and no route -- it could not be reached over HTTP because
its effect is an in-process `ItemMutation` trait object and the host is a
separate process on a synchronous JSON boundary.

Closes that gap with a CLOSED, Core-validated effect vocabulary rather than a
pass-through: `InstanceEffect::AddContractMatches { amount, cap,
default_when_unset }`. A generic "apply this field/value" escape hatch would
hand economic authority back to the caller and break the architecture.

The read-modify-write runs INSIDE the caller's transaction
(`min(cap, COALESCE(contract_matches, default) + amount)`) so two concurrent
applies cannot lose an update, and the reported `granted` stays the requested
amount even when the cap clamps the total.

Migration 0028 adds `owned_cards.contract_matches` NULLABLE: NULL means "Core
tracks no contract here", which keeps the pack-fresh default (a FIFA-specific
7) out of Core and leaves every existing row unchanged in meaning. ADD COLUMN,
not a rebuild -- a rebuild would drop 0026's transfer trigger.

Two ordering fixes forced by putting this on the live path:
* consume_item moves from DEFERRED `pool.begin()` to `BEGIN IMMEDIATE`, the
  discipline economy.rs documents: three reads precede the first write, which
  is exactly the shape that returns SQLITE_BUSY past the busy handler.
* the replay answer now precedes source validation. With DestroyInstance the
  first apply deletes the source, so the old order answered a retry with 404
  instead of the recorded outcome -- replay semantics were unreachable.
2026-08-22 18:23:05 +00:00
2026-06-25 14:54:51 -07:00
2026-06-25 14:54:51 -07:00

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

S
Description
Offline Ultimate Team backend — game-independent core
Readme 635 MiB
Languages
Rust 100%