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