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