# Передача сессии Состояние проекта на **21 октября 2026**. Документ нужен, чтобы новая сессия продолжила работу без повторного разбора истории. Краткая оперативная сводка — `docs/SESSION-STATE.md`; этот файл хранит подробную историю. --- ## 1. Проект Фан-ремастер **Cardfight!! Vanguard** базовой эры (2011), формат **Classic**. MVP: бустер **BT01** + триал-деки **TD01/TD02** — 94 карты, 124 принта. Стек: - сервер — **.NET 10**, **Orleans** (опционально), PostgreSQL, Redis; - клиент — **Unity 6000.6.6f1**, URP, 3D-сцены (каркас готов); - лаунчер — **WPF** (ещё не начат); - контент — YAML + C#-скрипты карт; - транспорт — **WebSocket + MessagePack** (реализован в сервере и клиенте). Репозиторий: - ветка `master`; - remote `ssh://git@192.168.31.158:2222/mpakobecex/VanguardReMaster.git`; - актуальный HEAD смотреть через `git log -1` (на момент записи — `6ed0998`); - рабочее дерево чистое. Главные принципы: - сервер авторитарный, клиент тонкий; - скрытая информация (колода, рука противника, закрытый урон, seed) не уходит клиенту; - `Vanguard.Rules` — чистая библиотека без сети, БД и Orleans: команда на вход, события на выход; - каждый блок работы коммитится и пушится в `master`. --- ## 2. Текущее состояние | Проверка | Результат | |---|---| | Тесты сервера | **175/175** (131 правила/контракты + 44 сервер) | | `validate` | 0 ошибок, 0 предупреждений | | `translations check` | 94/94 | | `stress 1000` | 1000/1000 партий, 0 ошибок | | Карты | **94/94** (66 с текстом + 28 ванильных) | | Протокол и контракты | `docs/protocol.md`, `Vanguard.Shared` готовы | | Сервер | сеть, матчмейкинг, PostgreSQL + Redis, Orleans, Docker | | Мета-API | JWT, коллекция, колоды, магазин, экономика, история, реплеи | | Клиент | Unity 6.6, 3D-сцены Login/Lobby/Collection/DeckBuilder/Battle, логин/коллекция/колоды/очередь/gray-box бой | | Лаунчер | не начат | Блок «Игровое ядро» из `IMPLEMENTATION_CHECKLIST.md` закрыт по его критериям: боты доигрывают 1000 партий, инварианты держатся, реплей воспроизводится бит-в-бит по хэшу событий. --- ## 3. Как проверить `dotnet` установлен в `%LOCALAPPDATA%\dotnet`, поэтому в PowerShell: ```powershell $env:PATH = "$env:LOCALAPPDATA\dotnet;$env:PATH" dotnet test server\Vanguard.slnx dotnet run --project server\src\Vanguard.Cli -- validate dotnet run --project server\src\Vanguard.Cli -- translations check dotnet run --project server\src\Vanguard.Cli -- stress 1000 ``` Ожидаемо: ``` Пройден! : не пройдено 0, пройдено 131 (Vanguard.Rules.Tests) Пройден! : не пройдено 0, пройдено 44 (Vanguard.Server.Tests) OK: ошибок 0, предупреждений 0 Переведено: 94/94 Партий: 1000, завершено: 1000, ошибок: 0 ``` Клиент (Unity): ```powershell & 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' ` -batchmode -nographics -projectPath client ` -runTests -testPlatform EditMode -testResults client\TestResults.xml & 'C:\Program Files\Unity\Hub\Editor\6000.6.6f1\Editor\Unity.exe' ` -batchmode -nographics -projectPath client ` -executeMethod Vanguard.Editor.SceneSmoke.Run ``` Сервер локально: ```powershell dotnet run --project server\src\Vanguard.Server --urls http://localhost:5080 # GET http://localhost:5080/health ``` Тестовый стенд на `ssh home` (Linux, Docker): ```bash ssh home cd ~/VanguardReMaster # .env: VANGUARD_PORT=18080, VANGUARD_WS_URL=ws://192.168.31.158:18080/game docker compose up -d --build curl http://127.0.0.1:18080/health # остановить: docker compose down ``` `Storage:Provider=None` по умолчанию: локальный запуск без базы. Стенд использует `Postgres`, `Redis` и восстанавливает матчи после рестарта. Solution — **`server/Vanguard.slnx`**, не `.sln`. --- ## 4. Что сделано ### 4.1. Контент - 94 карты, 124 принта в `content/sets/BT01.yaml`, `TD01.yaml`, `TD02.yaml`. - Импортёр `tools/import_cards.py`. - Валидатор контента: щиты, Sentinel, параллельные арты, лимиты колоды. - Токенизация текстов, `content/keywords.yaml`. - Русские переводы `content/translations/ru.yaml` — 94/94. - Канон перевода `docs/translation-rules.md` v0.8. ### 4.2. Игровое ядро `server/src/Vanguard.Rules/Engine/GameEngine.cs`. Реализовано: - фазы `Mulligan → Stand → Draw → Ride → Main → Battle → End`; - стартовый Авангард, маллиган, добор; - райд, призыв, перемещение в пределах колонки (занятый круг меняется местами); - атака по Авангарду и переднему ряду Арьергарда; - поддержка, перехват, идеальная защита (Sentinel); - drive check и damage check с выбором целей; - триггеры: Critical, Stand, Draw, Heal; - победа/поражение, детерминированный RNG; - стоимости: Counter Blast, Soul Blast, сброс, поворот себя, уничтожение себя; - поиск в колоде без просмотра, поиск с призывом и с райдом; - просмотр верхней карты; - способности: `Triggered`, `Continuous`, `Activated`; - модификаторы силы/крита с источником и сроком жизни (`StatModifier`); - выдача и снятие способностей (`GrantedAbility`); - запреты (`CardRestriction`): не вставать, не атаковать, не поддерживать, не перехватывать, не быть выбранным; - снятие Сдерживания эффектом; - возврат карты с поля в колоду после боя и в конце хода; - способности из руки, сброса и круга защиты; - `AskUnitMultiChoice` для «до N» / «ровно N»; - `LegalCommands()` — все легальные команды текущего момента; - `GameInvariants.Check()` — проверка 50 карт, уникальных `instanceId`, урона ≤ 6. Триггеры (`AbilityTrigger`): `OnRideOver`, `OnPlace`, `OnBoost`, `OnBoosted`, `OnBoostedHit`, `OnDriveCheck`, `OnGuard`, `OnRetireFromRC`, `OnOpponentRetire`, `OnLeaveGuardian`, `OnPhaseStart`, `OnAttackDeclared`, `OnAttackHit`, `OnDamageTaken` (объявлен, но пока не вызывается — карт с ним нет). ### 4.3. Карты - **59** скриптов в `CardScripts.cs`, `CardScriptsBT01.cs`, `CardScriptsTD.cs`. - **6** карт Sentinel/Heal обрабатываются данными и движком без скрипта. - **Tejas** реализован флагом `snipeBackRow` в YAML. - **28** ванильных карт скрипта не требуют. - Итого **66/66** карт с текстом закрыты. ### 4.4. Реплей `server/src/Vanguard.Rules/Replay/`: - `CommandCodec` — команда в стабильную строку и обратно; - `MatchRecord` — seed, колоды, команды, хэш событий; - `MatchRecorder` — записывает только принятые команды; - `ReplayRunner` — воспроизводит и проверяет хэш; - `EventHasher` — FNV-1a по тексту событий. Запись переживает JSON и воспроизводится бит-в-бит. ### 4.5. Диагностика - `server/src/Vanguard.Rules/Diagnostics/GameInvariants.cs`; - CLI-команда `stress [партий]` — случайный бот, лимит 5000 шагов на партию. ### 4.6. Тесты `server/tests/Vanguard.Rules.Tests/GameEngineTests.cs` — **117** тестов: - механики фаз, боя, защиты, перехвата, триггеров; - все ключевые карты и стоимости; - 50 детерминированных партий случайного бота с инвариантами; - JSON-реплей полной партии с проверкой хэша. `server/tests/Vanguard.Rules.Tests/ProtocolTests.cs` — **12** тестов: - round-trip всех кадров клиента и сервера через MessagePack, включая вложенные union'ы `MatchView` и `EventView`; - отказ на пустом и слишком большом кадре; - соответствие интентов и `GameCommand` в обе стороны; - скрытие руки противника, колоды и закрытого урона; - отсутствие seed/RNG в DTO; - `LegalIntents` только у решающего игрока; - скрытие поиска в колоде от противника и сохранение глобальных номеров событий при фильтрации. ### 4.7. Сеть: протокол и контракты - `docs/protocol.md` — версия 1: WebSocket + MessagePack, handshake по одноразовому тикету, `intentId` и идемпотентность, `eventSeq` и resync, heartbeat, реконнект 0/2/10/30 с, таймер 90 с + банк 5 мин, авто-поражение через 5 минут отсутствия, таблица видимости зон. - `Vanguard.Shared` — мультитаргет `netstandard2.1;net10.0`: - `netstandard2.1` — DTO и MessagePack для Unity, без ссылки на правила; - `net10.0` — дополнительно проекция состояния поверх `Vanguard.Rules`. - DTO: кадры клиента и сервера, 18 интентов, 30 событий, `MatchView`, `PlayerView`, `CardView`, `ClockView`. - `ProtocolResolver` — `StandardResolver` + интернирование строк; `MessagePackSecurity.UntrustedData`; лимит кадра 256 КиБ. - `MatchProjection` — отдельный `MatchView` на каждого игрока; `EventProjection` — фильтрация скрытых событий и диапазон по `eventSeq`; `IntentMapping` — единственное место связи DTO с командами движка. - Проекция вызывается для наблюдателя без побочных эффектов и не трогает `LegalCommands()` чужого игрока (иначе утекала бы рука решающего). ### 4.8. Сервер `server/src/Vanguard.Server` — ASP.NET Core 10, один процесс. Матчи, очередь и тикеты сохраняются в PostgreSQL/Redis, если они настроены; по умолчанию (`Storage:Provider=None`) сервер работает без внешних сервисов. - **HTTP**: `POST /api/game/queue`, `GET /api/game/queue/{id}`, `DELETE /api/game/queue/{id}`, `POST /api/game/tickets`, `GET /health`. Контракт — `docs/server-api.md`. - **Матчмейкинг**: две очереди (Classic Ranked / Casual), скрытый MMR, окно ±100 с расширением на 50 каждые 5 с до 2000, выбор ближайшего MMR. Ранкед меняет MMR по Эло (K = 32), casual — нет. - **Тикеты**: одноразовые, TTL 60 с, привязаны к `(playerId, matchId, seat)`; при настроенном Redis изымаются через `GETDEL` (одноразовость между инстансами), иначе — из памяти. - **`IMatchGrain`**: 1 матч = 1 экземпляр, все входы (интенты, подключения, тики) проходят через один семафор; внутри нет ни ASP.NET Core, ни WebSocket — только `IMatchConnection`. Orleans-хост (`MatchActorGrain`) подключается поверх того же контракта; соединения живут в `IMatchConnectionRegistry`, чтобы состояние грана оставалось сериализуемым. - **WebSocket** `/game`: handshake, MessagePack, `intentId`, idempotency, `eventSeq`, `resync`, heartbeat (20 с простоя → ping, ещё 20 с без ответа → `4408`), реконнект, захват сессии (`4409`), rate limit 16 интентов/с. - **Таймеры**: 90 с на решение + банк 300 с, отсутствие 300 с → поражение; `ServerClock` каждые 5 с. - **Результат**: сдача, явный выход, таймер и отсутствие завершают партию через `GameEngine.Finish(winner)`; событие `GameFinished` уходит обоим, результат пишется в профили. - **Проекция**: своя рука, свой выбор, свои легальные интенты; у противника только количества и открытые зоны. `seed` и RNG не покидают сервер. - **PostgreSQL**: таблицы `matches` и `match_intents`. В заголовке — seed, колоды, игроки, часы, банки, статус; в журнале — `intentId`, команда, результат. При старте незавершённые матчи восстанавливаются повтором принятых команд на детерминированном движке, включая ожидания выбора: делегаты движка не сериализуются. Журнал реплея сверяется по `eventSeq`. Банк и дедлайн пишутся каждые ~5 с; потеря при аварии — не больше тика. Схема — EF-миграции (`Migrations/InitialCreate`), на старте `Migrate`. - **Redis**: тикеты и записи очереди (JSON + sorted set по режимам, TTL); очередь поднимается при старте. Ключи с префиксом `vanguard:`. - **Конфигурация**: `Storage:Provider` = `None` / `Memory` / `Postgres`, `Storage:ConnectionString`; `Redis:Configuration`. `Memory` — EF InMemory для тестов, `Postgres` — постоянное хранилище. - **Orleans**: `Orleans:Enabled` (`false` по умолчанию, `true` в compose). Включённый режим поднимает silo с localhost-clustering в том же процессе (`Orleans:SiloPort` / `GatewayPort`, по умолчанию 11111/30000); `MatchActorGrain` держит доменный `MatchGrain`, `IMatchConnectionRegistry` остаётся в процессе. Активный матч держится тикером, завершённые матчи выгружаются после `CompletedMatchTtlSeconds`; страховочный CollectionAge 12 часов. Один silo на инстанс; для нескольких реплик нужен настоящий кластер и внешний реестр соединений. Тест кластера — `OrleansMatchTests`. - **Docker**: `Dockerfile` + `docker-compose.yml` поднимают postgres, redis (AOF) и сервер; `/health` на `http://localhost:8080` (порт меняется через `VANGUARD_PORT`, адрес WebSocket — `VANGUARD_WS_URL`). - **CI**: `.gitea/workflows/ci.yml` — restore, тесты, `validate` и `translations check` на push/PR. Actions включены для репозитория, `home-runner` выполняет job (проверено: все шаги зелёные). `setup-dotnet` указан явным URL с GitHub: в зеркале `code.forgejo.org` его нет. - **Тесты серверного ядра** (35 из 44 в `Vanguard.Server.Tests`): - тикеты: одноразовость и TTL; - heartbeat: ping после 20 с простоя, закрытие через 20 с, сброс любым кадром; - grain: attach, чужое место, duplicate intent, чужой ход, сдача, явный выход, команды после финала, банк времени, отсутствие, старт часов, reconnect с `eventSeq`, захват сессии; - матчмейкинг: подбор, разные очереди, окно, плохая колода, отсутствие стартового Авангарда, отмена, замена очереди, истечение, MMR, перезагрузка очереди из хранилища; - персистентность: EF-хранилище, восстановление матча повтором команд, `MatchRecoveryService` после «рестарта»; - внешние хранилища (`ExternalStorageTests`, запускаются при `VANGUARD_TEST_POSTGRES` / `VANGUARD_TEST_REDIS`): реальные PostgreSQL и Redis, изъятие тикета, очередь; - E2E: два клиента играют полный матч по WebSocket и сходятся в победителе; обрыв и возврат по новому тикету с догоном состояния; - Orleans: грана инициализируется, принимает attach и интент, граница Orleans сериализуется (тестовый `TestCluster`); полный матч двух клиентов через HTTP/WebSocket при `Orleans:Enabled=true`. Проверено на `ssh home`: 131 + 44 теста, включая реальные PostgreSQL и Redis; `docker compose up` поднимает стенд, матч создаётся через HTTP-очередь, а после `docker compose restart server` восстанавливается (`Recovery: 1 of 1`). ### 4.9. Мета-API - **Аккаунты**: `register`, `login`, `refresh`, `logout`, `me`. BCrypt, JWT access 15 минут, refresh 30 дней с ротацией; в БД хранится хэш refresh-токена, старый после ротации отзывается. - **Онбординг**: одна из двух триал-дек, 50 карт в коллекцию и 5 токенов; повтор невозможен. Триал-деки — `content/trial-decks.yaml`. - **Коллекция**: по `printingId`, без верхнего предела; разбор C 5 / R 10 / RR 25 / RRR 100 пыли, крафт 20 / 40 / 100 / 400, SP нельзя. - **Колоды**: CRUD, валидация 50 карт / ≤4 по `nameId` / Sentinel / Heal / стартовый Авангард, проверка владения картами, лимит 30, шаринг по коду. - **Магазин**: бустер 100 Gold или 1 токен; вторая триал-дека 500 Gold; состав бустера 4 C + 1 R+ (R 70 / RR 22 / RRR 7 / SP 1) из `content/pack_odds.yaml`; duplicate protection по `printingId` с порогом 4. - **Экономика**: журнал транзакций, баланс вычисляется, `idempotencyKey` на всех покупках, разборе и крафте. - **Очередь с JWT**: `{ mode, deckId }`, игрок и колода берутся из токена и коллекции. Dev-режим `playerId + deck` остался только при `Game:AllowDevQueue=true`. - **Rate limiting**: 60/мин на auth, 300/мин на прочие мутации, плюс глобальный лимит по IP. ### 4.10. История матчей и реплеи - `MetaProfileService` после подтверждённого завершения матча пишет историю обоим игрокам, меняет MMR в ранкеде и начисляет Gold/XP. Победа 30 Gold / 150 XP, поражение 15 / 75; сдача, явный выход, таймер и отсутствие дают 0, но остаются в истории. - `GET /api/matches`, `GET /api/matches/{id}` — история игрока. - `GET /api/replays/{id}` восстанавливает матч из seed, колод и журнала принятых команд и отдаёт: - `events` — события, видимые этому игроку; - `frames` — `MatchView` до и после каждой команды. - Реплей собирается под зрителя: рука противника и закрытый урон скрыты, `legalIntents` пуст, часы обнулены. Смотреть может только участник. - Проверено на `ssh home`: живой Postgres, регистрация → онбординг → бустер за токен → колода → очередь → матч; `/api/replays/{id}` отдаёт 11 стартовых событий и кадр, рука противника пуста. После рестарта сервера матч восстановлен (`matches: 1`). Счётчик `Vanguard.Server.Tests` вырос до 44: добавлены auth, мета-профиль, мета-E2E (онбординг → бустер → колода → матч → история → реплей), проверки реплея, явного выхода и истории/реплея после выхода. --- ## 5. Структура и ключевые файлы ``` /server/src/Vanguard.Rules /Engine/GameEngine.cs основной движок (~2500 строк) /Engine/GameState.cs состояние, зоны, InTransit /Engine/GameModels.cs CardInstance, Step, Phase, PendingChoice /Engine/GameCommands.cs команды /Engine/GameEvents.cs события /Engine/Modifiers.cs StatModifier, GrantedAbility, CardRestriction /Engine/Abilities/Abilities.cs AbilityDefinition, ActivatedAbility, триггеры /Engine/Abilities/CardScripts.cs реестр Default + базовые карты /Engine/Abilities/CardScriptsBT01.cs /Engine/Abilities/CardScriptsTD.cs /Cards/CardDefinition.cs статы, Sentinel, Restraint, snipeBackRow /Decks/DeckValidator.cs правила колоды /Replay/ запись и воспроизведение /Diagnostics/GameInvariants.cs /server/src/Vanguard.Content загрузка YAML и валидация /server/src/Vanguard.Cli validate, stats, translations, stress, deck /server/src/Vanguard.Shared /Protocol/*.cs кадры, интенты, события, view, кодек /Projection/*.cs проекция состояния, фильтр событий, маппинг /server/src/Vanguard.Server /Program.cs ASP.NET Core, DI, WebSocket endpoint /Api/ очередь, тикеты, /health /Auth/ JWT, register/login/refresh, BCrypt /Meta/ коллекция, колоды, магазин, экономика, история, реплеи /Matchmaking/ очереди, MMR, Redis-хранилище очереди /Matches/ IMatchGrain, таймеры, тикер, регистр /Realtime/ WebSocket-обвязка, чтение кадров /Tickets/ одноразовые тикеты: память или Redis /Storage/ EF-хранилище, восстановление матчей /Profiles/ интерфейс MMR /Dockerfile /server/tests/Vanguard.Rules.Tests 131 тест /server/tests/Vanguard.Server.Tests 44 теста, включая E2E и внешние хранилища /content/sets карты /content/trial-decks.yaml две триал-деки /content/pack_odds.yaml веса бустера /content/translations/ru.yaml переводы /docs/translation-rules.md канон перевода /docs/protocol.md протокол матча, версия 1 /docs/server-api.md HTTP-очередь, тикеты, /health /docs/HANDOFF.md этот файл /docker-compose.yml postgres, redis, server /.env.example VANGUARD_PORT и VANGUARD_WS_URL ``` --- ## 6. Принятые решения Их не нужно пересматривать без причины. - **Общение:** ответы короткие, без повторов и длинных вступлений. - **Сомнения:** любое сомнение/вопрос — диалог с 2–4 вариантами; выбор делает пользователь. Не угадывать. - Формат называется **Classic**, не Standard. - Имена карт английские; `nameRu` зарезервировано. - Термины: Авангард, Арьергард, ранг, смена, поддержка, проверка, фазы Подъёма/Добора/Смены/Основная/Боевая/Завершающая, завершающий шаг, Сдерживание, «вы», буква ё. - Способности ищутся по `nameId`, а не по принту. - Лимит 4 копий — по `nameId`, только правило декбилдера. - Дубль в коллекции — по `printingId`; порог 4, замена внутри редкости. - Бустер: 5 карт = 4 C + 1 R+; веса R 70 / RR 22 / RRR 7 / SP 1. - Gold за матч: 30/15/20 (победа/поражение/ничья); Gold = XP / 5. - Таймер хода 90 с + банк 5 мин. - Транспорт матча — **WebSocket** + MessagePack; TCP с префиксом длины — резерв для Unity, если WebSocket не подойдёт. - Тикет на подключение — одноразовый, TTL 60 с, передаётся в `ClientHello`, не в URL; reconnect получает новый тикет. - `playerId` в HTTP-API — dev-заглушка до JWT. Выставлять сервер в интернет в таком виде нельзя. - Состояние сервера: PostgreSQL — снапшот/журнал матчей, Redis — тикеты и очередь; без конфигурации работает in-memory. `IMatchGrain` отделён от транспорта, Orleans подключается поверх и включается конфигом. - Rate limit — 16 интентов в секунду на соединение. - Скрытый MMR: старт 1000, Эло K = 32, casual рейтинг не меняет. - Аккаунты: BCrypt, access JWT 15 минут, refresh 30 дней с ротацией; в БД хранится только хэш refresh-токена. - Онбординг: одна триал-дека бесплатно + 5 токенов; вторая триал-дека за 500 Gold; бустер 100 Gold или 1 токен. - Коллекция по `printingId` без лимита; лимит 4 по `nameId` — только декбилдер. - Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт 20 / 40 / 100 / 400, SP не разбирается и не крафтится. - Матч: победа 30 Gold / 150 XP, поражение 15 / 75; Gold = XP / 5. Сдача, явный выход, таймер и отсутствие наград не дают. - История матча пишется обоим игрокам один раз; реплей доступен только участникам и собирается под зрителя, без руки противника. - Порядок триггеров выбирает игрок; опциональные способности спрашиваются. - Непрерывные эффекты пересчитываются при каждом запросе. - Поиск в колоде: 0 — уведомление и перемешивание, 1 — сразу, 2+ — выбор. - Эффекты не вкладываются: цепочка строго друг за другом. - Каждый блок — коммит и push в `master`. --- ## 7. Что осталось ### 7.1. Ближайший блок — сервер Сделано в этом блоке: 1. **[x] `docs/protocol.md`** — версия 1: кадры, handshake по тикету, `intent` с `intentId` и `eventSeq`, resync, реконнект 0 / 2 / 10 / 30 с, отсутствие 5 минут → авто-поражение, таймер 90 с + банк 5 мин, heartbeat, коды ошибок, таблица видимости. 2. **[x] `Vanguard.Shared`**: DTO интентов и событий, MessagePack-резолвер, проекция состояния на каждого игрока, фильтрация скрытых событий, маппинг интентов. 3. **[x] `Vanguard.Server`** — сетевой матч работает: ASP.NET Core + WebSocket, тикеты, `IMatchGrain` (in-process), матчмейкинг, MMR, идемпотентность, таймеры, реконнект, `/health`, Dockerfile и compose. Контракт HTTP — `docs/server-api.md`. 4. **[x] Проекция состояния** подключена к грану и проверена E2E-тестом: своя рука и свой выбор полностью, у противника только количества и открытые зоны, seed и RNG не уходят. 5. **[x] `Docker Compose`** — postgres, redis (AOF) и сервер; `/health`, порт через `VANGUARD_PORT`. Сервер запускается с `Orleans:Enabled=true`. 6. **[x] Персистентность**: - PostgreSQL — заголовок матча + журнал интентов, восстановление повтором команд после рестарта, `MatchRecoveryService`; - Redis — одноразовые тикеты (`GETDEL`) и очередь с перезагрузкой. Осталось в серверном направлении: 7. **Мета-API** — сделано: JWT, аккаунты, коллекция, колоды, магазин, экономика, онбординг, матчмейкинг по `deckId`, история и реплеи. `playerId` остался только как dev-режим при `Game:AllowDevQueue=true`. 8. **[x] Orleans** — `MatchActorGrain` + `OrleansMatchAdapter`; silo включается `Orleans:Enabled=true`, в compose включён. `MatchGrain` остался доменным ядром, соединения вынесены в `IMatchConnectionRegistry`, `MatchActorGrain` восстанавливается из снапшота на активации. Покрыто `OrleansMatchTests` (тестовый кластер) и `OrleansServerE2ETests` (полный матч через HTTP/WebSocket при включённом silo). Критерий готовности серверного блока выполнен: два клиента играют полный матч по сети; kill клиента восстанавливается через новый тикет и `resumeFromSeq`; рестарт сервера восстанавливает матчи из PostgreSQL. CI подключён и проходит; серверный блок закрыт. ### 7.2. Мета-API Сделано: - `User`, BCrypt, JWT (access 15 мин + refresh 30 дней, ротация). - `register`, `login`, `refresh`, `logout`, `me`. - Коллекция по принтам, без верхнего предела; разбор и крафт. - Колоды: CRUD, `validate`, лимит 30, шаринг по коду, проверка владения. - Магазин: бустеры, триал-деки, токены; duplicate protection. - Пыль: разбор C 5 / R 10 / RR 25 / RRR 100, крафт 20 / 40 / 100 / 400; SP не разбирается и не крафтится. - Онбординг: триал-дека + 5 токенов. - Матчмейкинг по JWT и `deckId`; dev-режим только для разработки. - Результат матча: Gold/XP, MMR, история, реплеи. - Rate limiting. Осталось после MVP: косметика, квесты, баттлпасс, Draft, друзья/чат, история покупок в клиенте. ### 7.3. Клиент Первый блок начат: **Unity 6000.6.6f1**, URP-проект в `client/`. Сделано: - проект создан из URP-шаблона Unity 6.6; пакеты: URP, Input System, Addressables, Localization, Newtonsoft.Json, VContainer (OpenUPM); - визуальное направление зафиксировано: **3D**, отдельные сцены-экраны Login/Lobby/Collection/DeckBuilder/Battle (`docs/client-visual-direction.md`); сцены и `SceneFlow` уже созданы, UI пока code-generated gray-box; - сцена `Boot` + runtime-инициализатор, поэтому клиент стартует и в пустой сцене; - composition root на VContainer: `AppConfig`, `TokenStore`, `ApiClient`, `AuthService`, `MetaService`, `PlayerState`, `SceneFlow`; - HTTP-клиент на `UnityWebRequest` + Newtonsoft.Json, разбор `ApiError`, таймаут 20 с; - login/register/refresh/me/logout, восстановление сессии по refresh, сохранение токенов в `PlayerPrefs` (dev); - экран входа с выбором адреса сервера (стенд по умолчанию `http://192.168.31.158:18080`) и главный экран с балансами, MMR и переходами в разделы; - онбординг: список триал-дек из `/api/store`, выбор одной, 50 карт + 5 токенов, обновление профиля; - коллекция: `/api/collection`, балансы, разбор в пыль с подтверждением; - колоды: `/api/decks`, удаление и автосборка стартовой колоды из выбранной триал-деки (50 карт); - магазин в Lobby: `/api/store`, бустер за Gold/токен с показом 5 карт, покупка второй триал-деки; - история матчей и реплей: `/api/matches`, `/api/replays/{id}`, просмотр событий и кадров состояния; - очередь classic-ranked, опрос статуса, отмена, тикет и `wsUrl`; - `GameConnection` (WebSocket + MessagePack), handshake `ClientHello`, snapshot/events, интенты с ответом сервера, ping/pong, закрытие; - серый боевой экран: доска, рука, часы, все легальные интенты кнопками, сдача с подтверждением, результат; - `Vanguard.Shared.dll` (netstandard2.1) и MessagePack 3.1.12 подключены как плагины; `client/README.md` описывает запуск и ограничения; - сцены `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle` и `SceneFlow`; `Boot` — стартовая служебная сцена, `VanguardApp` и сервисы живут между сценами; - 3D-фон на каждой сцене: камера, свет, пол и тематические примитивы; UI пока code-generated uGUI; - EditMode smoke `Vanguard.Tests.Editor` и `Vanguard.Editor.SceneSmoke` проверяют запуск Boot → Login и создание экранов/3D-окружения; live-probe (не входит в набор) проверил онбординг, коллекцию, создание/удаление колоды, а также два WebSocket-подключения, маллиган, сдачу и `GameFinished` против стенда. Дальше по клиенту: 1. Боевой UI: арты, зоны, анимации, скип/ускорение; реконнект и таймауты. 2. Полный декбилдер: фильтры, счётчики, валидация. 3. 3D-наполнение сцен: модели/арт, prefab-экраны, камера. 4. Локализация RU/EN, настройки, DOTween. **Зафиксированное визуальное направление** — `docs/client-visual-direction.md`: 3D-клиент на URP, отдельные сцены-экраны Login / Lobby / Collection / DeckBuilder / Battle; Boot и общие сервисы остаются персистентными. Сцены и `SceneFlow` уже созданы; UI пока code-generated gray-box. - Unity 6 LTS, URP, Addressables, Input System, Localization, DOTween. - VContainer, `ApiClient`, `GameConnection`, `SceneFlow`. - Сцены: `Boot` (служебная), `Login`, `Lobby`, `Collection`, `DeckBuilder`, `Battle`; очередь/онбординг — оверлеи Lobby. - История матчей и экран реплея: `GET /api/matches`, `GET /api/replays/{id}`, проигрывание `frames` + `events`. - Локализация RU/EN на лету. ### 7.4. Лаунчер - WPF: обновления, запуск клиента, выбор сервера. ### 7.5. Мелкие технические долги - `OnDamageTaken` не вызывается — добавить, когда появится карта с таким триггером. - Глобальный лимит шагов внутри движка не enforced — только в стресс-тесте. - `GameEngine.cs` большой; при росте разнести на partial-файлы. - `MatchRecord` сейчас JSON; для сети позже MessagePack. - В `LegalCommands()` не входят активируемые способности со стоимостью «сброс карты» — для UI их нужно будет перечислить с вариантами сброса. --- ## 8. Порядок действий в новой сессии 1. Прочитать `docs/SESSION-STATE.md`, `docs/HANDOFF.md`, `IMPLEMENTATION_CHECKLIST.md`, `README.md`. 2. Прогнать проверки из раздела 3. 3. Следующий блок — клиент: магазин/бустеры в Lobby и история/реплей, затем полный декбилдер и 3D-наполнение сцен. Сервер и мета-API готовы: `docs/protocol.md`, `docs/server-api.md`, `Vanguard.Shared`. 4. Перед началом сверить `docs/client-visual-direction.md`: 3D-сцены, `Boot` и общие сервисы персистентны. 5. После каждого блока: тесты, `validate`, `translations check`, коммит, push. 6. Не ломать `Vanguard.Rules`: сервер должен зависеть от него, а не наоборот. --- ## 9. Подводные камни - **Клиент: HTTP.** В Unity по умолчанию запрещены незащищённые соединения: для dev-стенда в `client/ProjectSettings/ProjectSettings.asset` стоит `insecureHttpOption: 2` (Always). Для релиза — HTTPS и вернуть запрет. - **Клиент: WebSocket.** `GameConnection` использует `System.Net.WebSockets.ClientWebSocket`; это окно для Windows-десктопа. Для WebGL/мобильного IL2CPP нужен отдельный транспорт/плагин. - **Кодировка.** Не использовать PowerShell `Get-Content`/`Set-Content` для UTF-8 файлов с русским текстом: однажды это уже испортило комментарии. Правки — только через `edit`/`write`, либо Python с `encoding="utf-8"`. - **PATH.** `dotnet` лежит в `%LOCALAPPDATA%\dotnet`; без добавления в PATH команды не находятся. - **Solution.** `server/Vanguard.slnx`, не `.sln`. - **Тесты.** `StartedEngine()` принудительно ставит Conroe стартовым Авангардом, чтобы Barcgal не срабатывал. - **Shared.** Мультитаргет: файлы `Projection/` компилируются только для `net10.0`; в `netstandard2.1` (Unity) ссылки на `Vanguard.Rules` нет. - **Видимость.** `LegalCommands()` описывает решающего игрока. Вызывать его для наблюдателя нельзя — в списке есть printingId из чужой руки. - **Сервер: rate limit.** 16 интентов в секунду на соединение. Быстрый бот в E2E специально притормаживается; живой UI в лимит не упирается. - **Сервер: хранилище.** По умолчанию `Storage:Provider=None`: матчи живут в памяти (`NullMatchStore`), мета-API — в EF InMemory. Для рестарта нужен `Postgres` + `Storage:ConnectionString`; `Memory` — EF InMemory для всего. Схема — EF-миграции (`Migrations/InitialCreate`), на старте `MigrateAsync`; для InMemory — `EnsureCreated`. Миграции применяются автоматически. - **Сервер: снапшот матча.** Сериализуется не состояние движка, а seed, колоды, игроки, часы и журнал интентов. При восстановлении команды проигрываются заново; `EventSeq` обязан совпасть, иначе матч пропускается с ошибкой. Делегаты `PendingChoice`/`Continuation` восстанавливаются самим реплеем. - **Сервер: восстановление.** `MatchRecoveryService` поднимает только незавершённые матчи за последние 7 дней. Завершённые остаются историей в PostgreSQL и не восстанавливаются в память. Банк/дедлайн пишутся раз в ~5 с, поэтому авария теряет не больше тика. - **Сервер: Redis.** Тикеты изымаются через `GETDEL` (Redis 6.2+; в compose 7). Очередь — JSON + sorted set по режимам, TTL в два раза больше очереди. Префикс ключей — `Redis:Prefix` (`vanguard:`). - **Сервер: Docker.** На домашнем сервере порт 8080 занят, поэтому стенд поднимается с `VANGUARD_PORT=18080` и `VANGUARD_WS_URL` в `.env`. `docker compose restart server` проверяет восстановление матчей. - **Сервер: внешние тесты.** `VANGUARD_TEST_POSTGRES` и `VANGUARD_TEST_REDIS` включают `ExternalStorageTests`; без них тесты проходят как no-op. Postgres-тест делает `EnsureDeleted`, поэтому БД должна быть тестовой. - **Сервер: тесты.** E2E поднимает `WebApplicationFactory`, играет полный матч по WebSocket и проверяет, что победитель совпал. Случайные партии завершаются за ~350 действий; таймаут теста — 120 с. - **Мета: JWT.** `Jwt:SigningKey` — не короче 32 символов; дефолтный ключ только для разработки. Access 15 минут, refresh 30 дней с ротацией; в БД лежит SHA-256 от refresh, старый токен после ротации недействителен. - **Мета: dev-очередь.** `Game:AllowDevQueue=true` разрешает очередь по `playerId` без JWT. В compose выключено; не включать в production. - **Мета: колода.** Сохранение проверяет не только правила Classic, но и владение каждой картой. Триал-дека даёт карты в коллекцию, а не готовую колоду — её собирают декбилдером. - **Мета: бустер.** 4 C + 1 R+, веса в `content/pack_odds.yaml`. Duplicate protection считает `printingId` и порог 4; при насыщении редкости выбирается другая, иначе допускается дубль. SP не крафтится и не разбирается. - **Мета: награды.** Gold/XP только за `normal`; сдача, явный выход, таймер и отсутствие дают 0. История пишется один раз на `(userId, matchId)`, повтор результата не дублирует валюту. - **Реплей.** Доступен только участнику; собирается из seed, колод и журнала принятых команд. `frames` — состояние после каждой команды, `events` — только видимые зрителю события; рука противника и закрытый урон скрыты. Сырые колоды и seed наружу не отдаются. - **Сервер: таймеры.** Тик раз в секунду. После 90 с банк списывается по фактическому превышению; `TurnDeadlineMs` в этот момент сдвигается, поэтому UI должен показывать ещё и `BankMs`. - **Сервер: часы.** Отсчёт стартует только когда подключились оба места. До этого `ClockView.IsRunning = false`, но таймер отсутствия уже идёт. - **Сервер: проекция.** `ServerSnapshot` и `ServerEvents` собираются отдельно на каждое место; не переиспользовать один `MatchView` на двоих. - **Порядок фаз.** Вход в Основную — `EnterMainPhase()` с `OnPhaseStart`. - **Отложенные продолжения.** `State.Continuation` — что выполнить после паузы; `State.PendingQueue` + `_resolvingDepth` запрещают вложенность эффектов. - **`OfferAbility`.** Возвращает `Skipped / Paused / Resolved`; вызывающий код продолжает очередь только при `Skipped`. Ошибка здесь уже приводила к потере шага защиты. - **`InTransit`.** Найденная карта, ждущая выбора круга, лежит в `State.InTransit`, иначе инвариант «50 карт» падает. - **Задний ряд.** Обычные карты атакуют только передний ряд; Tejas — задний в своей колонке через `snipeBackRow`. - **Реестр карт.** Новая карта без записи в `CardScripts.Default` молча останется без способностей; проверять покрытие скриптом. - **Тестовые колоды.** `BuildDeck()` в тестах не содержит все карты; для конкретной карты использовать `MakeCard()` и класть вручную.