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