Статус: ТЗ, решения приняты 2026-07-31 и 2026-08-01, кода нет. Ход мысли, внешний ресёрч и
отвергнутые ветки — журнал захода docs/coop-slice-progress.md (в репо, не в vault). Здесь только то,
что реализуется.
Смежное: дизайн коопа — Coop - Index и Coop - Sync Model (классы синхронности), слой сохранений — Planning - Save System (снимок сессии = сейв-конверт), авторитет — Journal - Host-Authoritative, Not Lockstep, раздача боя — Journal - The Tape Ships As Events Plus Packed Snapshots.
1. Что решаем и чего НЕ решаем
Вертикаль отвечает на один вопрос: двое игроков видят один и тот же бой и видят друг друга — и делает это так, чтобы остальной кооп достраивался без переделки уже написанного.
Критерий приёмки (Макс, 01.08.2026): двое соединились по Steam-инвайту, оба смотрят один бой с одинаковыми таймингами, смертями и исходом, оба видят курсор напарника; выход любого из них не роняет второго.
Вне рамок: транзакции над экранами подготовки (казна, драфт, найм, голосование — §10), пари и арбитры, миграция хоста (§2 п.2), late-join в идущий забег.
Стек — ИЗМЕНЁН 02.08.2026 (вердикт Макса «планирую только Steam»): высокоуровневого netcode нет
вовсе. Работаем со Steam Networking Sockets напрямую через свой INetTransport; NGO, UTP и четыре
пакета удалены из проекта. Разбор и цена —
Journal - The Stack Shrinks To Steam.
Ниже по тексту ТЗ NGO ещё упоминается — читать как историю решения, а не как состав работ.
2. Принятые решения
Даты — когда Макс сказал «да»; строчки без даты — техническое, принято моей властью.
| № | Решение | Дата | Почему так |
|---|---|---|---|
| 1 | Критерий приёмки — «двое смотрят бой + курсоры» | 01.08 | Совет из ресёрча: первая версия должна убедить, что игроки в одном мире, а не быть пиксельно точной |
| 2 | «Хост ушёл» = конец сессии, гости уносят открытия в профиль | 01.08 | Гильдия одна и живёт у хоста (дизайн). Миграция авторитета не пишется вовсе — а это одна из самых дорогих и самых багованных подсистем сетевого кода |
| 3 | Идемпотентность команд — пара «игрок + его счётчик» | 01.08 | Клиент нумерует сам и не ждёт номера от хоста; без этого оптимистичный локальный отклик (п.6) требовал бы второго механизма рядом |
| 4 | Бой раздаётся чанками: события + сжатые снимки | 01.08 | См. журнал вердикта. Клиент не считает бой ничем |
| 5 | Присутствие — 128 Гц, dirty-check, короткий буфер | 31.07 | Плавность даёт интерполяция на приёме, частота покупает размер буфера; 60→120 даёт ~16 мс даром |
| 6 | Своё действие — оптимистичный локальный отклик, хост подтверждает | 31.07 | Задержка перехвата — полный RTT; дизайн к «не успел» готов («обратимое хулиганство») |
| 7 | Порядок конкурирующих интентов — по клиентскому времени на синхронных часах | 31.07 | Иначе у хоста RTT нулевой и он всегда выигрывает гонку за кубик и за вещь в воздухе |
| 8 | Лог команд забега — append-only, RunState меняется только через команды с номерами | 31.07 | Даёт тесты, аудит «кто передвинул», реконнект и идемпотентность одним механизмом |
| 9 | Отпечаток контента + версия сборки едут в Connection Approval | — | Хеш ловит расхождение контента, версия — расхождение кода; порознь каждый пропускает свой класс поломки |
| 10 | Два транспорта: UTP в dev, Facepunch на релизе | — | Network Simulator работает с UTP; Steam-транспорт, приросший к коду, лишает нас отладки на годы |
2.1 Steam AppId: тесты на старом слоте, свой — к демке
Решено 01.08.2026 (Макс): тесты идут на AppId 3259720 — его неизданной Few Seconds - Many
Deaths!, страница которой осталась жива. Свой слот покупается ближе к демке. Уже вписано в
Net/FacepunchTransportBootstrap.cs; переезд стоит одного числа, компонент нигде не размещён.
Почему это лучше Spacewar (480), на котором обычно тестируют: у 480 владеют все, поэтому он не проверяет главное — что лобби и relay работают под приложением с ограниченным кругом владельцев. Оттуда же три следствия, и все три надо закрыть до фазы Г, а не в ней:
- Тестировать может только аккаунт с доступом. Второй тестовый аккаунт заводится в Steamworks заранее (ключ или playtest-доступ) — иначе «двое на двух машинах» упрётся не в код.
- Steam Auto-Cloud у 3259720 надо проверить и на время тестов выключить. Облако действует по
AppId и не знает, кто писал файл; наша маска —
Saves/**/*.jsonподLocalLow/Alebardium/Guildmaster/(Planning - Save System). Пути старой игры растут из еёproductName, так что пересечения быть не должно — но проверяется это один раз, а лечится потерянными сейвами. - Один Steam-клиент на машине = один аккаунт, поэтому два клиента локально под Steam-транспортом не поднять вовсе, и MPPM с виртуальными игроками для этого пути не годится. Это не неудобство, а причина, по которой UTP-профиль (§4.2) обязателен: без него отладка вдвоём требует второй машины каждый раз.
Косметика, о которой стоит помнить, зовя друзей на тест: в списке друзей игра будет называться Few Seconds - Many Deaths!.
3. Инвентарь: на что опираемся
Проверено по коду 31.07-01.08.2026.
| Есть | Где | Годность |
|---|---|---|
| Боевая лента | Combat/Tape/ — BattleTape, TapeEvent, UnitSnapshot, BattleTapePlayback, BattleTapeDispatcher | готова, менять не надо: события за весь бой, снимки кольцевым окном, показ с лагом и своими часами |
Владелец RunState | Guild/RunStateService — SetSlotPosition, SetSlotRelic, AddGold, RemoveRelic… | глаголы записи уже есть — их заворачиваем в команды, а не изобретаем заново |
| Поле владельца слота | RunState.SlotOwner (int[], соло — все нули) | кооп-шов заложен заранее, «кого кто ведёт» уже есть куда писать |
| Гейт готовности | Game/Flow/RunFlowSeams.cs — IReadyGate + SoloReadyGate, IPlayerIntentSource.IsLocalAuthority | шов есть, тела нет — сюда встаёт сетевая реализация |
| Транспорт Steam | Net/FacepunchTransportBootstrap.cs | инициализация на AppId 480, больше ничего |
Net/BattleControlRelay.cs | переписан 02.08: lockstep-broadcast удалён вместе с NetworkCommandRelay, осталась общая пауза как состояние показа | |
| Проба чек-сумм | Net/_Parked/SimSyncProbe.cs → CombatSimulation.ComputeChecksum() | запаркована; расконсервируется в фазе Г как dev-инструмент, ставкой не является |
| Детерминизм | Core/Random/IRngService, BattleBootstrap.ReseedForBattle | есть; готча — CombatLifetimeScope регистрирует new XorShiftRng(0UL), спасает пересев перед боем |
| Реестр dev-команд | Core/DevConsole/DevCommandRegistry — явная регистрация, Execute(line), Unregister | 35 команд уже идут через один вход; дев-команды становятся хост-авторитативными бесплатно, если пройдут через шину (§4.1) |
| Сейв-конверт забега | ISaveService.TryLoad<T> → SaveLoadResult<T>, плоский DTO по строковым id | снимок сессии не требует своего формата — это run.json по сети |
4. Швы (фаза А) — то, что дорого ретрофитить
Два шва делаются первыми и до любого экрана коопа, потому что второе место записи в RunState
и приросший к коду Steam-транспорт лечатся только переписыванием.
4.1 Лог команд забега — РЕАЛИЗОВАНО 01.08.2026
Код:
Guild/Commands/(RunCommand,RunCommandLog,RunCommandApplier,RunCommandBus,IRunCommands), барьер —Guild/AssemblyInfo.csплюсinternalу мутаторов. Тесты —RunCommandLogTests. Решение и отвергнутые альтернативы — Journal - An Assembly Boundary Guards The Run Log.Через шину идут односторонние записи: позиция и кит в расстановке, золото, снятие мементо, награда за победу. Мимо лога пока пишут четыре транзакции —
TrySpendGold,TryAddRelic,TrySpendRestart,IncreaseCapacity: они отвечают «вышло ли» синхронно, а это несовместимо с «хост подтвердит». Их переезд = §10, транзакции. Граница закреплена тестомTransactions_DoNotGoThroughTheBusYet, то есть заявлена, а не забыта.
Правило: RunState мутируется только обработчиком команды. Прямой вызов RunStateService
мимо шины после фазы А — дефект, а не срезка.
- команда — плоская структура: тип, аргументы примитивами,
(PlayerId, Sequence)и клиентский штамп времени (п.7 §2); - лог append-only;
RunState— проекция лога. Отсюда бесплатно: реплей, аудит «кто передвинул юнита», реконнект как «снимок плюс хвост», тест «один лог → одинRunState»; - повтор пары
(PlayerId, Sequence)отбрасывается — это единственное лекарство от дублей на стыке реконнекта; - порядок применения — по клиентскому времени, при равенстве — по
PlayerId. Тай-брейк обязан быть детерминированным, иначе тест «один лог → одинRunState» ложно краснеет; - соло идёт тем же путём: локальная шина без сети. Это и есть проверка шва в одиночной игре — если соло работает мимо лога, кооп обнаружит это первым же расхождением.
Дев-команды тоже команды. DevCommandRegistry остаётся входом для строки, но команды, меняющие
RunState, отправляют интент в шину. Следствие: battle, kit, preset в коопе исполняются у всех
и попадают в аудит; читающие команды (fx, лог, витрина) сетью не касаются вовсе.
4.2 Транспорт за интерфейсом — РЕАЛИЗОВАН 01.08.2026, кроме UTP-профиля
Код:
Net/Transport/—INetTransport,LoopbackNetwork,ChaosTransport+ChaosProfile. Тесты —TransportSeamTests. Пакетcom.unity.multiplayer.tools2.2.9 в манифесте. Решение и грабли — Journal - The Network Becomes Debuggable Before It Exists.UTP-профиля пока НЕТ: третья реализация поверх NGO требует
NetworkManagerв сцене, то есть работы через редактор, и делается вместе с сессией (фаза Г). Loopback и chaos закрывают всё, что закрывается без сцены, — включая chaos-сценарии, ради которых Network Simulator и нужен был бы.
INetTransport с тремя реализациями:
| Реализация | Где живёт | Зачем |
|---|---|---|
LoopbackTransport | EditMode-тесты | всё выше интерфейса тестируется без NGO вообще |
| UTP-профиль | dev-сборки и PlayMode | Network Simulator из Multiplayer Tools (задержка, джиттер, потеря, лаг-спайки) работает только с UTP |
| Facepunch/Steam | релиз | лобби, инвайты, relay |
Плюс свой chaos-слой поверх интерфейса: задержка, потеря, переупорядочивание и дубли из сида
— чтобы падение воспроизводилось по номеру, а не «иногда на двух копиях». Это Deterministic
Simulation Testing, и половина метода у нас уже стоит (детерминированное ядро, Reseed,
ComputeChecksum).
Multiplayer Tools в манифесте нет — добавляется в фазе А вместе с UTP-профилем.
Решено моей властью: UTP-профиль — постоянная dev-настройка, а не только тестовая. Переключать транспорт руками ради воспроизведения бага означает не воспроизводить его вовсе. В prefs не выносится: выбор делает сборка (dev/release), игроку выбирать нечего.
4.3 Handshake версии и контента — ОТПЕЧАТОК ГОТОВ 01.08.2026
Код:
Data/ContentFingerprint.cs(хеш отсортированного набора id, FNV-1a; версия сборки; версия схемы;MatchesиDescribeMismatchдля текста отказа). Тесты —ContentFingerprintTests. Остался только провод: положить отпечаток в Connection Approval — это часть сессии (фаза Г), потому что до неё подключение неоткуда принимать.
В Connection Approval едет отпечаток контента (хеш реестра строковых id) + версия сборки. Отказ — с понятным текстом («у вас другая версия контента»), а не молчаливым разъездом.
Почему в фазе А, а не «потом»: у нас data-driven контент на строковых id, публичный репозиторий и живой
поток правок в SO. Чанк ленты несёт строковые id определений эффектов и способностей (§5), поэтому
неизвестный id на приёме роняет показ, а не слегка расходит картинку. NGO сверяет свой NetworkConfig,
про наш контент он не знает ничего.
5. Бой-как-кино: формат чанка ленты (фаза Б)
Состояние 01.08.2026: кодек и читатель написаны (
Net/Tape/TapeChunkCodec.cs,TapeChunkReader.cs), раздача и склейка тоже —TapeStreamerу хоста,TapeIntakeу гостя, конверт с байтом канала вNet/NetEnvelope.cs. Тесты —TapeChunkCodecTests,TapeDeliveryTests. Решения и грабли — Journal - The Tape Is Handed Out In Chunks. Фаза Б закрыта 02.08.2026. Провод к живой симуляции —Net/Tape/BattleTapeBroadcast.cs(раздаёт по последний досчитанный тик, дожимает хвост на конце боя, обнуляется наResetBattle).NetworkCommandRelayудалён: на его местеNet/BattleControlRelay.cs— общая пауза как состояние ПОКАЗА, поверхINetTransport, без NGO и сцены. Решения и грабли — Journal - The Shared Pause Belongs To The View. Остаётся регистрация в боевом скоупе — она едет вместе с сессией (фаза Г), потому что до неё транспорта в контейнере нет.
Чанк — append-only срез ленты по диапазону тиков, с номером и тиковым штампом. Клиент складывает
чанки в свою BattleTape и проигрывает её тем же BattleTapePlayback, которым играет соло.
5.1 Что едет и в каком виде
| Часть | Как едет | Почему так |
|---|---|---|
| Позиция юнита | квантованная, 2 × 2 байта | единственное, что течёт непрерывно |
PreviousPosition | не едет | это позиция предыдущего тика, у приёмника она уже есть |
| HP, щит, ресурс, фазы, кулдауны, теги, признаки | dirty-маска + дельта к прошлому тику | меняются редко; полный снимок каждый тик — это ~100 байт на юнита и мегабайт с лишним на бой |
События (TapeEvent) | как есть, плоскими полями | их пара сотен на бой, они уже примитивны |
Payload’ы событий (DamageResult, AreaHit, BattleOutcome) | плоскими полями по индексу | индексы внутри чанка локальные, склейка на приёме |
Определения (EffectData, AbilityData) | строковые id, резолв через реестр контента | лента держит ССЫЛКИ на ассеты; UnityEngine.Object по сети не едет |
Ориентир по объёму: 100-150 КБ на бой. Это следствие формата, а не свойство ленты: наивная
сериализация UnitSnapshot даёт ~1.4 МБ.
5.2 Транспортные пределы — считаем сами
FacepunchTransport отдаёт данные прямо в Connection.SendMessage(..., sendType): своей
фрагментации нет, размер не проверяется, а все reliable-варианты NetworkDelivery (включая
ReliableFragmentedSequenced) сводятся к одному SendType.Reliable. Фрагментирует Steam, до 512 КБ
на сообщение; сверх лимита возвращается InvalidParam, который транспорт не читает — чанк
уезжает в тишину.
Отсюда правила, и все три обязаны быть в коде, а не в этом документе:
- предел чанка проверяет наш код и отказывает громко. Ниже его не проверяет никто;
- рабочий размер чанка — десятки КБ, не «весь бой одним сообщением»: потеря дешевле, прогресс раздачи виден;
- присутствие идёт ненадёжным каналом и обязано влезать в MTU — фрагментированный unreliable теряется целиком.
Механика отправки — CustomMessagingManager.SendNamedMessage с записью в переиспользуемый
FastBufferWriter (named messages в NGO аллоцируют byte[] на каждую отправку). NetworkObject
ленте не нужен и не заводится.
5.3 Когда едет — ПОТОКОМ, а не «весь бой перед показом»
Исправлено 01.08.2026 (вопрос Макса: «а если бой дольше? если очень долгий?»). Прежняя формулировка «хост считает бой до конца, потом раздаёт» нереализуема, и не из-за трафика:
- окно снимков в ленте — 12 секунд (
BattleTapeRecorder.DefaultWindowTicks = (10+2)×30). Уехавший на весь бой сим вытеснит снимки первых секунд, и раздавать будет нечего. Для боёв длиннее двенадцати секунд — то есть для всех — этот путь просто не работает; - растить окно под длину боя — тупик: пять минут это ~4.5 МБ живой памяти ради данных, нужных один раз.
Как на самом деле: сим держит обычный лаг (~10 с), чанки уезжают по мере готовности, показ у гостя стартует, набрав первый. Тогда длина боя не значит ничего: битрейт постоянный (~3.3 КБ/с ≈ 26 кбит/с при двенадцати юнитах), память у гостя не растёт — снимки ложатся в такое же кольцевое окно и вытесняются показанными.
Требование пари при этом соблюдено, если читать его точно: «пока открыты ставки, лента не уходит
игрокам даже фрагментом». Ставки закрываются до старта боя; запрет был на «увидеть бой раньше, чем
закрылись ставки», а не на «раздавать по ходу». sync-model п.5 разрешает задержку перед боем — её и
занимает набор первого чанка.
Потеря чанка — повтор по номеру; разрыв больше окна — доезд снимком состояния на опорном тике.
5.4 Тесты фазы
- роундтрип:
BattleTape→ чанки →BattleTapeдаёт побайтово те же события и снимки (TapeChunkCodecTests); - чанк сверх предела — громкий отказ, а не тишина;
- chaos-транспорт: потеря, переупорядочивание и дубли чанков из сида → показ идентичен эталонному;
- неизвестный строковый id в чанке — отказ на handshake, а не падение показа.
6. Присутствие: курсоры (фаза В) — РЕАЛИЗОВАНО 01.08.2026, кроме отрисовки
Код:
Net/Presence/—PresenceState,PresenceCodec(склейка, ненадёжный канал),PresenceSender(128 Гц + dirty-check),PresenceInterpolator(Эрмит по скорости, инерция с потолком 100 мс). Тесты —PresenceTests. Почему плавность живёт на приёме — Journal - Smoothness Lives On The Receiving End.Отрисовки курсора на экране нет: это UI-слой и сцена. Всё остальное к ней готово и от неё не зависит — курсорный слой рисуется поверх и разметки экранов не ждёт.
Модель — Figma: presence broadcast’ится по тому же каналу, что и правки, но никогда не пишется в журнал и не сохраняется.
- один склеенный пакет на игрока: позиция курсора, скорость (для Hermite-интерполяции), что наведено, что держит. ~20 байт на курсор;
- 128 Гц с dirty-check — курсор стоит, пакетов ноль. Буфер 1-2 интервала (~8-16 мс), потерянный пакет экстраполируется по последней скорости;
- отправка по тику с dirty-check, не по
Update;RpcTargetUse.Tempпротив аллокаций; - HARD: ни одна механика не читает присутствие. Присутствие не идёт в лог команд, не влияет на
RunState, не сохраняется. Как только от жеста что-то зависит — он переезжает в транзакции (§10) и становится командой. Дизайн-владелец правила — Coop - Presence; - курсорный слой не ждёт ничего из UI: он рисуется поверх и не зависит от разметки экранов.
Чужое выделение видно — это и есть «мягкая заявка на объект» из дизайна, и Figma доказала, что локов для этого не нужно.
7. Сессия (фаза Г) — минимум под критерий приёмки
Настоящая цена мультиплеера по внешним свидетельствам — не синхронизация геймплея, а именно это.
У Facepunch почти всё готово: Lobby (создать/войти, RoomEnter.Success), Lobby.InviteFriend,
SteamFriends.OpenGameInviteOverlay, SteamFriends.OnGameLobbyJoinRequested (игрок жмёт «Join» в
списке друзей). Стыковка с NGO — Connection Approval (§4.3).
В вертикаль входит:
- создать лобби, пригласить друга, войти по приглашению;
- дисконнект без падения: уход гостя не роняет хост и наоборот;
- уход хоста = конец сессии (решение 2 §2): гости получают понятный экран и уносят открытия в профиль плюс запись «гостевали у гильдии такой-то» в Хронику. Авторитет никуда не передаётся;
- часы сессии: общее время нужно и для порядка интентов (п.7 §2), и для «3…2…1» при возобновлении.
После вертикали, не в ней: реконнект (снимок сессии + хвост лога — механизм заложен §4.1, но в критерий приёмки не входит), лобби-экран как часть UI-навигации (сейчас хватает Steam-оверлея), rich presence.
8. Dev-контур: чем это отлаживается
Вторая статья расходов мультиплеера по ресёрчу — отладка: «поднять два инстанса и воспроизвести баг в одиночку занимает вечность». Лечим до того, как заболит.
| Инструмент | Что даёт | Готовность |
|---|---|---|
LoopbackTransport + EditMode | вся логика шины, лога и кодека — без NGO и без сцен | пишется в фазе А |
| chaos-слой из сида | падение воспроизводится по номеру сида | пишется в фазе А |
NetcodeIntegrationTest (Unity.Netcode.TestHelpers.Runtime) | несколько NetworkManager в одном процессе, MockTimeProvider, NetcodeLogAssert | есть в NGO |
| MPPM 1.3.2 | до 4 игроков в редакторе, одни ассеты | стоит; готча — в виртуальных инстансах нельзя менять объекты |
| Network Simulator | задержка, джиттер, потеря, лаг-спайки | нужен Multiplayer Tools + UTP (фаза А) |
| Своя dev-консоль | сценарий «двое, один жмёт, второй смотрит» скриптуется командами | готова (35 команд, DevCommandRegistry) |
SimSyncProbe | статистика чек-сумм с живых машин | расконсервируется в фазе Г как dev-инструмент |
Решено моей властью: SimSyncProbe включается по dev-флагу, а не постоянно. Он собирает
данные для будущей оптимизации «вход + сид», а не участвует в раздаче боя — постоянная работа платила
бы за возможность, которой мы не пользуемся.
9. Порядок работ
| Фаза | Что | Готово, когда |
|---|---|---|
| А | лог команд забега · INetTransport + loopback + UTP-профиль + chaos · handshake контента · Multiplayer Tools в манифест | соло целиком идёт через лог команд; тест «один лог → один RunState» зелёный; chaos воспроизводит потерю по сиду |
| Б — ЗАКРЫТА 02.08 | кодек чанка ленты · раздача и склейка · провод к живому бою · релей переписан под общую паузу | роундтрип побайтово совпал; два клиента на loopback проиграли один бой одинаково |
| В | присутствие: курсоры | чужой курсор видно, механика его не читает (тест) |
| Г | сессия: Steam-транспорт, лобби, инвайт, рукопожатие, дисконнект, «хост ушёл» · свой AppId | критерий приёмки §1 на двух машинах |
Достроено 02.08.2026 (вечер): гостевая половина целиком. Сеанс, мероприятие и бой ветвятся по роли составом — ролевых проверок в рантайме не осталось (
IBattleAuthorityудалён). Гость получает забег снимком (Game/Session/Net/), где идёт игра — состоянием (ActivityState), бой — своим скоупом-приёмником. Вход прошит: приглашение → рукопожатие → меню закрывается выборомJoinCoop→GameFlow.PlayAsGuestAsyncдержит цикл, пока чужая сессия жива. Разбор — три журнала2026-08-02-the-guest-is-given-the-state-not-the-recipe,-the-role-picks-the-battle-composition-and-the-question-disappears,-the-guest-follows-the-place-and-asks-for-it-himself. Чего нет: реконнекта, сетевого гейта готовности, отрисовки курсоров и проверки вдвоём вживую.Состояние фазы Г на 02.08.2026. Написаны и компилируются:
SteamNetTransport(relay-сокет иConnectRelay),CoopHandshake(версия, отпечаток контента, выдача номера пира),SteamLobbyService(лобби friends-only, оверлей приглашений, приёмOnGameLobbyJoinRequested),CoopSessionбез единой зависимости от чужого стека, экран «Сетевая игра» (создать одним кликом, пригласить, отключиться). Не проверено вживую — нужен второй аккаунт с доступом к AppId и вторая машина.SimSyncProbeиз плана выбыл: он былNetworkBehaviourиз lockstep-эпохи и удалён вместе с NGO.
Дополнено вердиктом Макса 2026-08-01 (Planning - Road To Playable Build §5): после фазы Г заход не заканчивается, а продолжается двумя режимами-спаррингами (PvE «против пачки мобов» и PvP с приватной расстановкой, общий гейт «готов») и билдом с деплоем в Steam — вертикаль делается ради того, чтобы кооп можно было потрогать вдвоём из установленной игры.
Порядок контринтуитивен и таким выбран сознательно: бой делается не первым, хотя он самый готовый. Первыми идут швы, потому что их ретрофит стоит переписывания, а бой без транспорта всё равно некуда раздавать.
10. Отложено сознательно
- Транзакции над экранами подготовки (казна, драфт, найм, расстановка, голосование) — самый
большой кусок работы, и для «смотрим бой вместе» не нужен. Ретрофит закрыт швом §4.1: запись в
RunStateуже пойдёт через шину. Что понадобится дописать: предсказание отказа («кто-то уже смотрит эту позицию» до клика) и откат оптимистичного показа. - Миграция хоста — не пишется вовсе (решение 2 §2).
- Реконнект — механизм заложен, реализация после вертикали (§7).
- «Вход + сид» вместо снимков — возможная оптимизация трафика, гейт на неё один: чек-суммы с живых машин держатся бит-в-бит месяцами. Не держатся — мы ничего не потеряли.
- Пари, арбитры, споры, личная валюта — дизайн есть, техника ждёт транзакций.
11. Что осталось решить
Открытых вопросов к Максу нет. В работе решается по ходу и записывается в журнал:
- Размер чанка в тиках — подбирается замером на реальном бое, не назначается тут.
- Что именно квантуется в позиции (шаг сетки) — из размера арены и радиуса тел.
- Формат опорного снимка для «разрыв больше окна» — полный кадр или дельта от нуля.