VanguardReMaster/docs/server-api.md

224 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HTTP API сервера
Служебный HTTP-слой матчевого блока: очередь, тикеты на подключение и `/health`.
После подключения клиент переходит на WebSocket и работает по `docs/protocol.md`.
Базовая часть стека: **ASP.NET Core 10**, один процесс, состояние в памяти.
---
## 1. Запуск
```powershell
$env:PATH = "$env:LOCALAPPDATA\dotnet;$env:PATH"
dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080
```
Проверка:
```powershell
Invoke-RestMethod http://localhost:5080/health
```
Контейнер:
```powershell
docker compose up --build
# http://localhost:8080/health
```
Каталог карт ищется в `content/sets` вверх от каталога сборки. Если контент
лежит в другом месте, задайте `Content:Path` (или `Content__Path` в окружении).
---
## 2. Авторизация
Пока это **dev-заглушка**: игрок представляется строкой `playerId`.
Никаких токенов и паролей нет, поэтому сервер нельзя выставлять в интернет
как есть.
Когда появится мета-API, `playerId` будет браться из JWT, а контракты очереди
и тикетов не изменятся. Ограничения на формат: 1–64 символа, буквы, цифры,
`_`, `.`, `-`.
---
## 3. Очередь
### `POST /api/game/queue`
Поставить игрока в очередь.
```json
{
"playerId": "p1",
"mode": "classic-ranked",
"deck": ["BT01/001EN", "BT01/002EN", "..."]
}
```
- `mode`: `classic-ranked` или `classic-casual`.
- `deck`: ровно 50 `printingId`. Проверяются те же правила, что и в декбилдере:
50 карт, ≤4 по `nameId`, ≤4 Sentinel, ≤4 Heal, известные принты и хотя бы
одна карта grade 0 без триггера для стартового Авангарда.
Ответ `200`:
```json
{
"queueId": "5b5c7a4e-9d5a-4f7b-a0d8-6f1b2c3d4e5f",
"status": "queued",
"mode": "classic-ranked"
}
```
Ответ `400` при плохой колоде:
```json
{
"error": "deck.invalid",
"message": "Колода не проходит проверку.",
"violations": [
{ "code": "deck.size", "message": "В колоде 49 карт, требуется 50." }
]
}
```
Повторный запрос от того же `playerId` отменяет его предыдущую очередь
в любом режиме: один игрок — одна активная очередь.
### `GET /api/game/queue/{queueId}?playerId=p1`
Статус очереди. Клиент опрашивает, пока не получит `matched`.
```json
{
"queueId": "5b5c7a4e-9d5a-4f7b-a0d8-6f1b2c3d4e5f",
"status": "matched",
"mode": "classic-ranked",
"waitedSeconds": 7,
"window": 100,
"matchId": "9f2c...",
"seat": 0,
"ticket": "c2VjcmV0...",
"opponent": "p2",
"reason": null,
"wsUrl": "ws://localhost:5080/game"
}
```
`status`:
| Значение | Смысл |
|---|---|
| `queued` | ждёт противника, `window` — текущее окно MMR |
| `matched` | матч найден, `ticket` — одноразовый тикет |
| `cancelled` | игрок отменил очередь или встал в новую |
| `expired` | очередь истекла (10 минут) |
| `failed` | не удалось создать матч |
`wsUrl` заполняется только для `matched`. Он строится из `Game:PublicWsUrl`
или из текущего запроса. Тикет живёт 60 секунд: если клиент не успел
подключиться, он запрашивает новый через `POST /api/game/tickets` с `matchId`.
Подбор:
- две независимые очереди — ranked и casual;
- скрытый MMR, стартовое окно ±100;
- окно расширяется на 50 каждые 5 секунд, максимум 2000;
- пара выбирается по ближайшему MMR в пределах взаимных окон;
- кто встал раньше — садится за место 0.
MMR наружу не отдаётся. В ранкеде после матча он меняется по Эло (K = 32),
в casual — нет.
### `DELETE /api/game/queue/{queueId}?playerId=p1`
Отменить очередь. `204` — отменена, `409` — уже не активна.
---
## 4. Тикеты
### `POST /api/game/tickets`
Выдать новый одноразовый тикет на подключение. Нужен для переподключения
после обрыва: старый тикет уже израсходован.
```json
{ "playerId": "p1", "matchId": "9f2c..." }
```
Ответ `200`:
```json
{
"ticket": "c2VjcmV0...",
"matchId": "9f2c...",
"seat": 0,
"wsUrl": "ws://localhost:5080/game",
"expiresAt": "2026-10-21T12:01:00+00:00"
}
```
- TTL — 60 секунд, тикет одноразовый.
- Тикет привязан к `(playerId, matchId, seat)`.
- Если матч уже завершён, тикет всё равно выдаётся: клиент может подключиться
и посмотреть итоговое состояние.
- `404` — матч не найден, `403` — игрок не сидит в этом матче.
---
## 5. Health
### `GET /health`
```json
{
"status": "ok",
"matches": 3,
"activeMatches": 2,
"queued": 4,
"serverTimeMs": 1760000000000
}
```
`matches` включает завершённые матчи, которые ещё живут в памяти
(10 минут после финала — для реконнекта и просмотра результата).
---
## 6. Полный сценарий
```
POST /api/game/queue → queueId
GET /api/game/queue/{id} → matched + ticket + wsUrl
WS /game → ClientHello(ticket)
← ServerHello + ServerSnapshot
→ ClientIntent(...)
← ServerIntentResult + ServerEvents
...
обрыв
POST /api/game/tickets → новый ticket
WS /game → ClientHello(ticket, resumeFromSeq)
← ServerHello + ServerEvents (догон)
```
Формат WebSocket-кадров, интентов, событий, таймеров и кодов ошибок —
`docs/protocol.md`.
---
## 7. Ограничения текущего этапа
- Всё состояние (очереди, тикеты, матчи, профили) — в памяти процесса.
Рестарт сервера теряет матчи; клиентский kill и реконнект переживаются.
- Один процесс: горизонтальное масштабирование и Orleans-хостинг ещё впереди.
- `IMatchGrain` уже отделён от ASP.NET Core и WebSocket: замена на Orleans
grain не затронет транспорт и правила.
- Нет PostgreSQL/Redis, JWT, коллекций и наград — это следующие блоки.
- Rate limit: 16 интентов в секунду с одного соединения. Живой UI в него
не упирается; тестовый бот специально притормаживается.