f6dd41fd41
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>
175 lines
5.1 KiB
Markdown
175 lines
5.1 KiB
Markdown
# 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 1–99.
|
||
- Attribute values are 1–99.
|
||
- `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.
|