Решили: сеанс владения состоянием рождается ВХОДОМ В РЕЖИМ, а не бутом. GameBootstrap поднимает
только мир; сеанс открывает выбор в меню («Создать» → владелец, «Присоединиться» → гость). Верхний
цикл закрывает сеанс перед каждым показом главного меню.
Почему изменили своё же решение того же дня: утром сеанс открывался на буте — ровно потому, что меню спрашивает «есть ли сейв», и держатель состояния был нужен раньше первого узла. Макс описал модель входа (две кнопки, три режима, галочка онлайн-лобби), и стало видно, что это подпорка: роль и решение про лобби известны в момент выбора режима, а не за минуту до него. Открывать сеанс раньше значит подставлять роль по умолчанию и надеяться, что её потом переоткроют.
Что развело узел: «где лежит забег» вынесено в RunSaves — знание об этом спрашивают ДВОЕ из
разных жизней (меню вне сеанса и RunStateService внутри), и разойтись им нельзя. Меню теперь
спрашивает диск и активную гильдию, а не объект в памяти.
Дев-разрезы заводят сеанс сами (RequireRun): разрез — это тот же вход в игру, только без выбора.
Без этого флаги на буте и bones со сплеша упирались бы в «сеанса нет».
Тест поймал смену контракта раньше прогона: ActivityLifeCycleTest открывал мероприятие напрямую,
а мероприятие рождается внутри сеанса. Пришлось привести тест к реальности — он входит тем же путём,
что игрок. Ровно для этого он и писался сутки назад.
Осталось: само меню на две кнопки и три режима (UXML, локализация, галочка лобби) — отдельный UI-заход; гостевой сеанс — за кооп-вертикалью.
Владелец правды: Game/Services/GameFlow.RunGameAsync, Guild/RunSaves.cs,
Game/Session/SessionHost.cs.