Статус: ТЗ, решения приняты 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 работают под приложением с ограниченным кругом владельцев. Оттуда же три следствия, и все три надо закрыть до фазы Г, а не в ней:

  1. Тестировать может только аккаунт с доступом. Второй тестовый аккаунт заводится в Steamworks заранее (ключ или playtest-доступ) — иначе «двое на двух машинах» упрётся не в код.
  2. Steam Auto-Cloud у 3259720 надо проверить и на время тестов выключить. Облако действует по AppId и не знает, кто писал файл; наша маска — Saves/**/*.json под LocalLow/Alebardium/Guildmaster/ (Planning - Save System). Пути старой игры растут из её productName, так что пересечения быть не должно — но проверяется это один раз, а лечится потерянными сейвами.
  3. Один Steam-клиент на машине = один аккаунт, поэтому два клиента локально под Steam-транспортом не поднять вовсе, и MPPM с виртуальными игроками для этого пути не годится. Это не неудобство, а причина, по которой UTP-профиль (§4.2) обязателен: без него отладка вдвоём требует второй машины каждый раз.

Косметика, о которой стоит помнить, зовя друзей на тест: в списке друзей игра будет называться Few Seconds - Many Deaths!.


3. Инвентарь: на что опираемся

Проверено по коду 31.07-01.08.2026.

ЕстьГдеГодность
Боевая лентаCombat/Tape/BattleTape, TapeEvent, UnitSnapshot, BattleTapePlayback, BattleTapeDispatcherготова, менять не надо: события за весь бой, снимки кольцевым окном, показ с лагом и своими часами
Владелец RunStateGuild/RunStateServiceSetSlotPosition, SetSlotRelic, AddGold, RemoveRelicглаголы записи уже есть — их заворачиваем в команды, а не изобретаем заново
Поле владельца слотаRunState.SlotOwner (int[], соло — все нули)кооп-шов заложен заранее, «кого кто ведёт» уже есть куда писать
Гейт готовностиGame/Flow/RunFlowSeams.csIReadyGate + SoloReadyGate, IPlayerIntentSource.IsLocalAuthorityшов есть, тела нет — сюда встаёт сетевая реализация
Транспорт SteamNet/FacepunchTransportBootstrap.csинициализация на AppId 480, больше ничего
Релей командNet/BattleControlRelay.csпереписан 02.08: lockstep-broadcast удалён вместе с NetworkCommandRelay, осталась общая пауза как состояние показа
Проба чек-суммNet/_Parked/SimSyncProbe.csCombatSimulation.ComputeChecksum()запаркована; расконсервируется в фазе Г как dev-инструмент, ставкой не является
ДетерминизмCore/Random/IRngService, BattleBootstrap.ReseedForBattleесть; готча — CombatLifetimeScope регистрирует new XorShiftRng(0UL), спасает пересев перед боем
Реестр dev-командCore/DevConsole/DevCommandRegistry — явная регистрация, Execute(line), Unregister35 команд уже идут через один вход; дев-команды становятся хост-авторитативными бесплатно, если пройдут через шину (§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.tools 2.2.9 в манифесте. Решение и грабли — Journal - The Network Becomes Debuggable Before It Exists.

UTP-профиля пока НЕТ: третья реализация поверх NGO требует NetworkManager в сцене, то есть работы через редактор, и делается вместе с сессией (фаза Г). Loopback и chaos закрывают всё, что закрывается без сцены, — включая chaos-сценарии, ради которых Network Simulator и нужен был бы.

INetTransport с тремя реализациями:

РеализацияГде живётЗачем
LoopbackTransportEditMode-тестывсё выше интерфейса тестируется без NGO вообще
UTP-профильdev-сборки и PlayModeNetwork 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, который транспорт не читает — чанк уезжает в тишину.

Отсюда правила, и все три обязаны быть в коде, а не в этом документе:

  1. предел чанка проверяет наш код и отказывает громко. Ниже его не проверяет никто;
  2. рабочий размер чанка — десятки КБ, не «весь бой одним сообщением»: потеря дешевле, прогресс раздачи виден;
  3. присутствие идёт ненадёжным каналом и обязано влезать в 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), бой — своим скоупом-приёмником. Вход прошит: приглашение → рукопожатие → меню закрывается выбором JoinCoopGameFlow.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. Что осталось решить

Открытых вопросов к Максу нет. В работе решается по ходу и записывается в журнал:

  1. Размер чанка в тиках — подбирается замером на реальном бое, не назначается тут.
  2. Что именно квантуется в позиции (шаг сетки) — из размера арены и радиуса тел.
  3. Формат опорного снимка для «разрыв больше окна» — полный кадр или дельта от нуля.