Решили: кооп вошёл в боевой скоуп. Роль узла (Solo / Host / Guest) выведена из транспорта
за швом IBattleAuthority и спрашивается каждый кадр, а не решается регистрацией: боевой скоуп
поднимается на буте, в главном меню, когда сети ещё нет. Регистрируются обе половины раздачи у
всех; расходятся узлы в рантайме. У гостя CombatLoopService не тикает симуляцию вовсе и не
снимает кадров — он держит только требуемое отставание показа, а ленту наполняет TapeIntake.
Почему так, а не «хосту стример, гостю приёмник» при сборке контейнера: в момент Configure
роли не существует. Единственная альтернатива — пересоздавать боевой скоуп на входе в сессию, но
он персистентный по конституции persist-мира (бой начинается командой в живую симуляцию, а не
загрузкой сцены), и снос скоупа означал бы снос мира.
Пауза переехала на общий путь. Интент уходит в BattleControlRelay (владелец флага), применяет
его NetPauseBridge через TimeScaleService. BattleInputController больше не трогает время сам и
читает состояние у релея, а не у времени: интент, посчитанный от чужого состояния, оказывается «уже
в этом состоянии» и теряется молча. На смене фазы боя сбрасываются оба владельца сразу.
Состав боя поехал отдельным каналом (BattleRoster, номер 7). В снимках его нет намеренно —
определение юнита, команда и арт за бой не меняются, а слать их тридцать раз в секунду значит
платить за неизменное. В соло реестр наполняет событие спавна; у гостя спавнов нет, и без
объявления лента доезжала бы, а рисовать было бы нечего.
Отставание у гостя постоянное, без оглядки на фазу. Лента гостя пополняется тогда и только
тогда, когда у хоста идёт бой: вне боя фронт хостовой ленты стоит на месте (кадр перезаписывает
один и тот же тик), и чанки не уходят. Значит растущая лента и есть «бой идёт». Лаг тот же, что у
хоста: хост показывает фронт − окно, и гость, идущий вплотную к своему фронту, увидел бы бой на
десять секунд раньше напарника.
Грабли: распознавание канала в NetEnvelope было рукописным списком рядом с перечислением.
Новый канал завели, в список не дописали — сообщения уходили и молча отбрасывались на приёме;
симптом «у гостя не появились юниты», причина за два файла оттуда. Список заменён таблицей, которая
строится из самого перечисления, и накрыт тестом на все каналы разом.
Вторая: RuntimeUnit.Id назначает фабрика, а не симуляция. Юнит, собранный в тесте руками, приходит
с нулевым id — два таких юнита дают одну запись в реестре на двоих, и падает это не как «паспорт не
доехал», а как «команда не та».
Владелец правды: Core/Net/IBattleAuthority.cs, Net/NetBattleAuthority.cs,
Net/Tape/BattleRosterRelay.cs, Game/Services/NetPauseBridge.cs, Game/CombatLifetimeScope.cs
(RegisterCoop). Тесты CoopBattleWiringTests (хост считает и раздаёт, гость приходит к тому же
исходу; соло и гость не раздают), NetPauseBridgeTests (один путь применения паузы),
NetEnvelopeTests (каждый объявленный канал узнаётся конвертом).