Решили: транспорт живёт за INetTransport, и за ним сразу два тела — LoopbackNetwork (несколько пиров в одном процессе, без NGO и сцен) и ChaosTransport (порча канала по сиду). Отпечаток совместимости — ContentFingerprint (хеш отсортированного набора id + версия сборки + версия схемы). Пакет com.unity.multiplayer.tools 2.2.9 добавлен.

Почему свой chaos, а не Network Simulator от Unity: тот работает только с UTP, а релизный транспорт у нас Facepunch/Steam — но главное не это. Симулятор не даёт воспроизводимости: падение ловится ожиданием, пока сеть поведёт себя так же. Из сида история канала повторяется вызовом теста. Приём называется Deterministic Simulation Testing, и половина метода у нас уже стояла — детерминированное боевое ядро.

Почему MaxReliableMessageBytes торчит в интерфейсе: ниже нашего кода предел не проверяет никто (FacepunchTransport отдаёт данные прямо в Connection.SendMessage, Steam режет на 512 КБ и возвращает InvalidParam, который транспорт не читает). Loopback сознательно врёт тем же числом: шов, где в тестах проходит то, что в игре уезжает в тишину, хуже отсутствующего.

Почему FNV-1a, а не string.GetHashCode: последний рандомизирован между запусками процесса, и отпечаток на нём не совпал бы даже у одной сборки с самой собой. Набор id сортируется нами, хотя база и обещает порядок: отпечаток обязан зависеть от НАБОРА контента, иначе пересборка базы у одного из игроков разводила бы сессию на ровном месте. Разделитель между id обязателен — без него «a»+«bc» совпало бы с «ab»+«c».

Грабли:

  • Правка .asmdef не видна compile-check.ps1 до рефреша. Скрипт собирает по готовым командам редактора из Library/Bee/artifacts/*.rsp, а новую ссылку сборки генерирует Unity. Симптом — «CS0234: пространство имён не существует» при верном asmdef на диске. Лечится одним refresh_unity, но помнить надо: asmdef — ассет, и правило «ассеты через редактор» на него распространяется.
  • add_package упал первым вызовом на EPERM: rename внутри Library/PackageCache (файл держал другой процесс), манифест при этом остался чистым — половинчатого состояния не возникло. Вторая попытка прошла. Плюс сам ответ MCP пришёл таймаутом, потому что Unity в этот момент импортировал пакет: проверять результат надо по манифесту на диске, а не по ответу инструмента.

Не сделано и знаем об этом: UTP-профиль как третья реализация INetTransport поверх NGO. Он требует NetworkManager в сцене, то есть работы через редактор, и приезжает вместе с сессией. Loopback и chaos закрывают всё, что можно закрыть без сцены.

Владелец правды: Net/Transport/ целиком, Data/ContentFingerprint.cs; инварианты — тесты TransportSeamTests (доставка только в Poll, копия payload, громкий отказ сверх предела, один сид → одна история, надёжное не теряется) и ContentFingerprintTests (независимость от порядка, чувствительность к набору, границы id, отдельная проверка версий).