Решили: приглашение, принятое посреди своей игры, рвёт текущий забег штатным путём (IRunControl), а цикл игры сам видит, что сессия стала гостевой, и уходит в чужую игру, не показывая главного меню.

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

Сказано: «Нужно чтобы „присоединиться“ корректно работало у игрока где бы он не находило. Мб перед этим проигрываются действия в духе „вернуться в главное меню“, а лишь потом попытке соединения. Но мб можно и без таких костылей.» (Макс, 04.08.2026, сессия fef16360).

Грабли, две.

Первая — в самом цикле игры: на входе в виток он закрывал любую поднятую кооп-сессию (_coop.Leave()). То есть подключение, пришедшее из оверлея во время забега, цикл оборвал бы сам, вернувшись в меню, — и выглядело бы это как «приглашение не работает». Ветку «мы уже идём к кому-то» пришлось поставить ВЫШЕ этой строки.

Вторая — моя, в тесте: я написала, что хост не должен принимать гостя, умершего между «привет» и «добро пожаловать». Ожидание неверное — «привет» приходит раньше обрыва, и хост честно отвечает. Значение имеет не это, а то, что обрыв доходит следом: не дойди он, гость остался бы в составе призраком, ему слали бы курсоры, а гейт готовности ждал бы его согласия вечно — бой не начался бы уже никогда. Тест переписан на этот инвариант.

Владелец правды: Game/Services/GameFlow.cs (ветка «уже идём к кому-то»), Game/Session/CoopJoinInterrupt.cs, Game/Session/CoopDisconnectPresenter.cs, тесты HandshakeDropTests.