Решили: боевую ленту раздаём по сети чанками своего формата — события плюс сжатые снимки
(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, ещё не написаны).