sync
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
# Prompt for Unity Cursor — no CC on forfeit (including skip/auto-forfeit)
|
||||
|
||||
Copy everything below the line into Unity Cursor. Do not change join/collect, rematch, hidden MMR display, or player-facing UI.
|
||||
|
||||
---
|
||||
|
||||
You are working in the Kick Kings **dedicated server** Unity project.
|
||||
|
||||
The matchmaker already awards 100 CC only to players still **connected** at settle. That is not enough: a skip-forfeit / auto-forfeit player stays connected, so they still received CC. The API now **also** withholds CC from anyone who forfeited.
|
||||
|
||||
Your only job: on `PATCH /internal/match/{matchId}/winner`, send `forfeit: true` for **every** forfeit path — leave, disconnect, last-disconnect-wins, **and skip/auto-forfeit (two skipped turns)**. Keep `red_connected` / `blue_connected` accurate. Do not infer them from `winner`. Keep the existing header `X-Dedicated-Server-Secret`. Call winner **exactly once** when the outcome is known.
|
||||
|
||||
Do not reimplement CC. The API decides who gets it from the flags you send.
|
||||
|
||||
## Endpoint (unchanged URL)
|
||||
|
||||
`PATCH /internal/match/{matchId}/winner`
|
||||
|
||||
## JSON body (already exists — fix `forfeit` on skip/auto-forfeit)
|
||||
|
||||
Always send all six fields. JSON types must be real booleans and integers (`true` not `"true"`, `3` not `"3"`).
|
||||
|
||||
```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. RC prize still goes here. 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. |
|
||||
| `blue_score` | int | Same for blue. |
|
||||
| `forfeit` | bool | `true` for leave, disconnect, last-disconnect-wins, **and skip/auto-forfeit**. `false` **only** for a normal score win. |
|
||||
|
||||
## What changed (this is the bug)
|
||||
|
||||
Previous contract: skip-forfeit sent `forfeit: false` because the skipper was still connected. The API then paid them 100 CC.
|
||||
|
||||
New contract: skip-forfeit / auto-forfeit is still not a disconnect (`*_connected` stays `true` if they are in the match), but it **is** a forfeit. Send `forfeit: true`. The API then pays CC to the connected **non-forfeiting** side only.
|
||||
|
||||
## How the API awards CC (do not reimplement)
|
||||
|
||||
Let `amount` = 100. Do **not** infer connectedness from `winner`. A last-disconnect winner can be disconnected: they get RC, not CC.
|
||||
|
||||
On any forfeit, CC is never paid to the **loser**.
|
||||
|
||||
| `red_connected` | `blue_connected` | `forfeit` | CC credit |
|
||||
|-----------------|------------------|-----------|-----------|
|
||||
| true | true | false | +amount to **both** |
|
||||
| true | true | true | +amount to the **winner only** (skip/auto-forfeit) |
|
||||
| true | false | true | +amount to **red only** |
|
||||
| false | true | true | +amount to **blue only** |
|
||||
| false | false | true | **nobody** |
|
||||
|
||||
RC prize / escrow is unchanged: still paid to `winner`.
|
||||
|
||||
## 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.
|
||||
|
||||
Skip-forfeit is **not** a disconnect. If that player is still in the match, keep their `*_connected: true` **and** send `forfeit: true`.
|
||||
|
||||
## What to send in each end condition
|
||||
|
||||
### 1. Normal score win (first to 3)
|
||||
|
||||
Both still in the match. `forfeit: false`. Both get CC.
|
||||
|
||||
```json
|
||||
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 3, "blue_score": 1, "forfeit": false }
|
||||
```
|
||||
|
||||
### 2. One player leaves or disconnects
|
||||
|
||||
Remaining player wins. Leaver `*_connected: false`. `forfeit: true`. Only the remaining connected player gets CC.
|
||||
|
||||
```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`. Nobody gets CC. RC still goes to `winner`.
|
||||
|
||||
```json
|
||||
{ "winner": "blue", "red_connected": false, "blue_connected": false, "red_score": 0, "blue_score": 2, "forfeit": true }
|
||||
```
|
||||
|
||||
### 4. Skip-forfeit / auto-forfeit (two skipped turns) — this is the change
|
||||
|
||||
Not a disconnect. Keep `*_connected: true` if they are still in the match. Send `forfeit: true`. Winner is whoever the game rules already pick. The skipper (loser) does **not** get CC. The opponent gets CC if connected.
|
||||
|
||||
```json
|
||||
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 1, "blue_score": 0, "forfeit": true }
|
||||
```
|
||||
|
||||
### 5. Process crash / forced shutdown after fees were collected
|
||||
|
||||
Same as remaining-player or last-disconnect. Send accurate `*_connected`, scores, and `forfeit: true` if it was a disconnect/forfeit path. Call winner **before** exit whenever possible.
|
||||
|
||||
Never skip the winner call after collect. Never invent a draw.
|
||||
|
||||
## Success response (use `cc_awarded_*` as today)
|
||||
|
||||
```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": true
|
||||
}
|
||||
```
|
||||
|
||||
- Keep using `economy.cc_awarded_red` / `cc_awarded_blue` as today.
|
||||
- `mmr_delta` and `forfeit` on the response are for server logs only. **Do not forward them to player clients. Do not show them in UI.**
|
||||
- Honor response `winner` / `winner_id`. On simultaneous Leave/skip, a second `forfeit: true` PATCH returns **200** `already_settled: true` 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.
|
||||
|
||||
## Where to change
|
||||
|
||||
Find the dedicated-server method that PATCHes `/internal/match/{id}/winner`.
|
||||
|
||||
Today skip-forfeit / two skipped turns likely sets `forfeit = false` because the player is still connected. Change that path so `forfeit = true`.
|
||||
|
||||
Leave, disconnect, and last-disconnect should already send `forfeit: true`. Do not regress those.
|
||||
|
||||
Payload shape is unchanged:
|
||||
|
||||
```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;
|
||||
}
|
||||
```
|
||||
|
||||
## Do not
|
||||
|
||||
- Set `forfeit: false` for skip-forfeit / auto-forfeit
|
||||
- Set `*_connected: false` for skip-forfeit just to kill CC (the player is still in the match; that would also switch hidden MMR to the 20% disconnect path)
|
||||
- Infer `red_connected` / `blue_connected` from who won
|
||||
- Change join PATCH (`red_joined_at` / `blue_joined_at`) or rematch
|
||||
- Show MMR, `mmr_delta`, or CC award flags in player UI unless they are already shown
|
||||
- Skip the winner call after fees were collected
|
||||
|
||||
## Done when
|
||||
|
||||
- [ ] Normal score wins still send `forfeit: false`
|
||||
- [ ] Leave / disconnect / last-disconnect still send `forfeit: true` and `*_connected: false` on leavers
|
||||
- [ ] Skip-forfeit / auto-forfeit sends `forfeit: true` with accurate `*_connected` (usually both `true`)
|
||||
- [ ] Skipper no longer receives CC; opponent still does if connected
|
||||
- [ ] Winner is still always called after fees were collected
|
||||
+154
-16
@@ -18,7 +18,8 @@ Header on all `/internal/*` calls: `X-Dedicated-Server-Secret: <secret from sett
|
||||
→ 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" }
|
||||
{ "winner": "red" | "blue", "red_connected": true | false, "blue_connected": true | false,
|
||||
"red_score": 0-3, "blue_score": 0-3, "forfeit": true | false }
|
||||
```
|
||||
|
||||
## Join PATCH
|
||||
@@ -75,25 +76,152 @@ Idempotent: repeating the join PATCH after a successful collect returns `entries
|
||||
|
||||
`PATCH /internal/match/:matchId/winner`
|
||||
|
||||
```json
|
||||
{ "winner": "red" }
|
||||
```
|
||||
`participant_cc` (100 CC) is **not** a join/participation prize for everyone in the match. Award it only to players who **finish the match still connected and did not forfeit**. Players who leave, disconnect, skip-forfeit (auto-forfeit), or forfeit in any other way must not receive it. The opponent who stayed and did not forfeit still gets CC if they were connected.
|
||||
|
||||
or
|
||||
RC prize / escrow payout is unchanged: still paid to the reported `winner`.
|
||||
|
||||
```json
|
||||
{ "winner": "blue" }
|
||||
{
|
||||
"winner": "red",
|
||||
"red_connected": true,
|
||||
"blue_connected": true,
|
||||
"red_score": 3,
|
||||
"blue_score": 1,
|
||||
"forfeit": false
|
||||
}
|
||||
```
|
||||
|
||||
Requires entry fees already collected. Response includes economy summary (`entry_fee_rc`, `rc_prize`, `participant_cc`).
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `winner` | `"red"` \| `"blue"` | Match winner (unchanged). RC prize goes to this side. |
|
||||
| `red_connected` | bool | Dedicated server: red still had a live connection at settle. |
|
||||
| `blue_connected` | bool | Dedicated server: blue still had a live connection at settle. |
|
||||
| `red_score` | int | Rounds/sets won by red (first-to-3). Send on every settle. |
|
||||
| `blue_score` | int | Rounds/sets won by blue. Send on every settle. |
|
||||
| `forfeit` | bool | `true` for leave, disconnect, last-disconnect-wins, **and skip/auto-forfeit** (two skipped turns). `false` only for a normal score win. |
|
||||
|
||||
Set a side to `false` when:
|
||||
|
||||
- Mirror reports that client disconnected and the reconnect grace expired (or both left)
|
||||
- That player pressed Leave (voluntary forfeit) — even if the TCP connection is still up for a moment
|
||||
|
||||
`true` means that player was still in the match when the outcome was reported (score win, opponent forfeit, or skip-forfeit while they stayed connected). Skip-forfeit still sends `forfeit: true` even if that side is `*_connected: true`.
|
||||
|
||||
Hidden MMR (admin-only, not shown to players) uses score and forfeit:
|
||||
|
||||
| Result | MMR % of the Clash-style base |
|
||||
|--------|-------------------------------|
|
||||
| `forfeit: true` **and** losing side disconnected, or both disconnected | 20% |
|
||||
| `forfeit: true` **and** both still connected (skip/auto-forfeit) | score table below |
|
||||
| Loser scored 0 (3–0) | 100% |
|
||||
| Loser scored 1 (3–1) | 90% |
|
||||
| Loser scored 2+ (3–2) | 75% |
|
||||
|
||||
Skip-forfeit (two skipped turns, both still connected) sends `forfeit: true`. Hidden MMR still uses the score table, not 20%. Old dedicated builds that omit scores and connected flags are treated as 3–0 (100%). 3–1 / 3–2 multipliers apply only once this build sends scores.
|
||||
|
||||
### Award rules
|
||||
|
||||
Let `amount` = existing `participant_cc` setting (100). Do **not** infer connectedness from `winner`. The winner can be a disconnected player (last-disconnect-wins); that player must not get CC.
|
||||
|
||||
Skip-forfeit / auto-forfeit (two skipped turns): send `forfeit: true`. The skipper is the loser and **does not** get CC, even if they are still connected. The opponent gets CC if they were connected.
|
||||
|
||||
On any forfeit (`forfeit: true` or a disconnected loser), CC is never paid to the losing side.
|
||||
|
||||
| `red_connected` | `blue_connected` | `forfeit` | CC credit |
|
||||
|-----------------|------------------|-----------|-----------|
|
||||
| true | true | false | +amount to **both** users |
|
||||
| true | true | true | +amount to the **winner only** (skip/auto-forfeit) |
|
||||
| true | false | true | +amount to **red only** |
|
||||
| false | true | true | +amount to **blue only** |
|
||||
| false | false | true | **nobody** gets participation CC |
|
||||
|
||||
### Backward compatibility
|
||||
|
||||
New dedicated servers always send both bools.
|
||||
|
||||
If `red_connected` / `blue_connected` are **omitted** (old dedicated builds):
|
||||
|
||||
- Keep awarding CC to **both** players (previous behaviour)
|
||||
|
||||
Do not treat omitted fields as `false`; that would stop CC for every match until the dedicated build is deployed.
|
||||
|
||||
### Success response
|
||||
|
||||
Requires entry fees already collected.
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `winner` | `"red"` or `"blue"` for the **stored** outcome. Honor this, not the `winner` you sent. |
|
||||
| `winner_id` | User id of that side. |
|
||||
| `already_settled` | `true` if this call did not settle (retry or second forfeit). |
|
||||
| `participant_cc` | Amount per finisher (still 100). Not “total CC paid”. |
|
||||
| `cc_awarded_red` | Red’s user received `participant_cc` on this settle. |
|
||||
| `cc_awarded_blue` | Blue’s user received `participant_cc` on this settle. |
|
||||
| `mmr_delta` | Hidden MMR the winner gained (loser lost). Do not show to players. |
|
||||
| `forfeit` | Whether the match ended by forfeit (leave, disconnect, last-disconnect, or skip/auto-forfeit). |
|
||||
|
||||
| HTTP | Meaning |
|
||||
|------|---------|
|
||||
| 200 | Settled (or already settled for the same winner) |
|
||||
| 409 | Entries not collected yet, or winner already set to the other side |
|
||||
| 200 | Settled, already settled for the same winner, **or** a second `forfeit: true` that lost the race (`already_settled: true`; use response `winner` / `winner_id`) |
|
||||
| 409 | Entries not collected yet, or winner already set to the other side on a **non-forfeit** conflict (score win vs forfeit, or two score wins that disagree) |
|
||||
| 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.
|
||||
Call winner **exactly once** when the match outcome is known. Retries with the **same** side are safe if the first call succeeded: they must not pay CC twice, and they repeat the same `cc_awarded_*` flags from the first settle.
|
||||
|
||||
Simultaneous Leave / skip from both sides may fire two `forfeit: true` PATCHes with opposite `winner` values. That is also safe: the first settle stands; the second is 200 `already_settled` with the stored winner. Apply the **response** `winner`, not the side you requested. Do not treat both players as losers.
|
||||
|
||||
### Examples
|
||||
|
||||
Both finish (score 3–1):
|
||||
|
||||
```json
|
||||
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 3, "blue_score": 1, "forfeit": false }
|
||||
```
|
||||
|
||||
→ RC to red, 100 CC to red **and** blue. Hidden MMR uses 90% (3–1).
|
||||
|
||||
Blue left / disconnected, red wins:
|
||||
|
||||
```json
|
||||
{ "winner": "red", "red_connected": true, "blue_connected": false, "red_score": 1, "blue_score": 0, "forfeit": true }
|
||||
```
|
||||
|
||||
→ RC to red, 100 CC to red only. Hidden MMR uses 20% (forfeit).
|
||||
|
||||
Both disconnected, last disconnect was blue:
|
||||
|
||||
```json
|
||||
{ "winner": "blue", "red_connected": false, "blue_connected": false, "red_score": 0, "blue_score": 0, "forfeit": true }
|
||||
```
|
||||
|
||||
→ RC to blue, **no** participation CC. Hidden MMR uses 20% (forfeit).
|
||||
|
||||
Blue skip-forfeit / auto-forfeit (two skipped turns), still connected:
|
||||
|
||||
```json
|
||||
{ "winner": "red", "red_connected": true, "blue_connected": true, "red_score": 1, "blue_score": 0, "forfeit": true }
|
||||
```
|
||||
|
||||
→ RC to red, 100 CC to **red only** (blue forfeited). Hidden MMR uses the score table (100% here, blue scored 0), not 20%.
|
||||
|
||||
## Always finish — disconnect / forfeit rules
|
||||
|
||||
@@ -102,18 +230,24 @@ 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" }`.
|
||||
Remaining player wins. Set the disconnected side to `false` and `forfeit: true`.
|
||||
Example: blue disconnects → `{ "winner": "red", "red_connected": true, "blue_connected": false, "forfeit": true }`.
|
||||
|
||||
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" }`.
|
||||
The **last player who disconnected is the winner**. Both sides are disconnected, so neither gets participation CC.
|
||||
Example: red disconnects first, then blue → blue was last to leave → `{ "winner": "blue", "red_connected": false, "blue_connected": false, "forfeit": true }`.
|
||||
|
||||
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.
|
||||
If fees were collected, choose a winner with the same rules (remaining player, or last disconnect), send accurate `*_connected` flags, and call winner **before** exit whenever possible.
|
||||
|
||||
4. **Normal score / time win**
|
||||
Report the actual winning side as today.
|
||||
Report the actual winning side, scores, `forfeit: false`, and `true` for any player still connected.
|
||||
|
||||
5. **Skip-forfeit / auto-forfeit (two skipped turns)**
|
||||
Not a disconnect. Both `*_connected` stay `true` if they are still in the match. Send `forfeit: true`. The skipper is the loser and does **not** get CC. The opponent gets CC if connected. Hidden MMR uses the score table, not 20%.
|
||||
|
||||
6. **Simultaneous forfeits (both Leave / skip at once)**
|
||||
Each handler may PATCH with the opponent as `winner` and `forfeit: true`. The API keeps the **first** settle (first forfeiter loses). The second returns **200** `already_settled: true` with the real `winner` / `winner_id` — not 409. Apply the **response** winner. Do not show both players as losers. Do not invent a draw. Score-win vs forfeit disagreement is still 409.
|
||||
|
||||
Do **not** leave escrow locked with no winner. Do **not** invent a draw path that skips the winner endpoint.
|
||||
|
||||
@@ -127,4 +261,8 @@ Do **not** leave escrow locked with no winner. Do **not** invent a draw path tha
|
||||
- [ ] 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)
|
||||
- [ ] Send `red_connected` / `blue_connected` (do not infer from `winner`; last-disconnect winner with both `false` still gets RC, not CC)
|
||||
- [ ] Send `red_score` / `blue_score` on every settle (3–1 / 3–2 MMR multipliers need these)
|
||||
- [ ] Send `forfeit: true` on leave, disconnect, last-disconnect, **and skip/auto-forfeit**
|
||||
- [ ] Persist disconnect order until winner is acknowledged
|
||||
- [ ] Dual Leave/skip: honor 200 `already_settled` `winner` even if it differs from the side you sent
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user