feat: Phase 4 — dev experience complete
CI / Build, lint & test (push) Successful in 2m21s

Docs:
- .env.example: documents all environment variables with defaults
- CONTRIBUTING.md: dev setup, running tests, code style, design constraints
- MODDING.md: full schema reference for cards, packs, objectives, and SBCs
- .gitea/workflows/ci.yml: Gitea Actions CI (fmt check, clippy, build, test)

Middleware (#50-52):
- Body limit (256 KB) via DefaultBodyLimit on all routes (#50)
- Concurrency limit (256 concurrent requests) via semaphore middleware;
  returns 429 Too Many Requests when at capacity (#51)
- Correlation IDs via tower_http request_id layers; sets x-request-id
  UUID on every request and propagates it to the response headers (#52)

Layer order (outermost → innermost):
  CorsLayer → DefaultBodyLimit → concurrency limit → SetRequestId
  → TraceLayer → PropagateRequestId → routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 15:53:28 -07:00
parent 4ae1081aa9
commit f6dd41fd41
9 changed files with 384 additions and 2 deletions
+174
View File
@@ -0,0 +1,174 @@
# OpenFUT Modding Guide
All game content is driven by JSON files in the `data/` directory. You can add
new cards, packs, objectives, and SBCs without touching any Rust code.
The server reloads data files on startup. After changing a file, restart
`openfut-core` to pick up the changes.
---
## Cards — `data/cards/*.json`
Each file contains a JSON array of `CardDefinition` objects. All files in the
directory are loaded at startup.
```json
[
{
"id": "card_unique_id",
"name": "Player Name",
"overall": 82,
"position": "ST",
"nation": "Brazil",
"league": "Brasileirao",
"club": "Flamengo",
"pace": 88,
"shooting": 85,
"passing": 74,
"dribbling": 82,
"defending": 38,
"physical": 76,
"rarity": "gold",
"image_path": null
}
]
```
**Valid rarity values:** `bronze`, `silver`, `gold`, `raregold`, `totw`, `hero`, `icon`
**Valid positions:** `GK`, `CB`, `LB`, `RB`, `CDM`, `CM`, `CAM`, `LM`, `RM`,
`LW`, `RW`, `ST`, `CF`
- `id` must be unique across all card files.
- `overall` is 199.
- Attribute values are 199.
- `image_path` can be a relative path to an image or `null`.
---
## Packs — `data/packs/*.json`
Each file contains a JSON array of `PackDefinition` objects.
```json
[
{
"id": "my_custom_pack",
"name": "Custom Pack",
"description": "12 random gold players.",
"cost_coins": 7500,
"slots": [
{
"count": 12,
"min_overall": 75,
"rarity_filter": ["gold", "raregold"],
"guaranteed_rare": false
}
]
}
]
```
Each **slot** defines one group of cards to draw:
- `count` — how many cards to draw for this slot.
- `min_overall` — minimum overall rating (inclusive), or `null` for no filter.
- `rarity_filter` — array of rarity strings to draw from, or `null` for any rarity.
- `guaranteed_rare` — if `true`, this slot's cards are marked as rare.
A pack can have multiple slots (e.g., one slot of 11 gold cards + one guaranteed
rare slot of 1 card).
---
## Objectives — `data/objectives/*.json`
Each file contains a JSON array of `ObjectiveDefinition` objects.
```json
[
{
"id": "daily_score_3",
"title": "Hat-Trick Hero",
"description": "Score 3 goals in one session.",
"objective_type": "daily",
"metric": "goalsscored",
"target": 3,
"reward_coins": 500,
"reward_xp": 200,
"reward_pack_id": null
}
]
```
**Valid `objective_type` values:** `daily`, `weekly`, `milestone`, `lifetime`, `season`
**Valid `metric` values:**
| Metric | Incremented by |
|------------------|--------------------------------------|
| `matcheswon` | Winning a match |
| `matchesplayed` | Playing any match |
| `goalsscored` | Goals scored in a match |
| `packsopened` | Opening a pack |
| `sbcscompleted` | Completing an SBC |
| `coinsearned` | Coins awarded after a match |
- `reward_pack_id` must match a pack `id` from `data/packs/` or be `null`.
- Daily objectives are auto-reset to 0 at midnight UTC.
---
## SBCs — `data/sbcs/*.json`
Each file contains a JSON array of `SbcDefinition` objects.
```json
[
{
"id": "my_sbc",
"name": "Custom SBC",
"description": "Submit 5 gold players for a reward.",
"expires_at": null,
"requirements": {
"squad_size": 5,
"min_overall": 75,
"max_overall": null,
"required_leagues": ["Premier League"],
"required_nations": [],
"required_clubs": [],
"min_players_from_same_league": null,
"min_players_from_same_nation": null,
"min_players_from_same_club": null
},
"reward": {
"coins": 1000,
"xp": 300,
"pack_id": null
}
}
]
```
**Requirements:**
- `squad_size` — exact number of cards to submit (required).
- `min_overall` — average overall must meet or exceed this (optional).
- `max_overall` — no individual card may exceed this (optional).
- `required_leagues` — at least one card from each listed league.
- `required_nations` — at least one card from each listed nation.
- `required_clubs` — at least one card from each listed club.
- `min_players_from_same_league` — max count of any single league ≥ this.
- `min_players_from_same_nation` — max count of any single nation ≥ this.
- `min_players_from_same_club` — max count of any single club ≥ this.
Cards submitted to an SBC are permanently consumed from the club's collection.
---
## Tips
- Use unique `id` values that won't collide with future official cards.
A good convention is `card_<nation>_<number>`, e.g. `card_br_042`.
- You can have multiple JSON files per directory — they are all merged at load time.
- Invalid JSON will prevent the server from starting; check the log output.
- Cards referenced by packs must exist in `data/cards/` or they will not be drawn.