# HTTP API сервера Служебный HTTP-слой матчевого блока: очередь, тикеты на подключение и `/health`. После подключения клиент переходит на WebSocket и работает по `docs/protocol.md`. Базовая часть стека: **ASP.NET Core 10**, один процесс. Матчи и очередь сохраняются в PostgreSQL/Redis, если они настроены; без внешних сервисов сервер работает in-memory (удобно для тестов). --- ## 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 ``` Контейнер (PostgreSQL + Redis + сервер): ```powershell docker compose up --build # http://localhost:8080/health ``` Порт и внешний адрес WebSocket задаются в `.env`: ``` VANGUARD_PORT=8080 VANGUARD_WS_URL=ws://localhost:8080/game ``` `VANGUARD_PORT` нужен, если 8080 занят (на тестовом стенде — 18080). Для ручной проверки колоду на 50 карт можно получить так: ```powershell dotnet run --project server\src\Vanguard.Cli -- deck ``` Каталог карт ищется в `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. Хранилище и восстановление ### 7.1. Конфигурация | Ключ | Значения | Смысл | |---|---|---| | `Storage:Provider` | `None`, `Memory`, `Postgres` | `None` — без сохранения, `Memory` — EF InMemory, `Postgres` — постоянное | | `Storage:ConnectionString` | строка Npgsql | обязательно для `Postgres` | | `Redis:Configuration` | `host:port` | пусто — тикеты и очередь в памяти | | `Redis:Prefix` | `vanguard:` | префикс ключей | В контейнере переменные задаются как `Storage__Provider`, `Storage__ConnectionString`, `Redis__Configuration`. ### 7.2. Матчи в PostgreSQL - `matches` — заголовок: игроки, колоды, seed, часы, банки, статус, `eventSeq`. - `match_intents` — журнал: `intentId`, место, команда, результат, код отказа. - На каждый интент пишется строка журнала и обновляется заголовок. - После рестарта `MatchRecoveryService` поднимает незавершённые матчи: создаёт движок с сохранённым seed и проигрывает принятые команды заново. `GameFinished`, ожидания выбора и продолжения восстанавливаются реплеем, делегаты никуда не сериализуются. - `eventSeq` сверяется: если реплей разошёлся со снапшотом, матч пропускается с ошибкой в логе. - Банк и дедлайн сохраняются раз в ~5 секунд; авария теряет не больше тика. - Завершённые матчи остаются историей и при старте в память не поднимаются. ### 7.3. Redis - Тикеты: ключ `vanguard:ticket:{token}`, TTL 60 с, изъятие через `GETDEL` (одноразовость сохраняется между инстансами). - Очередь: запись `vanguard:queue:entry:{id}` (JSON) и sorted set `vanguard:queue:{mode}` по номеру постановки. При старте очередь загружается обратно. - Состояние профилей (MMR) пока остаётся в памяти: это закроет мета-API вместе с аккаунтами. --- ## 8. Ограничения текущего этапа - PostgreSQL/Redis опциональны. По умолчанию (`Storage:Provider=None`) матчи, тикеты и очередь живут только в памяти. - Профили и MMR пока не персистентны: после рестарта рейтинг начинается с 1000. Матчи и очередь при этом восстанавливаются. - Один процесс: горизонтальное масштабирование и Orleans-хостинг ещё впереди. - `IMatchGrain` уже отделён от ASP.NET Core и WebSocket: замена на Orleans grain не затронет транспорт и правила. - Нет JWT, коллекций и наград — это следующие блоки. - Rate limit: 16 интентов в секунду с одного соединения. Живой UI в него не упирается; тестовый бот специально притормаживается.