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