Решили: третья реализация шва транспорта — NgoTransport поверх Netcode for GameObjects (именованные сообщения, одно имя на все каналы), плюс CoopSession: поднять хост, войти гостем, рукопожатие с отпечатком контента, «хост ушёл — конец сессии». В сцене CoreScene появился объект [Net] с NetworkManager и UnityTransport.

Почему так:

  1. Одно имя сообщения на все каналы. NGO умеет разводить потоки по строковому имени, но канал у нас уже живёт в первом байте конверта. Второй способ разводить то же самое означал бы двух владельцев одного правила и расхождение при добавлении канала.
  2. Очередь на приёме, а не прямой проброс. NGO поднимает колбэк внутри своего апдейта, а наш INetTransport обещает доставку только в Poll. Обещание держит воспроизводимость тестов, где два узла живут в одном процессе, поэтому качает транспорт отдельный NetPump.
  3. Себе не шлём. SendNamedMessageToAll у хоста включает его собственного клиента; наш контракт — «всем, кроме себя». Иначе хост получал бы эхо каждого своего чанка ленты.
  4. EnableSceneManagement = false. Сцены грузит наш SceneLoader, мир persist, бой едет лентой — синхронизация сцен NGO нам не нужна и мешала бы.

Грабли — две, обе дорогие.

NetworkManager.MaximumFragmentedMessageSize бросает NullReferenceException, пока сеть не поднята. Свойство спрашивает менеджер сообщений, а тот создаётся вместе с сессией. Поймано в редакторе при настройке [Net]: тот же вызов из нашего MaxReliableMessageBytes уронил бы стример в соло-игре, где сеть не запущена вовсе. Значение теперь снимается один раз при старте, до старта отдаётся MTU-безопасный минимум.

Предел надёжного сообщения у транспортов разный, и наш потолок чанка в него не влезает. У Steam это 512 КБ, у UTP — MaximumFragmentedMessageSize (заметно меньше), а TapeChunkFormat.MaxChunkBytes — 64 КБ. Сверх предела сообщение не уезжает вовсе и молча. Поэтому стример спрашивает предел у транспорта и, если чанк не влез, делит диапазон тиков пополам и пишет заново.

Деление потребовало TapeChunkWriter.DiscardLast: номер чанка тратится при записи, и без отката у приёмника осталась бы дыра в нумерации, которую он просил бы повторить до конца боя. То есть откат — не удобство, а условие того, что деление вообще работает.

Владелец правды: Net/Transport/NgoTransport.cs, Net/Session/CoopSession.cs, Net/NetPump.cs, Net/Tape/TapeStreamer.cs (деление под предел); тест TapeDeliveryTests.ChunkTooBigForTheTransport_IsSplit_NotDropped держит и деление, и непрерывность нумерации после него.