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