131 lines
4.1 KiB
Markdown
131 lines
4.1 KiB
Markdown
# Unity dedicated server — match lifecycle (escrow + always finish)
|
|
|
|
Contract for the Kick Kings dedicated server build against the matchmaker API.
|
|
Header on all `/internal/*` calls: `X-Dedicated-Server-Secret: <secret from settings>`.
|
|
|
|
## Goals
|
|
|
|
1. Collect entry fees into a **per-match escrow** only after **both** players are connected.
|
|
2. Pay the winner (and house fee) **only from that escrow**.
|
|
3. **Never leave a match unfinished** — always report a winner, including disconnect forfeits.
|
|
|
|
## Sequence
|
|
|
|
```
|
|
1. Match row exists (matchmaking or rematch) with match_id, user_red, user_blue, entry_fee
|
|
2. Red connects → PATCH /internal/match/:matchId { "red_joined_at": "<ISO-8601 UTC>" }
|
|
3. Blue connects → PATCH /internal/match/:matchId { "blue_joined_at": "<ISO-8601 UTC>" }
|
|
→ When both join timestamps are set, the API collects entry fees into escrow
|
|
4. Start the match only after collect succeeds (see responses below)
|
|
5. Match ends (score / disconnect / crash path) → PATCH /internal/match/:matchId/winner
|
|
{ "winner": "red" | "blue" }
|
|
```
|
|
|
|
## Join PATCH
|
|
|
|
`PATCH /internal/match/:matchId`
|
|
|
|
Example bodies:
|
|
|
|
```json
|
|
{ "red_joined_at": "2026-08-12T18:00:00.000Z" }
|
|
```
|
|
|
|
```json
|
|
{ "blue_joined_at": "2026-08-12T18:00:01.000Z" }
|
|
```
|
|
|
|
### Success when both have joined (collect ran)
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"id": 123,
|
|
"entries_collected": true,
|
|
"already_collected": false,
|
|
"escrow_user_id": 456
|
|
}
|
|
```
|
|
|
|
### Success when only one side joined yet
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"id": 123,
|
|
"entries_collected": false
|
|
}
|
|
```
|
|
|
|
### Collect failure (fatal — do not start the match)
|
|
|
|
Typical cases:
|
|
|
|
| HTTP | Meaning |
|
|
|------|---------|
|
|
| 402 | One or both players lack enough RC for `entry_fee` |
|
|
| 409 | Players/joins incomplete (should not happen if you set both joins) |
|
|
| 503 | DB / economy RPCs not installed on the server |
|
|
|
|
On any non-2xx after the second join, **abort kickoff**, disconnect both clients, and do **not** call winner (fees were not escrowed).
|
|
|
|
Idempotent: repeating the join PATCH after a successful collect returns `entries_collected: true` (or `already_collected: true`).
|
|
|
|
## Winner PATCH
|
|
|
|
`PATCH /internal/match/:matchId/winner`
|
|
|
|
```json
|
|
{ "winner": "red" }
|
|
```
|
|
|
|
or
|
|
|
|
```json
|
|
{ "winner": "blue" }
|
|
```
|
|
|
|
Requires entry fees already collected. Response includes economy summary (`entry_fee_rc`, `rc_prize`, `participant_cc`).
|
|
|
|
| HTTP | Meaning |
|
|
|------|---------|
|
|
| 200 | Settled (or already settled for the same winner) |
|
|
| 409 | Entries not collected yet, or winner already set to the other side |
|
|
| 500 | Escrow inconsistency (should be rare; escalate) |
|
|
|
|
Call winner **exactly once** when the match outcome is known. Retries with the **same** side are safe if the first call succeeded.
|
|
|
|
## Always finish — disconnect / forfeit rules
|
|
|
|
Matches must **never** end without a winner call after fees were collected.
|
|
|
|
Track disconnect order on the dedicated server (timestamps or a queue).
|
|
|
|
1. **One player disconnects mid-match**
|
|
Remaining player wins.
|
|
Example: blue disconnects → `{ "winner": "red" }`.
|
|
|
|
2. **Both players disconnect**
|
|
The **last player who disconnected is the winner**.
|
|
Example: red disconnects first, then blue → blue was last to leave → `{ "winner": "blue" }`.
|
|
|
|
3. **Process crash / forced shutdown**
|
|
If fees were collected, choose a winner with the same rules (remaining player, or last disconnect) and call winner **before** exit whenever possible.
|
|
|
|
4. **Normal score / time win**
|
|
Report the actual winning side as today.
|
|
|
|
Do **not** leave escrow locked with no winner. Do **not** invent a draw path that skips the winner endpoint.
|
|
|
|
## Rematch
|
|
|
|
`POST /internal/rematch` still only creates the room/match row. Entry fees are **not** taken there. Collect still happens when both `*_joined_at` are set on the new `match_id`.
|
|
|
|
## Checklist for the Unity build
|
|
|
|
- [ ] Set `red_joined_at` / `blue_joined_at` in UTC ISO when each client is fully connected
|
|
- [ ] Treat 402/4xx/5xx on the second join as fatal — no kickoff
|
|
- [ ] Gate gameplay start on `entries_collected: true`
|
|
- [ ] Always call winner after collect (score, forfeit, or last-disconnect-wins)
|
|
- [ ] Persist disconnect order until winner is acknowledged
|