4.1 KiB
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
- Collect entry fees into a per-match escrow only after both players are connected.
- Pay the winner (and house fee) only from that escrow.
- 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:
{ "red_joined_at": "2026-08-12T18:00:00.000Z" }
{ "blue_joined_at": "2026-08-12T18:00:01.000Z" }
Success when both have joined (collect ran)
{
"ok": true,
"id": 123,
"entries_collected": true,
"already_collected": false,
"escrow_user_id": 456
}
Success when only one side joined yet
{
"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
{ "winner": "red" }
or
{ "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).
-
One player disconnects mid-match
Remaining player wins.
Example: blue disconnects →{ "winner": "red" }. -
Both players disconnect
The last player who disconnected is the winner.
Example: red disconnects first, then blue → blue was last to leave →{ "winner": "blue" }. -
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. -
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_atin 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