VanguardReMaster/docs/server-api.md

11 KiB
Raw Blame History

HTTP API сервера

Служебный HTTP-слой матчевого блока: очередь, тикеты на подключение и /health. После подключения клиент переходит на WebSocket и работает по docs/protocol.md.

Базовая часть стека: ASP.NET Core 10, один процесс. Матчи и очередь сохраняются в PostgreSQL/Redis, если они настроены; без внешних сервисов сервер работает in-memory (удобно для тестов).


1. Запуск

$env:PATH = "$env:LOCALAPPDATA\dotnet;$env:PATH"

dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080

Проверка:

Invoke-RestMethod http://localhost:5080/health

Контейнер (PostgreSQL + Redis + сервер):

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 карт можно получить так:

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

Поставить игрока в очередь.

{
  "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:

{
  "queueId": "5b5c7a4e-9d5a-4f7b-a0d8-6f1b2c3d4e5f",
  "status": "queued",
  "mode": "classic-ranked"
}

Ответ 400 при плохой колоде:

{
  "error": "deck.invalid",
  "message": "Колода не проходит проверку.",
  "violations": [
    { "code": "deck.size", "message": "В колоде 49 карт, требуется 50." }
  ]
}

Повторный запрос от того же playerId отменяет его предыдущую очередь в любом режиме: один игрок — одна активная очередь.

GET /api/game/queue/{queueId}?playerId=p1

Статус очереди. Клиент опрашивает, пока не получит matched.

{
  "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

Выдать новый одноразовый тикет на подключение. Нужен для переподключения после обрыва: старый тикет уже израсходован.

{ "playerId": "p1", "matchId": "9f2c..." }

Ответ 200:

{
  "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

{
  "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 в него не упирается; тестовый бот специально притормаживается.