Решили: боевую ленту раздаём по сети чанками своего формата — события плюс сжатые снимки (append-only, тиковый штамп). Клиент не пересчитывает бой ничем: исход и картинку считает хост. Закрывает открытый тех-вопрос tech-debt §4.1.

Почему: альтернатива «вход + сид, клиент крутит бой сам» даёт почти нулевой трафик и бесплатные снимки, но опирается на бит-в-бит совпадение float между машинами, чего JIT не гарантирует (выбор инструкций зависит от CPU игрока — Фидлер, «Deterministic Lockstep»). Цена расхождения тут коварнее lockstep-а: правила не ломаются — исход всё равно объявляет хост событием BattleEnded, — поэтому разъехавшаяся картинка молчит, вместо того чтобы упасть. Третий вариант, «реплицировать состояние юнитов каждый тик через NGO» (как предполагало решение 19.06), отвергнут как самый дорогой по трафику и уже не нужный: гарант 28.07 «игрок на бой не влияет» удешевил задачу задним числом.

Сказано: вердикт снят выбором варианта «События + сжатые снимки чанками» (01.08.2026, сессия 9e6eef1b). Формулировка варианта моя, не его — дословной цитаты за этим решением нет, и выдавать свою за его нельзя.

Грабли: прикидка трафика от 31.07 («сотня-две килобайт на бой, события сериализуются тривиально») была занижена на порядок, потому что считала только _events и не считала снимки. Показ рисует кадр не из событий, а из UnitSnapshot через BattleTape.TryGetFrame; у хоста снимки живут кольцевым ОКНОМ вокруг момента показа, истории боя в них нет вовсе, а клиенту нужен снимок каждого тика. Наивно это ~100 байт на юнита на тик — около 1.4 МБ за сорокасекундный бой на двенадцать юнитов.

Цифра спасается форматом, а не свойствами ленты, и формат обязан знать три вещи:

  • непрерывно течёт только позиция. PreviousPosition по сети не едет вообще — это позиция предыдущего тика, у приёмника она уже есть;
  • HP, фазы, кулдауны, теги меняются редко — dirty-маска и дельта к прошлому тику вместо полного снимка. С квантованной позицией это ~100-150 КБ на бой;
  • лента держит ССЫЛКИ на ассеты_effectDefs, _abilityDefs списками EffectData/AbilityData. Ссылка на UnityEngine.Object по сети не едет: в чанке это строковые id, на приёме резолв через реестр контента. Отсюда handshake отпечатка контента становится обязательным, а не страховкой: неизвестный id на приёме роняет показ, а не «слегка расходит картинку».

Вторые грабли — транспортные. Доки UnityTransport про «reliable payloads вправе превышать Max Payload Size» к нашему релизному транспорту не применимы: FacepunchTransport кладёт data прямо в Connection.SendMessage(..., sendType), своей фрагментации не имеет и размер не проверяет, а все reliable-варианты NetworkDelivery (включая ReliableFragmentedSequenced) сводит к одному SendType.Reliable. Фрагментирует Steam, до 512 КБ на сообщение (k_cbMaxSteamNetworkingSocketsMessageSizeSend); сверх лимита он возвращает InvalidParam, который транспорт не читает — то есть слишком большой чанк уезжает в тишину. Предел чанка обязан проверять наш код: ниже его не проверяет никто.

Владелец правды: Combat/Tape/BattleTape.cs, Combat/Tape/UnitSnapshot.cs, ТЗ Planning - Coop Vertical §5; лимит чанка и роундтрип формата — тесты вертикали (TapeChunkCodecTests, ещё не написаны).