Решили: у транспорта появилось ПОКОЛЕНИЕ сессии. Номер растёт на каждом Shutdown, сокет запоминает тот, при котором родился, и события чужого поколения не попадают даже во входящую очередь. Вторым рубежом состав сеанса больше не вычёркивает себя по «пир отключился».

Почему: Steam досказывает за закрытым соединением — OnDisconnected приходит не в момент закрытия, а на ближайшем RunCallbacks, когда на месте старого сокета уже подняли новый. Отличить такое эхо от настоящего события было нечем, и оно выходило наружу как факт про живую сессию.

Отвергли снятие Owner у закрываемого сокета: оно лечит только случай, когда объект соединения новый, и молчит, когда Facepunch переиспользует прежний. Поколение отвечает на настоящий вопрос — «из какой сессии это событие» — и отвечает одинаково для подключения, отключения и сообщения.

Грабли: один дефект дал два симптома, которые на прогоне выглядели как разные баги, и второй из них уводил в сторону убедительнее первого.

Первый: игрок побывал гостем, поднял свой хостинг и получил эхо «пир 0 отключился». Номер один, смысл разный — у гостя ноль означал чужого хозяина, у хозяина ноль означает его самого. Состав вычеркнул хозяина, и сеанс остался без него: «нас нет в составе сеанса (наш номер 0, участников 1)». Дальше сыпалось всё, что читает состав, — стороны, панель участников, права на кнопки.

Второй: подключение к чужому лобби падало с «Хост не ответил», хотя хост отвечал исправно. То же эхо, только в состоянии Connecting. В логе рядом стояла строка [GameFlow] запрос «В главное меню», и на неё легко списать разрыв — но Shutdown чистит очередь входящих и наружу не поднимает ничего, поэтому событие могло прийти только колбэком за уже закрытым соединением. Строка GameFlow оказалась соседом по времени: игрок нажал кнопку на диалоге потерянной связи.

Отсюда же вторая мораль: текст ошибки назначается по состоянию, а не по причине. «Хост не ответил» — это Connecting плюс разрыв, а не диагноз. Игрок читает диагноз.

Владелец правды: SteamNetTransport._generation, HostSessionRoster.OnPeerDisconnected, тест StaleSessionEchoTests.