bauiv1lib.mainmenu

Implements the main menu window.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Implements the main menu window."""
   4# pylint: disable=too-many-lines
   5
   6from __future__ import annotations
   7
   8from typing import TYPE_CHECKING
   9import logging
  10
  11import bauiv1 as bui
  12import bascenev1 as bs
  13
  14if TYPE_CHECKING:
  15    from typing import Any, Callable
  16
  17
  18class MainMenuWindow(bui.Window):
  19    """The main menu window, both in-game and in the main menu session."""
  20
  21    def __init__(self, transition: str | None = 'in_right'):
  22        # pylint: disable=cyclic-import
  23        import threading
  24        from bascenev1lib.mainmenu import MainMenuSession
  25
  26        plus = bui.app.plus
  27        assert plus is not None
  28
  29        self._in_game = not isinstance(
  30            bs.get_foreground_host_session(),
  31            MainMenuSession,
  32        )
  33
  34        # Preload some modules we use in a background thread so we won't
  35        # have a visual hitch when the user taps them.
  36        threading.Thread(target=self._preload_modules).start()
  37
  38        if not self._in_game:
  39            bui.set_analytics_screen('Main Menu')
  40            self._show_remote_app_info_on_first_launch()
  41
  42        # Make a vanilla container; we'll modify it to our needs in
  43        # refresh.
  44        super().__init__(
  45            root_widget=bui.containerwidget(
  46                transition=transition,
  47                toolbar_visibility=(
  48                    'menu_minimal_no_back'
  49                    if self._in_game
  50                    else 'menu_minimal_no_back'
  51                ),
  52            )
  53        )
  54
  55        # Grab this stuff in case it changes.
  56        self._is_demo = bui.app.env.demo
  57        self._is_arcade = bui.app.env.arcade
  58
  59        self._tdelay = 0.0
  60        self._t_delay_inc = 0.02
  61        self._t_delay_play = 1.7
  62        self._p_index = 0
  63        self._use_autoselect = True
  64        self._button_width = 200.0
  65        self._button_height = 45.0
  66        self._width = 100.0
  67        self._height = 100.0
  68        self._demo_menu_button: bui.Widget | None = None
  69        self._gather_button: bui.Widget | None = None
  70        self._start_button: bui.Widget | None = None
  71        self._watch_button: bui.Widget | None = None
  72        self._account_button: bui.Widget | None = None
  73        self._how_to_play_button: bui.Widget | None = None
  74        self._credits_button: bui.Widget | None = None
  75        self._settings_button: bui.Widget | None = None
  76        self._next_refresh_allow_time = 0.0
  77
  78        self._store_char_tex = self._get_store_char_tex()
  79
  80        self._refresh()
  81        self._restore_state()
  82
  83        # Keep an eye on a few things and refresh if they change.
  84        self._account_state = plus.get_v1_account_state()
  85        self._account_state_num = plus.get_v1_account_state_num()
  86        self._account_type = (
  87            plus.get_v1_account_type()
  88            if self._account_state == 'signed_in'
  89            else None
  90        )
  91        self._refresh_timer = bui.AppTimer(
  92            0.27, bui.WeakCall(self._check_refresh), repeat=True
  93        )
  94
  95    @staticmethod
  96    def _preload_modules() -> None:
  97        """Preload modules we use; avoids hitches (called in bg thread)."""
  98        import bauiv1lib.getremote as _unused
  99        import bauiv1lib.confirm as _unused2
 100        import bauiv1lib.store.button as _unused3
 101        import bauiv1lib.kiosk as _unused4
 102        import bauiv1lib.account.settings as _unused5
 103        import bauiv1lib.store.browser as _unused6
 104        import bauiv1lib.creditslist as _unused7
 105        import bauiv1lib.helpui as _unused8
 106        import bauiv1lib.settings.allsettings as _unused9
 107        import bauiv1lib.gather as _unused10
 108        import bauiv1lib.watch as _unused11
 109        import bauiv1lib.play as _unused12
 110
 111    def _show_remote_app_info_on_first_launch(self) -> None:
 112        app = bui.app
 113        assert app.classic is not None
 114        # The first time the non-in-game menu pops up, we might wanna show
 115        # a 'get-remote-app' dialog in front of it.
 116        if app.classic.first_main_menu:
 117            app.classic.first_main_menu = False
 118            try:
 119                force_test = False
 120                bs.get_local_active_input_devices_count()
 121                if (
 122                    (app.env.tv or app.classic.platform == 'mac')
 123                    and bui.app.config.get('launchCount', 0) <= 1
 124                ) or force_test:
 125
 126                    def _check_show_bs_remote_window() -> None:
 127                        try:
 128                            from bauiv1lib.getremote import GetBSRemoteWindow
 129
 130                            bui.getsound('swish').play()
 131                            GetBSRemoteWindow()
 132                        except Exception:
 133                            logging.exception(
 134                                'Error showing get-remote window.'
 135                            )
 136
 137                    bui.apptimer(2.5, _check_show_bs_remote_window)
 138            except Exception:
 139                logging.exception('Error showing get-remote-app info.')
 140
 141    def _get_store_char_tex(self) -> str:
 142        plus = bui.app.plus
 143        assert plus is not None
 144        return (
 145            'storeCharacterXmas'
 146            if plus.get_v1_account_misc_read_val('xmas', False)
 147            else (
 148                'storeCharacterEaster'
 149                if plus.get_v1_account_misc_read_val('easter', False)
 150                else 'storeCharacter'
 151            )
 152        )
 153
 154    def _check_refresh(self) -> None:
 155        plus = bui.app.plus
 156        assert plus is not None
 157
 158        if not self._root_widget:
 159            return
 160
 161        now = bui.apptime()
 162        if now < self._next_refresh_allow_time:
 163            return
 164
 165        # Don't refresh for the first few seconds the game is up so we
 166        # don't interrupt the transition in.
 167
 168        # bui.app.main_menu_window_refresh_check_count += 1
 169        # if bui.app.main_menu_window_refresh_check_count < 4:
 170        #     return
 171
 172        store_char_tex = self._get_store_char_tex()
 173        account_state_num = plus.get_v1_account_state_num()
 174        if (
 175            account_state_num != self._account_state_num
 176            or store_char_tex != self._store_char_tex
 177        ):
 178            self._store_char_tex = store_char_tex
 179            self._account_state_num = account_state_num
 180            account_state = self._account_state = plus.get_v1_account_state()
 181            self._account_type = (
 182                plus.get_v1_account_type()
 183                if account_state == 'signed_in'
 184                else None
 185            )
 186            self._save_state()
 187            self._refresh()
 188            self._restore_state()
 189
 190    def get_play_button(self) -> bui.Widget | None:
 191        """Return the play button."""
 192        return self._start_button
 193
 194    def _refresh(self) -> None:
 195        # pylint: disable=too-many-branches
 196        # pylint: disable=too-many-locals
 197        # pylint: disable=too-many-statements
 198        from bauiv1lib.store.button import StoreButton
 199
 200        plus = bui.app.plus
 201        assert plus is not None
 202
 203        # Clear everything that was there.
 204        children = self._root_widget.get_children()
 205        for child in children:
 206            child.delete()
 207
 208        self._tdelay = 0.0
 209        self._t_delay_inc = 0.0
 210        self._t_delay_play = 0.0
 211        self._button_width = 200.0
 212        self._button_height = 45.0
 213
 214        self._r = 'mainMenu'
 215
 216        app = bui.app
 217        assert app.classic is not None
 218        self._have_quit_button = app.ui_v1.uiscale is bui.UIScale.LARGE or (
 219            app.classic.platform == 'windows'
 220            and app.classic.subplatform == 'oculus'
 221        )
 222
 223        self._have_store_button = not self._in_game
 224
 225        self._have_settings_button = (
 226            not self._in_game or not app.ui_v1.use_toolbars
 227        ) and not (self._is_demo or self._is_arcade)
 228
 229        self._input_device = input_device = bs.get_ui_input_device()
 230
 231        # Are we connected to a local player?
 232        self._input_player = input_device.player if input_device else None
 233
 234        # Are we connected to a remote player?.
 235        self._connected_to_remote_player = (
 236            input_device.is_attached_to_player()
 237            if (input_device and self._input_player is None)
 238            else False
 239        )
 240
 241        positions: list[tuple[float, float, float]] = []
 242        self._p_index = 0
 243
 244        if self._in_game:
 245            h, v, scale = self._refresh_in_game(positions)
 246        else:
 247            h, v, scale = self._refresh_not_in_game(positions)
 248
 249        if self._have_settings_button:
 250            h, v, scale = positions[self._p_index]
 251            self._p_index += 1
 252            self._settings_button = bui.buttonwidget(
 253                parent=self._root_widget,
 254                position=(h - self._button_width * 0.5 * scale, v),
 255                size=(self._button_width, self._button_height),
 256                scale=scale,
 257                autoselect=self._use_autoselect,
 258                label=bui.Lstr(resource=f'{self._r}.settingsText'),
 259                transition_delay=self._tdelay,
 260                on_activate_call=self._settings,
 261            )
 262
 263        # Scattered eggs on easter.
 264        if (
 265            plus.get_v1_account_misc_read_val('easter', False)
 266            and not self._in_game
 267        ):
 268            icon_size = 34
 269            bui.imagewidget(
 270                parent=self._root_widget,
 271                position=(
 272                    h - icon_size * 0.5 - 15,
 273                    v + self._button_height * scale - icon_size * 0.24 + 1.5,
 274                ),
 275                transition_delay=self._tdelay,
 276                size=(icon_size, icon_size),
 277                texture=bui.gettexture('egg3'),
 278                tilt_scale=0.0,
 279            )
 280
 281        self._tdelay += self._t_delay_inc
 282
 283        if self._in_game:
 284            h, v, scale = positions[self._p_index]
 285            self._p_index += 1
 286
 287            # If we're in a replay, we have a 'Leave Replay' button.
 288            if bs.is_in_replay():
 289                bui.buttonwidget(
 290                    parent=self._root_widget,
 291                    position=(h - self._button_width * 0.5 * scale, v),
 292                    scale=scale,
 293                    size=(self._button_width, self._button_height),
 294                    autoselect=self._use_autoselect,
 295                    label=bui.Lstr(resource='replayEndText'),
 296                    on_activate_call=self._confirm_end_replay,
 297                )
 298            elif bs.get_foreground_host_session() is not None:
 299                bui.buttonwidget(
 300                    parent=self._root_widget,
 301                    position=(h - self._button_width * 0.5 * scale, v),
 302                    scale=scale,
 303                    size=(self._button_width, self._button_height),
 304                    autoselect=self._use_autoselect,
 305                    label=bui.Lstr(
 306                        resource=self._r
 307                        + (
 308                            '.endTestText'
 309                            if self._is_benchmark()
 310                            else '.endGameText'
 311                        )
 312                    ),
 313                    on_activate_call=(
 314                        self._confirm_end_test
 315                        if self._is_benchmark()
 316                        else self._confirm_end_game
 317                    ),
 318                )
 319            else:
 320                # Assume we're in a client-session.
 321                bui.buttonwidget(
 322                    parent=self._root_widget,
 323                    position=(h - self._button_width * 0.5 * scale, v),
 324                    scale=scale,
 325                    size=(self._button_width, self._button_height),
 326                    autoselect=self._use_autoselect,
 327                    label=bui.Lstr(resource=f'{self._r}.leavePartyText'),
 328                    on_activate_call=self._confirm_leave_party,
 329                )
 330
 331        self._store_button: bui.Widget | None
 332        if self._have_store_button:
 333            this_b_width = self._button_width
 334            h, v, scale = positions[self._p_index]
 335            self._p_index += 1
 336
 337            sbtn = self._store_button_instance = StoreButton(
 338                parent=self._root_widget,
 339                position=(h - this_b_width * 0.5 * scale, v),
 340                size=(this_b_width, self._button_height),
 341                scale=scale,
 342                on_activate_call=bui.WeakCall(self._on_store_pressed),
 343                sale_scale=1.3,
 344                transition_delay=self._tdelay,
 345            )
 346            self._store_button = store_button = sbtn.get_button()
 347            assert bui.app.classic is not None
 348            uiscale = bui.app.ui_v1.uiscale
 349            icon_size = (
 350                55
 351                if uiscale is bui.UIScale.SMALL
 352                else 55 if uiscale is bui.UIScale.MEDIUM else 70
 353            )
 354            bui.imagewidget(
 355                parent=self._root_widget,
 356                position=(
 357                    h - icon_size * 0.5,
 358                    v + self._button_height * scale - icon_size * 0.23,
 359                ),
 360                transition_delay=self._tdelay,
 361                size=(icon_size, icon_size),
 362                texture=bui.gettexture(self._store_char_tex),
 363                tilt_scale=0.0,
 364                draw_controller=store_button,
 365            )
 366            self._tdelay += self._t_delay_inc
 367        else:
 368            self._store_button = None
 369
 370        self._quit_button: bui.Widget | None
 371        if not self._in_game and self._have_quit_button:
 372            h, v, scale = positions[self._p_index]
 373            self._p_index += 1
 374            self._quit_button = quit_button = bui.buttonwidget(
 375                parent=self._root_widget,
 376                autoselect=self._use_autoselect,
 377                position=(h - self._button_width * 0.5 * scale, v),
 378                size=(self._button_width, self._button_height),
 379                scale=scale,
 380                label=bui.Lstr(
 381                    resource=self._r
 382                    + (
 383                        '.quitText'
 384                        if 'Mac' in app.classic.legacy_user_agent_string
 385                        else '.exitGameText'
 386                    )
 387                ),
 388                on_activate_call=self._quit,
 389                transition_delay=self._tdelay,
 390            )
 391
 392            # Scattered eggs on easter.
 393            if plus.get_v1_account_misc_read_val('easter', False):
 394                icon_size = 30
 395                bui.imagewidget(
 396                    parent=self._root_widget,
 397                    position=(
 398                        h - icon_size * 0.5 + 25,
 399                        v
 400                        + self._button_height * scale
 401                        - icon_size * 0.24
 402                        + 1.5,
 403                    ),
 404                    transition_delay=self._tdelay,
 405                    size=(icon_size, icon_size),
 406                    texture=bui.gettexture('egg1'),
 407                    tilt_scale=0.0,
 408                )
 409
 410            bui.containerwidget(
 411                edit=self._root_widget, cancel_button=quit_button
 412            )
 413            self._tdelay += self._t_delay_inc
 414        else:
 415            self._quit_button = None
 416
 417            # If we're not in-game, have no quit button, and this is
 418            # android, we want back presses to quit our activity.
 419            if (
 420                not self._in_game
 421                and not self._have_quit_button
 422                and app.classic.platform == 'android'
 423            ):
 424
 425                def _do_quit() -> None:
 426                    bui.quit(confirm=True, quit_type=bui.QuitType.BACK)
 427
 428                bui.containerwidget(
 429                    edit=self._root_widget, on_cancel_call=_do_quit
 430                )
 431
 432        # Add speed-up/slow-down buttons for replays. Ideally this
 433        # should be part of a fading-out playback bar like most media
 434        # players but this works for now.
 435        if bs.is_in_replay():
 436            b_size = 50.0
 437            b_buffer_1 = 50.0
 438            b_buffer_2 = 10.0
 439            t_scale = 0.75
 440            assert bui.app.classic is not None
 441            uiscale = bui.app.ui_v1.uiscale
 442            if uiscale is bui.UIScale.SMALL:
 443                b_size *= 0.6
 444                b_buffer_1 *= 0.8
 445                b_buffer_2 *= 1.0
 446                v_offs = -40
 447                t_scale = 0.5
 448            elif uiscale is bui.UIScale.MEDIUM:
 449                v_offs = -70
 450            else:
 451                v_offs = -100
 452            self._replay_speed_text = bui.textwidget(
 453                parent=self._root_widget,
 454                text=bui.Lstr(
 455                    resource='watchWindow.playbackSpeedText',
 456                    subs=[('${SPEED}', str(1.23))],
 457                ),
 458                position=(h, v + v_offs + 15 * t_scale),
 459                h_align='center',
 460                v_align='center',
 461                size=(0, 0),
 462                scale=t_scale,
 463            )
 464
 465            # Update to current value.
 466            self._change_replay_speed(0)
 467
 468            # Keep updating in a timer in case it gets changed elsewhere.
 469            self._change_replay_speed_timer = bui.AppTimer(
 470                0.25, bui.WeakCall(self._change_replay_speed, 0), repeat=True
 471            )
 472            btn = bui.buttonwidget(
 473                parent=self._root_widget,
 474                position=(
 475                    h - b_size - b_buffer_1,
 476                    v - b_size - b_buffer_2 + v_offs,
 477                ),
 478                button_type='square',
 479                size=(b_size, b_size),
 480                label='',
 481                autoselect=True,
 482                on_activate_call=bui.Call(self._change_replay_speed, -1),
 483            )
 484            bui.textwidget(
 485                parent=self._root_widget,
 486                draw_controller=btn,
 487                text='-',
 488                position=(
 489                    h - b_size * 0.5 - b_buffer_1,
 490                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 491                ),
 492                h_align='center',
 493                v_align='center',
 494                size=(0, 0),
 495                scale=3.0 * t_scale,
 496            )
 497            btn = bui.buttonwidget(
 498                parent=self._root_widget,
 499                position=(h + b_buffer_1, v - b_size - b_buffer_2 + v_offs),
 500                button_type='square',
 501                size=(b_size, b_size),
 502                label='',
 503                autoselect=True,
 504                on_activate_call=bui.Call(self._change_replay_speed, 1),
 505            )
 506            bui.textwidget(
 507                parent=self._root_widget,
 508                draw_controller=btn,
 509                text='+',
 510                position=(
 511                    h + b_size * 0.5 + b_buffer_1,
 512                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 513                ),
 514                h_align='center',
 515                v_align='center',
 516                size=(0, 0),
 517                scale=3.0 * t_scale,
 518            )
 519            self._pause_resume_button = btn = bui.buttonwidget(
 520                parent=self._root_widget,
 521                position=(h - b_size * 0.5, v - b_size - b_buffer_2 + v_offs),
 522                button_type='square',
 523                size=(b_size, b_size),
 524                label=bui.charstr(
 525                    bui.SpecialChar.PLAY_BUTTON
 526                    if bs.is_replay_paused()
 527                    else bui.SpecialChar.PAUSE_BUTTON
 528                ),
 529                autoselect=True,
 530                on_activate_call=bui.Call(self._pause_or_resume_replay),
 531            )
 532            btn = bui.buttonwidget(
 533                parent=self._root_widget,
 534                position=(
 535                    h - b_size * 1.5 - b_buffer_1 * 2,
 536                    v - b_size - b_buffer_2 + v_offs,
 537                ),
 538                button_type='square',
 539                size=(b_size, b_size),
 540                label='',
 541                autoselect=True,
 542                on_activate_call=bui.WeakCall(self._rewind_replay),
 543            )
 544            bui.textwidget(
 545                parent=self._root_widget,
 546                draw_controller=btn,
 547                # text='<<',
 548                text=bui.charstr(bui.SpecialChar.REWIND_BUTTON),
 549                position=(
 550                    h - b_size - b_buffer_1 * 2,
 551                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 552                ),
 553                h_align='center',
 554                v_align='center',
 555                size=(0, 0),
 556                scale=2.0 * t_scale,
 557            )
 558            btn = bui.buttonwidget(
 559                parent=self._root_widget,
 560                position=(
 561                    h + b_size * 0.5 + b_buffer_1 * 2,
 562                    v - b_size - b_buffer_2 + v_offs,
 563                ),
 564                button_type='square',
 565                size=(b_size, b_size),
 566                label='',
 567                autoselect=True,
 568                on_activate_call=bui.WeakCall(self._forward_replay),
 569            )
 570            bui.textwidget(
 571                parent=self._root_widget,
 572                draw_controller=btn,
 573                # text='>>',
 574                text=bui.charstr(bui.SpecialChar.FAST_FORWARD_BUTTON),
 575                position=(
 576                    h + b_size + b_buffer_1 * 2,
 577                    v - b_size * 0.5 - b_buffer_2 + 5 * t_scale + v_offs,
 578                ),
 579                h_align='center',
 580                v_align='center',
 581                size=(0, 0),
 582                scale=2.0 * t_scale,
 583            )
 584
 585    def _rewind_replay(self) -> None:
 586        bs.seek_replay(-2 * pow(2, bs.get_replay_speed_exponent()))
 587
 588    def _forward_replay(self) -> None:
 589        bs.seek_replay(2 * pow(2, bs.get_replay_speed_exponent()))
 590
 591    def _refresh_not_in_game(
 592        self, positions: list[tuple[float, float, float]]
 593    ) -> tuple[float, float, float]:
 594        # pylint: disable=too-many-branches
 595        # pylint: disable=too-many-locals
 596        # pylint: disable=too-many-statements
 597        plus = bui.app.plus
 598        assert plus is not None
 599
 600        assert bui.app.classic is not None
 601        if not bui.app.classic.did_menu_intro:
 602            self._tdelay = 2.0
 603            self._t_delay_inc = 0.02
 604            self._t_delay_play = 1.7
 605
 606            def _set_allow_time() -> None:
 607                self._next_refresh_allow_time = bui.apptime() + 2.5
 608
 609            # Slight hack: widget transitions currently only progress
 610            # when frames are being drawn, but this tends to get called
 611            # before frame drawing even starts, meaning we don't know
 612            # exactly how long we should wait before refreshing to avoid
 613            # interrupting the transition. To make things a bit better,
 614            # let's do a redundant set of the time in a deferred call
 615            # which hopefully happens closer to actual frame draw times.
 616            _set_allow_time()
 617            bui.pushcall(_set_allow_time)
 618
 619            bui.app.classic.did_menu_intro = True
 620        self._width = 400.0
 621        self._height = 200.0
 622        enable_account_button = True
 623        account_type_name: str | bui.Lstr
 624        if plus.get_v1_account_state() == 'signed_in':
 625            account_type_name = plus.get_v1_account_display_string()
 626            account_type_icon = None
 627            account_textcolor = (1.0, 1.0, 1.0)
 628        else:
 629            account_type_name = bui.Lstr(
 630                resource='notSignedInText',
 631                fallback_resource='accountSettingsWindow.titleText',
 632            )
 633            account_type_icon = None
 634            account_textcolor = (1.0, 0.2, 0.2)
 635        account_type_icon_color = (1.0, 1.0, 1.0)
 636        account_type_call = self._show_account_window
 637        account_type_enable_button_sound = True
 638        b_count = 3  # play, help, credits
 639        if self._have_settings_button:
 640            b_count += 1
 641        if enable_account_button:
 642            b_count += 1
 643        if self._have_quit_button:
 644            b_count += 1
 645        if self._have_store_button:
 646            b_count += 1
 647        uiscale = bui.app.ui_v1.uiscale
 648        if uiscale is bui.UIScale.SMALL:
 649            root_widget_scale = 1.6
 650            play_button_width = self._button_width * 0.65
 651            play_button_height = self._button_height * 1.1
 652            small_button_scale = 0.51 if b_count > 6 else 0.63
 653            button_y_offs = -20.0
 654            button_y_offs2 = -60.0
 655            self._button_height *= 1.3
 656            button_spacing = 1.04
 657        elif uiscale is bui.UIScale.MEDIUM:
 658            root_widget_scale = 1.3
 659            play_button_width = self._button_width * 0.65
 660            play_button_height = self._button_height * 1.1
 661            small_button_scale = 0.6
 662            button_y_offs = -55.0
 663            button_y_offs2 = -75.0
 664            self._button_height *= 1.25
 665            button_spacing = 1.1
 666        else:
 667            root_widget_scale = 1.0
 668            play_button_width = self._button_width * 0.65
 669            play_button_height = self._button_height * 1.1
 670            small_button_scale = 0.75
 671            button_y_offs = -80.0
 672            button_y_offs2 = -100.0
 673            self._button_height *= 1.2
 674            button_spacing = 1.1
 675        spc = self._button_width * small_button_scale * button_spacing
 676        bui.containerwidget(
 677            edit=self._root_widget,
 678            size=(self._width, self._height),
 679            background=False,
 680            scale=root_widget_scale,
 681        )
 682        assert not positions
 683        positions.append((self._width * 0.5, button_y_offs, 1.7))
 684        x_offs = self._width * 0.5 - (spc * (b_count - 1) * 0.5) + (spc * 0.5)
 685        for i in range(b_count - 1):
 686            positions.append(
 687                (
 688                    x_offs + spc * i - 1.0,
 689                    button_y_offs + button_y_offs2,
 690                    small_button_scale,
 691                )
 692            )
 693        # In kiosk mode, provide a button to get back to the kiosk menu.
 694        if bui.app.env.demo or bui.app.env.arcade:
 695            h, v, scale = positions[self._p_index]
 696            this_b_width = self._button_width * 0.4 * scale
 697            demo_menu_delay = (
 698                0.0
 699                if self._t_delay_play == 0.0
 700                else max(0, self._t_delay_play + 0.1)
 701            )
 702            self._demo_menu_button = bui.buttonwidget(
 703                parent=self._root_widget,
 704                position=(self._width * 0.5 - this_b_width * 0.5, v + 90),
 705                size=(this_b_width, 45),
 706                autoselect=True,
 707                color=(0.45, 0.55, 0.45),
 708                textcolor=(0.7, 0.8, 0.7),
 709                label=bui.Lstr(
 710                    resource=(
 711                        'modeArcadeText'
 712                        if bui.app.env.arcade
 713                        else 'modeDemoText'
 714                    )
 715                ),
 716                transition_delay=demo_menu_delay,
 717                on_activate_call=self._demo_menu_press,
 718            )
 719        else:
 720            self._demo_menu_button = None
 721        uiscale = bui.app.ui_v1.uiscale
 722        foof = (
 723            -1
 724            if uiscale is bui.UIScale.SMALL
 725            else 1 if uiscale is bui.UIScale.MEDIUM else 3
 726        )
 727        h, v, scale = positions[self._p_index]
 728        v = v + foof
 729        gather_delay = (
 730            0.0
 731            if self._t_delay_play == 0.0
 732            else max(0.0, self._t_delay_play + 0.1)
 733        )
 734        assert play_button_width is not None
 735        assert play_button_height is not None
 736        this_h = h - play_button_width * 0.5 * scale - 40 * scale
 737        this_b_width = self._button_width * 0.25 * scale
 738        this_b_height = self._button_height * 0.82 * scale
 739        self._gather_button = btn = bui.buttonwidget(
 740            parent=self._root_widget,
 741            position=(this_h - this_b_width * 0.5, v),
 742            size=(this_b_width, this_b_height),
 743            autoselect=self._use_autoselect,
 744            button_type='square',
 745            label='',
 746            transition_delay=gather_delay,
 747            on_activate_call=self._gather_press,
 748        )
 749        bui.textwidget(
 750            parent=self._root_widget,
 751            position=(this_h, v + self._button_height * 0.33),
 752            size=(0, 0),
 753            scale=0.75,
 754            transition_delay=gather_delay,
 755            draw_controller=btn,
 756            color=(0.75, 1.0, 0.7),
 757            maxwidth=self._button_width * 0.33,
 758            text=bui.Lstr(resource='gatherWindow.titleText'),
 759            h_align='center',
 760            v_align='center',
 761        )
 762        icon_size = this_b_width * 0.6
 763        bui.imagewidget(
 764            parent=self._root_widget,
 765            size=(icon_size, icon_size),
 766            draw_controller=btn,
 767            transition_delay=gather_delay,
 768            position=(this_h - 0.5 * icon_size, v + 0.31 * this_b_height),
 769            texture=bui.gettexture('usersButton'),
 770        )
 771
 772        # Play button.
 773        h, v, scale = positions[self._p_index]
 774        self._p_index += 1
 775        self._start_button = start_button = bui.buttonwidget(
 776            parent=self._root_widget,
 777            position=(h - play_button_width * 0.5 * scale, v),
 778            size=(play_button_width, play_button_height),
 779            autoselect=self._use_autoselect,
 780            scale=scale,
 781            text_res_scale=2.0,
 782            label=bui.Lstr(resource='playText'),
 783            transition_delay=self._t_delay_play,
 784            on_activate_call=self._play_press,
 785        )
 786        bui.containerwidget(
 787            edit=self._root_widget,
 788            start_button=start_button,
 789            selected_child=start_button,
 790        )
 791        v = v + foof
 792        watch_delay = (
 793            0.0
 794            if self._t_delay_play == 0.0
 795            else max(0.0, self._t_delay_play - 0.1)
 796        )
 797        this_h = h + play_button_width * 0.5 * scale + 40 * scale
 798        this_b_width = self._button_width * 0.25 * scale
 799        this_b_height = self._button_height * 0.82 * scale
 800        self._watch_button = btn = bui.buttonwidget(
 801            parent=self._root_widget,
 802            position=(this_h - this_b_width * 0.5, v),
 803            size=(this_b_width, this_b_height),
 804            autoselect=self._use_autoselect,
 805            button_type='square',
 806            label='',
 807            transition_delay=watch_delay,
 808            on_activate_call=self._watch_press,
 809        )
 810        bui.textwidget(
 811            parent=self._root_widget,
 812            position=(this_h, v + self._button_height * 0.33),
 813            size=(0, 0),
 814            scale=0.75,
 815            transition_delay=watch_delay,
 816            color=(0.75, 1.0, 0.7),
 817            draw_controller=btn,
 818            maxwidth=self._button_width * 0.33,
 819            text=bui.Lstr(resource='watchWindow.titleText'),
 820            h_align='center',
 821            v_align='center',
 822        )
 823        icon_size = this_b_width * 0.55
 824        bui.imagewidget(
 825            parent=self._root_widget,
 826            size=(icon_size, icon_size),
 827            draw_controller=btn,
 828            transition_delay=watch_delay,
 829            position=(this_h - 0.5 * icon_size, v + 0.33 * this_b_height),
 830            texture=bui.gettexture('tv'),
 831        )
 832        if not self._in_game and enable_account_button:
 833            this_b_width = self._button_width
 834            h, v, scale = positions[self._p_index]
 835            self._p_index += 1
 836            self._account_button = bui.buttonwidget(
 837                parent=self._root_widget,
 838                position=(h - this_b_width * 0.5 * scale, v),
 839                size=(this_b_width, self._button_height),
 840                scale=scale,
 841                label=account_type_name,
 842                autoselect=self._use_autoselect,
 843                on_activate_call=account_type_call,
 844                textcolor=account_textcolor,
 845                icon=account_type_icon,
 846                icon_color=account_type_icon_color,
 847                transition_delay=self._tdelay,
 848                enable_sound=account_type_enable_button_sound,
 849            )
 850
 851            # Scattered eggs on easter.
 852            if (
 853                plus.get_v1_account_misc_read_val('easter', False)
 854                and not self._in_game
 855            ):
 856                icon_size = 32
 857                bui.imagewidget(
 858                    parent=self._root_widget,
 859                    position=(
 860                        h - icon_size * 0.5 + 35,
 861                        v
 862                        + self._button_height * scale
 863                        - icon_size * 0.24
 864                        + 1.5,
 865                    ),
 866                    transition_delay=self._tdelay,
 867                    size=(icon_size, icon_size),
 868                    texture=bui.gettexture('egg2'),
 869                    tilt_scale=0.0,
 870                )
 871            self._tdelay += self._t_delay_inc
 872        else:
 873            self._account_button = None
 874
 875        # How-to-play button.
 876        h, v, scale = positions[self._p_index]
 877        self._p_index += 1
 878        btn = bui.buttonwidget(
 879            parent=self._root_widget,
 880            position=(h - self._button_width * 0.5 * scale, v),
 881            scale=scale,
 882            autoselect=self._use_autoselect,
 883            size=(self._button_width, self._button_height),
 884            label=bui.Lstr(resource=f'{self._r}.howToPlayText'),
 885            transition_delay=self._tdelay,
 886            on_activate_call=self._howtoplay,
 887        )
 888        self._how_to_play_button = btn
 889
 890        # Scattered eggs on easter.
 891        if (
 892            plus.get_v1_account_misc_read_val('easter', False)
 893            and not self._in_game
 894        ):
 895            icon_size = 28
 896            bui.imagewidget(
 897                parent=self._root_widget,
 898                position=(
 899                    h - icon_size * 0.5 + 30,
 900                    v + self._button_height * scale - icon_size * 0.24 + 1.5,
 901                ),
 902                transition_delay=self._tdelay,
 903                size=(icon_size, icon_size),
 904                texture=bui.gettexture('egg4'),
 905                tilt_scale=0.0,
 906            )
 907        # Credits button.
 908        self._tdelay += self._t_delay_inc
 909        h, v, scale = positions[self._p_index]
 910        self._p_index += 1
 911        self._credits_button = bui.buttonwidget(
 912            parent=self._root_widget,
 913            position=(h - self._button_width * 0.5 * scale, v),
 914            size=(self._button_width, self._button_height),
 915            autoselect=self._use_autoselect,
 916            label=bui.Lstr(resource=f'{self._r}.creditsText'),
 917            scale=scale,
 918            transition_delay=self._tdelay,
 919            on_activate_call=self._credits,
 920        )
 921        self._tdelay += self._t_delay_inc
 922        return h, v, scale
 923
 924    def _refresh_in_game(
 925        self, positions: list[tuple[float, float, float]]
 926    ) -> tuple[float, float, float]:
 927        # pylint: disable=too-many-branches
 928        # pylint: disable=too-many-locals
 929        # pylint: disable=too-many-statements
 930        assert bui.app.classic is not None
 931        custom_menu_entries: list[dict[str, Any]] = []
 932        session = bs.get_foreground_host_session()
 933        if session is not None:
 934            try:
 935                custom_menu_entries = session.get_custom_menu_entries()
 936                for cme in custom_menu_entries:
 937                    cme_any: Any = cme  # Type check may not hold true.
 938                    if (
 939                        not isinstance(cme_any, dict)
 940                        or 'label' not in cme
 941                        or not isinstance(cme['label'], (str, bui.Lstr))
 942                        or 'call' not in cme
 943                        or not callable(cme['call'])
 944                    ):
 945                        raise ValueError(
 946                            'invalid custom menu entry: ' + str(cme)
 947                        )
 948            except Exception:
 949                custom_menu_entries = []
 950                logging.exception(
 951                    'Error getting custom menu entries for %s.', session
 952                )
 953        self._width = 250.0
 954        self._height = 250.0 if self._input_player else 180.0
 955        if (self._is_demo or self._is_arcade) and self._input_player:
 956            self._height -= 40
 957        if not self._have_settings_button:
 958            self._height -= 50
 959        if self._connected_to_remote_player:
 960            # In this case we have a leave *and* a disconnect button.
 961            self._height += 50
 962        self._height += 50 * (len(custom_menu_entries))
 963        uiscale = bui.app.ui_v1.uiscale
 964        bui.containerwidget(
 965            edit=self._root_widget,
 966            size=(self._width, self._height),
 967            scale=(
 968                2.15
 969                if uiscale is bui.UIScale.SMALL
 970                else 1.6 if uiscale is bui.UIScale.MEDIUM else 1.0
 971            ),
 972        )
 973        h = 125.0
 974        v = self._height - 80.0 if self._input_player else self._height - 60
 975        h_offset = 0
 976        d_h_offset = 0
 977        v_offset = -50
 978        for _i in range(6 + len(custom_menu_entries)):
 979            positions.append((h, v, 1.0))
 980            v += v_offset
 981            h += h_offset
 982            h_offset += d_h_offset
 983        self._start_button = None
 984        bui.app.classic.pause()
 985
 986        # Player name if applicable.
 987        if self._input_player:
 988            player_name = self._input_player.getname()
 989            h, v, scale = positions[self._p_index]
 990            v += 35
 991            bui.textwidget(
 992                parent=self._root_widget,
 993                position=(h - self._button_width / 2, v),
 994                size=(self._button_width, self._button_height),
 995                color=(1, 1, 1, 0.5),
 996                scale=0.7,
 997                h_align='center',
 998                text=bui.Lstr(value=player_name),
 999            )
1000        else:
1001            player_name = ''
1002        h, v, scale = positions[self._p_index]
1003        self._p_index += 1
1004        btn = bui.buttonwidget(
1005            parent=self._root_widget,
1006            position=(h - self._button_width / 2, v),
1007            size=(self._button_width, self._button_height),
1008            scale=scale,
1009            label=bui.Lstr(resource=f'{self._r}.resumeText'),
1010            autoselect=self._use_autoselect,
1011            on_activate_call=self._resume,
1012        )
1013        bui.containerwidget(edit=self._root_widget, cancel_button=btn)
1014
1015        # Add any custom options defined by the current game.
1016        for entry in custom_menu_entries:
1017            h, v, scale = positions[self._p_index]
1018            self._p_index += 1
1019
1020            # Ask the entry whether we should resume when we call
1021            # it (defaults to true).
1022            resume = bool(entry.get('resume_on_call', True))
1023
1024            if resume:
1025                call = bui.Call(self._resume_and_call, entry['call'])
1026            else:
1027                call = bui.Call(entry['call'], bui.WeakCall(self._resume))
1028
1029            bui.buttonwidget(
1030                parent=self._root_widget,
1031                position=(h - self._button_width / 2, v),
1032                size=(self._button_width, self._button_height),
1033                scale=scale,
1034                on_activate_call=call,
1035                label=entry['label'],
1036                autoselect=self._use_autoselect,
1037            )
1038        # Add a 'leave' button if the menu-owner has a player.
1039        if (self._input_player or self._connected_to_remote_player) and not (
1040            self._is_demo or self._is_arcade
1041        ):
1042            h, v, scale = positions[self._p_index]
1043            self._p_index += 1
1044            btn = bui.buttonwidget(
1045                parent=self._root_widget,
1046                position=(h - self._button_width / 2, v),
1047                size=(self._button_width, self._button_height),
1048                scale=scale,
1049                on_activate_call=self._leave,
1050                label='',
1051                autoselect=self._use_autoselect,
1052            )
1053
1054            if (
1055                player_name != ''
1056                and player_name[0] != '<'
1057                and player_name[-1] != '>'
1058            ):
1059                txt = bui.Lstr(
1060                    resource=f'{self._r}.justPlayerText',
1061                    subs=[('${NAME}', player_name)],
1062                )
1063            else:
1064                txt = bui.Lstr(value=player_name)
1065            bui.textwidget(
1066                parent=self._root_widget,
1067                position=(
1068                    h,
1069                    v
1070                    + self._button_height
1071                    * (0.64 if player_name != '' else 0.5),
1072                ),
1073                size=(0, 0),
1074                text=bui.Lstr(resource=f'{self._r}.leaveGameText'),
1075                scale=(0.83 if player_name != '' else 1.0),
1076                color=(0.75, 1.0, 0.7),
1077                h_align='center',
1078                v_align='center',
1079                draw_controller=btn,
1080                maxwidth=self._button_width * 0.9,
1081            )
1082            bui.textwidget(
1083                parent=self._root_widget,
1084                position=(h, v + self._button_height * 0.27),
1085                size=(0, 0),
1086                text=txt,
1087                color=(0.75, 1.0, 0.7),
1088                h_align='center',
1089                v_align='center',
1090                draw_controller=btn,
1091                scale=0.45,
1092                maxwidth=self._button_width * 0.9,
1093            )
1094        return h, v, scale
1095
1096    def _change_replay_speed(self, offs: int) -> None:
1097        if not self._replay_speed_text:
1098            if bui.do_once():
1099                print('_change_replay_speed called without widget')
1100            return
1101        bs.set_replay_speed_exponent(bs.get_replay_speed_exponent() + offs)
1102        actual_speed = pow(2.0, bs.get_replay_speed_exponent())
1103        bui.textwidget(
1104            edit=self._replay_speed_text,
1105            text=bui.Lstr(
1106                resource='watchWindow.playbackSpeedText',
1107                subs=[('${SPEED}', str(actual_speed))],
1108            ),
1109        )
1110
1111    def _pause_or_resume_replay(self) -> None:
1112        if bs.is_replay_paused():
1113            bs.resume_replay()
1114            bui.buttonwidget(
1115                edit=self._pause_resume_button,
1116                label=bui.charstr(bui.SpecialChar.PAUSE_BUTTON),
1117            )
1118        else:
1119            bs.pause_replay()
1120            bui.buttonwidget(
1121                edit=self._pause_resume_button,
1122                label=bui.charstr(bui.SpecialChar.PLAY_BUTTON),
1123            )
1124
1125    def _quit(self) -> None:
1126        # pylint: disable=cyclic-import
1127        from bauiv1lib.confirm import QuitWindow
1128
1129        # no-op if our underlying widget is dead or on its way out.
1130        if not self._root_widget or self._root_widget.transitioning_out:
1131            return
1132
1133        # Note: Normally we should go through bui.quit(confirm=True) but
1134        # invoking the window directly lets us scale it up from the
1135        # button.
1136        QuitWindow(origin_widget=self._quit_button)
1137
1138    def _demo_menu_press(self) -> None:
1139        # pylint: disable=cyclic-import
1140        from bauiv1lib.kiosk import KioskWindow
1141
1142        # no-op if our underlying widget is dead or on its way out.
1143        if not self._root_widget or self._root_widget.transitioning_out:
1144            return
1145
1146        self._save_state()
1147        bui.containerwidget(edit=self._root_widget, transition='out_right')
1148        assert bui.app.classic is not None
1149        bui.app.ui_v1.set_main_menu_window(
1150            KioskWindow(transition='in_left').get_root_widget(),
1151            from_window=self._root_widget,
1152        )
1153
1154    def _show_account_window(self) -> None:
1155        # pylint: disable=cyclic-import
1156        from bauiv1lib.account.settings import AccountSettingsWindow
1157
1158        # no-op if our underlying widget is dead or on its way out.
1159        if not self._root_widget or self._root_widget.transitioning_out:
1160            return
1161
1162        self._save_state()
1163        bui.containerwidget(edit=self._root_widget, transition='out_left')
1164        assert bui.app.classic is not None
1165        bui.app.ui_v1.set_main_menu_window(
1166            AccountSettingsWindow(
1167                origin_widget=self._account_button
1168            ).get_root_widget(),
1169            from_window=self._root_widget,
1170        )
1171
1172    def _on_store_pressed(self) -> None:
1173        # pylint: disable=cyclic-import
1174        from bauiv1lib.store.browser import StoreBrowserWindow
1175        from bauiv1lib.account import show_sign_in_prompt
1176
1177        # no-op if our underlying widget is dead or on its way out.
1178        if not self._root_widget or self._root_widget.transitioning_out:
1179            return
1180
1181        plus = bui.app.plus
1182        assert plus is not None
1183
1184        if plus.get_v1_account_state() != 'signed_in':
1185            show_sign_in_prompt()
1186            return
1187        self._save_state()
1188        bui.containerwidget(edit=self._root_widget, transition='out_left')
1189        assert bui.app.classic is not None
1190        bui.app.ui_v1.set_main_menu_window(
1191            StoreBrowserWindow(
1192                origin_widget=self._store_button
1193            ).get_root_widget(),
1194            from_window=self._root_widget,
1195        )
1196
1197    def _is_benchmark(self) -> bool:
1198        session = bs.get_foreground_host_session()
1199        return getattr(session, 'benchmark_type', None) == 'cpu' or (
1200            bui.app.classic is not None
1201            and bui.app.classic.stress_test_update_timer is not None
1202        )
1203
1204    def _confirm_end_game(self) -> None:
1205        # pylint: disable=cyclic-import
1206        from bauiv1lib.confirm import ConfirmWindow
1207
1208        # FIXME: Currently we crash calling this on client-sessions.
1209
1210        # Select cancel by default; this occasionally gets called by accident
1211        # in a fit of button mashing and this will help reduce damage.
1212        ConfirmWindow(
1213            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
1214            self._end_game,
1215            cancel_is_selected=True,
1216        )
1217
1218    def _confirm_end_test(self) -> None:
1219        # pylint: disable=cyclic-import
1220        from bauiv1lib.confirm import ConfirmWindow
1221
1222        # Select cancel by default; this occasionally gets called by accident
1223        # in a fit of button mashing and this will help reduce damage.
1224        ConfirmWindow(
1225            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
1226            self._end_game,
1227            cancel_is_selected=True,
1228        )
1229
1230    def _confirm_end_replay(self) -> None:
1231        # pylint: disable=cyclic-import
1232        from bauiv1lib.confirm import ConfirmWindow
1233
1234        # Select cancel by default; this occasionally gets called by accident
1235        # in a fit of button mashing and this will help reduce damage.
1236        ConfirmWindow(
1237            bui.Lstr(resource=f'{self._r}.exitToMenuText'),
1238            self._end_game,
1239            cancel_is_selected=True,
1240        )
1241
1242    def _confirm_leave_party(self) -> None:
1243        # pylint: disable=cyclic-import
1244        from bauiv1lib.confirm import ConfirmWindow
1245
1246        # Select cancel by default; this occasionally gets called by accident
1247        # in a fit of button mashing and this will help reduce damage.
1248        ConfirmWindow(
1249            bui.Lstr(resource=f'{self._r}.leavePartyConfirmText'),
1250            self._leave_party,
1251            cancel_is_selected=True,
1252        )
1253
1254    def _leave_party(self) -> None:
1255        bs.disconnect_from_host()
1256
1257    def _end_game(self) -> None:
1258        assert bui.app.classic is not None
1259
1260        # no-op if our underlying widget is dead or on its way out.
1261        if not self._root_widget or self._root_widget.transitioning_out:
1262            return
1263
1264        bui.containerwidget(edit=self._root_widget, transition='out_left')
1265        bui.app.classic.return_to_main_menu_session_gracefully(reset_ui=False)
1266
1267    def _leave(self) -> None:
1268        if self._input_player:
1269            self._input_player.remove_from_game()
1270        elif self._connected_to_remote_player:
1271            if self._input_device:
1272                self._input_device.detach_from_player()
1273        self._resume()
1274
1275    def _credits(self) -> None:
1276        # pylint: disable=cyclic-import
1277        from bauiv1lib.creditslist import CreditsListWindow
1278
1279        # no-op if our underlying widget is dead or on its way out.
1280        if not self._root_widget or self._root_widget.transitioning_out:
1281            return
1282
1283        self._save_state()
1284        bui.containerwidget(edit=self._root_widget, transition='out_left')
1285        assert bui.app.classic is not None
1286        bui.app.ui_v1.set_main_menu_window(
1287            CreditsListWindow(
1288                origin_widget=self._credits_button
1289            ).get_root_widget(),
1290            from_window=self._root_widget,
1291        )
1292
1293    def _howtoplay(self) -> None:
1294        # pylint: disable=cyclic-import
1295        from bauiv1lib.helpui import HelpWindow
1296
1297        # no-op if our underlying widget is dead or on its way out.
1298        if not self._root_widget or self._root_widget.transitioning_out:
1299            return
1300
1301        self._save_state()
1302        bui.containerwidget(edit=self._root_widget, transition='out_left')
1303        assert bui.app.classic is not None
1304        bui.app.ui_v1.set_main_menu_window(
1305            HelpWindow(
1306                main_menu=True, origin_widget=self._how_to_play_button
1307            ).get_root_widget(),
1308            from_window=self._root_widget,
1309        )
1310
1311    def _settings(self) -> None:
1312        # pylint: disable=cyclic-import
1313        from bauiv1lib.settings.allsettings import AllSettingsWindow
1314
1315        # no-op if our underlying widget is dead or on its way out.
1316        if not self._root_widget or self._root_widget.transitioning_out:
1317            return
1318
1319        self._save_state()
1320        bui.containerwidget(edit=self._root_widget, transition='out_left')
1321        assert bui.app.classic is not None
1322        bui.app.ui_v1.set_main_menu_window(
1323            AllSettingsWindow(
1324                origin_widget=self._settings_button
1325            ).get_root_widget(),
1326            from_window=self._root_widget,
1327        )
1328
1329    def _resume_and_call(self, call: Callable[[], Any]) -> None:
1330        self._resume()
1331        call()
1332
1333    def _do_game_service_press(self) -> None:
1334        self._save_state()
1335        if bui.app.plus is not None:
1336            bui.app.plus.show_game_service_ui()
1337        else:
1338            logging.warning(
1339                'plus feature-set is required to show game service ui'
1340            )
1341
1342    def _save_state(self) -> None:
1343        # Don't do this for the in-game menu.
1344        if self._in_game:
1345            return
1346        assert bui.app.classic is not None
1347        ui = bui.app.ui_v1
1348        sel = self._root_widget.get_selected_child()
1349        if sel == self._start_button:
1350            ui.main_menu_selection = 'Start'
1351        elif sel == self._gather_button:
1352            ui.main_menu_selection = 'Gather'
1353        elif sel == self._watch_button:
1354            ui.main_menu_selection = 'Watch'
1355        elif sel == self._how_to_play_button:
1356            ui.main_menu_selection = 'HowToPlay'
1357        elif sel == self._credits_button:
1358            ui.main_menu_selection = 'Credits'
1359        elif sel == self._settings_button:
1360            ui.main_menu_selection = 'Settings'
1361        elif sel == self._account_button:
1362            ui.main_menu_selection = 'Account'
1363        elif sel == self._store_button:
1364            ui.main_menu_selection = 'Store'
1365        elif sel == self._quit_button:
1366            ui.main_menu_selection = 'Quit'
1367        elif sel == self._demo_menu_button:
1368            ui.main_menu_selection = 'DemoMenu'
1369        else:
1370            print('unknown widget in main menu store selection:', sel)
1371            ui.main_menu_selection = 'Start'
1372
1373    def _restore_state(self) -> None:
1374        # pylint: disable=too-many-branches
1375
1376        # Don't do this for the in-game menu.
1377        if self._in_game:
1378            return
1379        assert bui.app.classic is not None
1380        sel_name = bui.app.ui_v1.main_menu_selection
1381        sel: bui.Widget | None
1382        if sel_name is None:
1383            sel_name = 'Start'
1384        if sel_name == 'HowToPlay':
1385            sel = self._how_to_play_button
1386        elif sel_name == 'Gather':
1387            sel = self._gather_button
1388        elif sel_name == 'Watch':
1389            sel = self._watch_button
1390        elif sel_name == 'Credits':
1391            sel = self._credits_button
1392        elif sel_name == 'Settings':
1393            sel = self._settings_button
1394        elif sel_name == 'Account':
1395            sel = self._account_button
1396        elif sel_name == 'Store':
1397            sel = self._store_button
1398        elif sel_name == 'Quit':
1399            sel = self._quit_button
1400        elif sel_name == 'DemoMenu':
1401            sel = self._demo_menu_button
1402        else:
1403            sel = self._start_button
1404        if sel is not None:
1405            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1406
1407    def _gather_press(self) -> None:
1408        # pylint: disable=cyclic-import
1409        from bauiv1lib.gather import GatherWindow
1410
1411        # no-op if our underlying widget is dead or on its way out.
1412        if not self._root_widget or self._root_widget.transitioning_out:
1413            return
1414
1415        self._save_state()
1416        bui.containerwidget(edit=self._root_widget, transition='out_left')
1417        assert bui.app.classic is not None
1418        bui.app.ui_v1.set_main_menu_window(
1419            GatherWindow(origin_widget=self._gather_button).get_root_widget(),
1420            from_window=self._root_widget,
1421        )
1422
1423    def _watch_press(self) -> None:
1424        # pylint: disable=cyclic-import
1425        from bauiv1lib.watch import WatchWindow
1426
1427        # no-op if our underlying widget is dead or on its way out.
1428        if not self._root_widget or self._root_widget.transitioning_out:
1429            return
1430
1431        self._save_state()
1432        bui.containerwidget(edit=self._root_widget, transition='out_left')
1433        assert bui.app.classic is not None
1434        bui.app.ui_v1.set_main_menu_window(
1435            WatchWindow(origin_widget=self._watch_button).get_root_widget(),
1436            from_window=self._root_widget,
1437        )
1438
1439    def _play_press(self) -> None:
1440        # pylint: disable=cyclic-import
1441        from bauiv1lib.play import PlayWindow
1442
1443        # no-op if our underlying widget is dead or on its way out.
1444        if not self._root_widget or self._root_widget.transitioning_out:
1445            return
1446
1447        self._save_state()
1448        bui.containerwidget(edit=self._root_widget, transition='out_left')
1449
1450        assert bui.app.classic is not None
1451        bui.app.ui_v1.selecting_private_party_playlist = False
1452        bui.app.ui_v1.set_main_menu_window(
1453            PlayWindow(origin_widget=self._start_button).get_root_widget(),
1454            from_window=self._root_widget,
1455        )
1456
1457    def _resume(self) -> None:
1458        assert bui.app.classic is not None
1459        bui.app.classic.resume()
1460        # if self._root_widget:
1461        #     bui.containerwidget(edit=self._root_widget,
1462        # transition='out_right')
1463        bui.app.ui_v1.clear_main_menu_window(transition='out_right')
1464
1465        # If there's callbacks waiting for this window to go away, call them.
1466        for call in bui.app.ui_v1.main_menu_resume_callbacks:
1467            call()
1468        del bui.app.ui_v1.main_menu_resume_callbacks[:]