166 lines
6.9 KiB
Markdown
166 lines
6.9 KiB
Markdown
# Prompt for Unity Cursor — hidden MMR winner report
|
||
|
||
Copy everything below the line into Unity Cursor. Do not change join/collect, rematch, or client UI.
|
||
|
||
---
|
||
|
||
You are working in the Kick Kings **dedicated server** Unity project.
|
||
|
||
The matchmaker API now computes **hidden MMR** on settle. Players must **never** see MMR. Do not add MMR to HUD, post-match screens, profile, or any client-bound packet.
|
||
|
||
Your only job: extend the existing `PATCH /internal/match/{matchId}/winner` JSON body so every settle includes scores and a forfeit flag. Keep the existing header `X-Dedicated-Server-Secret`. Call winner **exactly once** when the outcome is known (retries with the same `winner` are safe).
|
||
|
||
## Endpoint (unchanged URL)
|
||
|
||
`PATCH /internal/match/{matchId}/winner`
|
||
|
||
## New required JSON body
|
||
|
||
Always send all six fields. `winner`, `red_connected`, and `blue_connected` already exist — do not remove them.
|
||
|
||
```json
|
||
{
|
||
"winner": "red",
|
||
"red_connected": true,
|
||
"blue_connected": true,
|
||
"red_score": 3,
|
||
"blue_score": 1,
|
||
"forfeit": false
|
||
}
|
||
```
|
||
|
||
| Field | Type | Rules |
|
||
|-------|------|--------|
|
||
| `winner` | `"red"` or `"blue"` | Winning side. Unchanged. |
|
||
| `red_connected` | bool | Red still had a live connection at settle. Do **not** infer from `winner`. |
|
||
| `blue_connected` | bool | Blue still had a live connection at settle. |
|
||
| `red_score` | int | Rounds/sets red has won (first-to-3). Send on **every** settle, including forfeits. Use the score at the moment the match ended (0–3 typical). |
|
||
| `blue_score` | int | Same for blue. |
|
||
| `forfeit` | bool | `true` for leave, disconnect, last-disconnect-wins, **and skip/auto-forfeit** (two skipped turns). `false` only for a normal score win. |
|
||
|
||
JSON types must be real booleans and integers, not strings (`true` not `"true"`, `3` not `"3"`).
|
||
|
||
## How the API uses this (do not reimplement)
|
||
|
||
The API applies hidden MMR. You only report facts.
|
||
|
||
| What you send | MMR % the API uses |
|
||
|---------------|--------------------|
|
||
| `forfeit: true` **and** loser `*_connected: false`, or both disconnected | 20% |
|
||
| `forfeit: true` **and** both still connected (skip/auto-forfeit) | score table (100% / 90% / 75%) |
|
||
| Normal finish, loser score 0 (3–0) | 100% |
|
||
| Normal finish, loser score 1 (3–1) | 90% |
|
||
| Normal finish, loser score 2+ (3–2) | 75% |
|
||
|
||
Skip-forfeit: both still connected → `forfeit: true` and send the actual score. API uses the score table, not 20%. The skipper (loser) does **not** get CC.
|
||
|
||
If you omit scores, the API treats the match as 3–0 (100%). That is wrong for 3–1 and 3–2. **Always send scores.**
|
||
|
||
## Connected flags (unchanged meaning)
|
||
|
||
Set a side to `false` when:
|
||
|
||
- Mirror reports disconnect and reconnect grace expired
|
||
- That player pressed Leave (even if TCP is still up for a moment)
|
||
|
||
`true` = still in the match when you report the outcome (score win, opponent forfeit, or skip-forfeit while they stayed).
|
||
|
||
CC (100) is awarded only to connected finishers who did **not** forfeit. Last-disconnect winner can be disconnected: they get RC, not CC. Skip/auto-forfeit: skipper gets no CC even if still connected. Do not infer `*_connected` from `winner`.
|
||
|
||
## What to send in each end condition
|
||
|
||
### 1. Normal score win (first to 3)
|
||
|
||
Both still in the match.
|
||
|
||
```json
|
||
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 3, "blue_score": 1, "forfeit": false }
|
||
```
|
||
|
||
Use the real score: 3–0, 3–1, or 3–2. `forfeit` is **false**.
|
||
|
||
### 2. One player leaves or disconnects
|
||
|
||
Remaining player wins. Disconnected side `false`. `forfeit: true`. Scores = rounds already won when they left.
|
||
|
||
```json
|
||
{ "winner": "red", "red_connected": true, "blue_connected": false, "red_score": 1, "blue_score": 0, "forfeit": true }
|
||
```
|
||
|
||
### 3. Both disconnect (last-disconnect-wins)
|
||
|
||
Last player who disconnected is `winner`. Both `*_connected: false`. `forfeit: true`. Keep disconnect-order tracking until the winner call is acknowledged.
|
||
|
||
```json
|
||
{ "winner": "blue", "red_connected": false, "blue_connected": false, "red_score": 0, "blue_score": 2, "forfeit": true }
|
||
```
|
||
|
||
### 4. Skip-forfeit (two skipped turns)
|
||
|
||
Not a disconnect. If that player is still connected, keep `*_connected: true`. Send `forfeit: true`. They do **not** get CC. Send the actual score. Winner is whoever the game rules already pick. Hidden MMR uses the score table.
|
||
|
||
### 5. Process crash / forced shutdown after fees were collected
|
||
|
||
Same as remaining-player or last-disconnect. Send accurate `*_connected`, scores, `forfeit: true` if it was a disconnect path, and call winner **before** exit whenever possible.
|
||
|
||
Never skip the winner call after collect. Never invent a draw.
|
||
|
||
## Success response (ignore MMR on clients)
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"id": 123,
|
||
"winner": "red",
|
||
"winner_id": 456,
|
||
"already_settled": false,
|
||
"economy": { "entry_fee_rc": 21, "rc_prize": 34, "participant_cc": 100, "cc_awarded_red": true, "cc_awarded_blue": false },
|
||
"mmr_delta": 27,
|
||
"forfeit": false
|
||
}
|
||
```
|
||
|
||
- Keep using `economy` / `cc_awarded_*` as today.
|
||
- `mmr_delta` and `forfeit` are for server logs only. **Do not forward them to player clients. Do not show them in UI.**
|
||
- Honor response `winner` / `winner_id`. Simultaneous Leave/skip: the second `forfeit: true` PATCH is **200** `already_settled` with the first settle’s winner, not 409.
|
||
|
||
HTTP: 200 = settled, already settled same winner, or a second forfeit that lost the race. 409 = entries not collected, or winner already set to the other side on a **non-forfeit** conflict.
|
||
|
||
## Suggested C# payload
|
||
|
||
Extend the existing winner DTO / anonymous object. Do not stringify bools/ints.
|
||
|
||
```csharp
|
||
public sealed class WinnerReportBody
|
||
{
|
||
public string winner; // "red" | "blue"
|
||
public bool red_connected;
|
||
public bool blue_connected;
|
||
public int red_score;
|
||
public int blue_score;
|
||
public bool forfeit;
|
||
}
|
||
```
|
||
|
||
Serialize with standard JSON (`true`/`false`, numeric scores). Field names must match exactly (snake_case as above).
|
||
|
||
Find the dedicated-server method that currently PATCHes `/internal/match/{id}/winner` and add `red_score`, `blue_score`, `forfeit` there. Pass the live round scoreboard and whether this settle is a leave, disconnect, or skip/auto-forfeit.
|
||
|
||
## Do not
|
||
|
||
- Show MMR, `mmr_delta`, or rating of any kind to players
|
||
- Change join PATCH (`red_joined_at` / `blue_joined_at`) or rematch
|
||
- Omit scores on forfeit (still send the score at leave)
|
||
- Set `forfeit: false` for skip-forfeit / auto-forfeit (must be `true` so the skipper gets no CC)
|
||
- Infer `red_connected` / `blue_connected` from who won
|
||
- Leave a collected match without a winner call
|
||
|
||
## Done when
|
||
|
||
- [ ] Winner PATCH body always includes `red_score`, `blue_score`, `forfeit`
|
||
- [ ] Score wins send `forfeit: false` and the real 3–0 / 3–1 / 3–2
|
||
- [ ] Leave / disconnect / last-disconnect send `forfeit: true` and `*_connected: false` on leavers
|
||
- [ ] Skip-forfeit / auto-forfeit sends `forfeit: true` (skipper gets no CC even if still connected)
|
||
- [ ] No client UI or gameplay packet exposes MMR
|
||
- [ ] Winner is still always called after fees were collected
|