bauiv1lib.playoptions
Provides a window for configuring play options.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides a window for configuring play options.""" 4 5from __future__ import annotations 6 7import logging 8from typing import TYPE_CHECKING, override 9 10import bascenev1 as bs 11import bauiv1 as bui 12 13from bauiv1lib.popup import PopupWindow 14 15if TYPE_CHECKING: 16 from typing import Any 17 18 19class PlayOptionsWindow(PopupWindow): 20 """A popup window for configuring play options.""" 21 22 def __init__( 23 self, 24 sessiontype: type[bs.Session], 25 playlist: str, 26 scale_origin: tuple[float, float], 27 delegate: Any = None, 28 ): 29 # FIXME: Tidy this up. 30 # pylint: disable=too-many-branches 31 # pylint: disable=too-many-statements 32 # pylint: disable=too-many-locals 33 from bascenev1 import filter_playlist, get_map_class 34 from bauiv1lib.playlist import PlaylistTypeVars 35 from bauiv1lib.config import ConfigNumberEdit 36 37 self._r = 'gameListWindow' 38 self._delegate = delegate 39 self._pvars = PlaylistTypeVars(sessiontype) 40 self._transitioning_out = False 41 42 # We behave differently if we're being used for playlist selection 43 # vs starting a game directly (should make this more elegant). 44 classic = bui.app.classic 45 assert classic is not None 46 self._selecting_mode = classic.selecting_private_party_playlist 47 48 self._do_randomize_val = bui.app.config.get( 49 self._pvars.config_name + ' Playlist Randomize', 0 50 ) 51 52 self._sessiontype = sessiontype 53 self._playlist = playlist 54 55 self._width = 500.0 56 self._height = 370.0 - 50.0 57 58 # In teams games, show the custom names/colors button. 59 if self._sessiontype is bs.DualTeamSession: 60 self._height += 50.0 61 62 self._row_height = 45.0 63 64 # Grab our maps to display. 65 mesh_opaque = bui.getmesh('level_select_button_opaque') 66 mesh_transparent = bui.getmesh('level_select_button_transparent') 67 mask_tex = bui.gettexture('mapPreviewMask') 68 69 # Poke into this playlist and see if we can display some of its maps. 70 map_textures = [] 71 map_texture_entries = [] 72 rows = 0 73 columns = 0 74 game_count = 0 75 scl = 0.35 76 c_width_total = 0.0 77 try: 78 max_columns = 5 79 name = playlist 80 if name == '__default__': 81 plst = self._pvars.get_default_list_call() 82 else: 83 try: 84 plst = bui.app.config[ 85 self._pvars.config_name + ' Playlists' 86 ][name] 87 except Exception: 88 print( 89 'ERROR INFO: self._config_name is:', 90 self._pvars.config_name, 91 ) 92 print( 93 'ERROR INFO: playlist names are:', 94 list( 95 bui.app.config[ 96 self._pvars.config_name + ' Playlists' 97 ].keys() 98 ), 99 ) 100 raise 101 plst = filter_playlist( 102 plst, 103 self._sessiontype, 104 remove_unowned=False, 105 mark_unowned=True, 106 name=name, 107 ) 108 game_count = len(plst) 109 for entry in plst: 110 mapname = entry['settings']['map'] 111 maptype: type[bs.Map] | None 112 try: 113 maptype = get_map_class(mapname) 114 except bui.NotFoundError: 115 maptype = None 116 if maptype is not None: 117 tex_name = maptype.get_preview_texture_name() 118 if tex_name is not None: 119 map_textures.append(tex_name) 120 map_texture_entries.append(entry) 121 rows = (max(0, len(map_textures) - 1) // max_columns) + 1 122 columns = min(max_columns, len(map_textures)) 123 124 if len(map_textures) == 1: 125 scl = 1.1 126 elif len(map_textures) == 2: 127 scl = 0.7 128 elif len(map_textures) == 3: 129 scl = 0.55 130 else: 131 scl = 0.35 132 self._row_height = 128.0 * scl 133 c_width_total = scl * 250.0 * columns 134 if map_textures: 135 self._height += self._row_height * rows 136 137 except Exception: 138 logging.exception('Error listing playlist maps.') 139 140 show_shuffle_check_box = game_count > 1 141 142 if show_shuffle_check_box: 143 self._height += 40 144 145 uiscale = bui.app.ui_v1.uiscale 146 scale = ( 147 1.69 148 if uiscale is bui.UIScale.SMALL 149 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.85 150 ) 151 # Creates our _root_widget. 152 super().__init__( 153 position=scale_origin, size=(self._width, self._height), scale=scale 154 ) 155 156 playlist_name: str | bui.Lstr = ( 157 self._pvars.default_list_name 158 if playlist == '__default__' 159 else playlist 160 ) 161 self._title_text = bui.textwidget( 162 parent=self.root_widget, 163 position=(self._width * 0.5, self._height - 89 + 51), 164 size=(0, 0), 165 text=playlist_name, 166 scale=1.4, 167 color=(1, 1, 1), 168 maxwidth=self._width * 0.7, 169 h_align='center', 170 v_align='center', 171 ) 172 173 self._cancel_button = bui.buttonwidget( 174 parent=self.root_widget, 175 position=(25, self._height - 53), 176 size=(50, 50), 177 scale=0.7, 178 label='', 179 color=(0.42, 0.73, 0.2), 180 on_activate_call=self._on_cancel_press, 181 autoselect=True, 182 icon=bui.gettexture('crossOut'), 183 iconscale=1.2, 184 ) 185 186 h_offs_img = self._width * 0.5 - c_width_total * 0.5 187 v_offs_img = self._height - 118 - scl * 125.0 + 50 188 bottom_row_buttons = [] 189 self._have_at_least_one_owned = False 190 191 for row in range(rows): 192 for col in range(columns): 193 tex_index = row * columns + col 194 if tex_index < len(map_textures): 195 tex_name = map_textures[tex_index] 196 h = h_offs_img + scl * 250 * col 197 v = v_offs_img - self._row_height * row 198 entry = map_texture_entries[tex_index] 199 owned = not ( 200 ('is_unowned_map' in entry and entry['is_unowned_map']) 201 or ( 202 'is_unowned_game' in entry 203 and entry['is_unowned_game'] 204 ) 205 ) 206 207 if owned: 208 self._have_at_least_one_owned = True 209 210 try: 211 desc = bui.getclass( 212 entry['type'], subclassof=bs.GameActivity 213 ).get_settings_display_string(entry) 214 if not owned: 215 desc = bui.Lstr( 216 value='${DESC}\n${UNLOCK}', 217 subs=[ 218 ('${DESC}', desc), 219 ( 220 '${UNLOCK}', 221 bui.Lstr( 222 resource='unlockThisInTheStoreText' 223 ), 224 ), 225 ], 226 ) 227 desc_color = (0, 1, 0) if owned else (1, 0, 0) 228 except Exception: 229 desc = bui.Lstr(value='(invalid)') 230 desc_color = (1, 0, 0) 231 232 btn = bui.buttonwidget( 233 parent=self.root_widget, 234 size=(scl * 240.0, scl * 120.0), 235 position=(h, v), 236 texture=bui.gettexture(tex_name if owned else 'empty'), 237 mesh_opaque=mesh_opaque if owned else None, 238 on_activate_call=bui.Call( 239 bui.screenmessage, desc, desc_color 240 ), 241 label='', 242 color=(1, 1, 1), 243 autoselect=True, 244 extra_touch_border_scale=0.0, 245 mesh_transparent=mesh_transparent if owned else None, 246 mask_texture=mask_tex if owned else None, 247 ) 248 if row == 0 and col == 0: 249 bui.widget(edit=self._cancel_button, down_widget=btn) 250 if row == rows - 1: 251 bottom_row_buttons.append(btn) 252 if not owned: 253 # Ewww; buttons don't currently have alpha so in this 254 # case we draw an image over our button with an empty 255 # texture on it. 256 bui.imagewidget( 257 parent=self.root_widget, 258 size=(scl * 260.0, scl * 130.0), 259 position=(h - 10.0 * scl, v - 4.0 * scl), 260 draw_controller=btn, 261 color=(1, 1, 1), 262 texture=bui.gettexture(tex_name), 263 mesh_opaque=mesh_opaque, 264 opacity=0.25, 265 mesh_transparent=mesh_transparent, 266 mask_texture=mask_tex, 267 ) 268 269 bui.imagewidget( 270 parent=self.root_widget, 271 size=(scl * 100, scl * 100), 272 draw_controller=btn, 273 position=(h + scl * 70, v + scl * 10), 274 texture=bui.gettexture('lock'), 275 ) 276 277 y_offs = 50 if show_shuffle_check_box else 0 278 279 # Series Length 280 y_offs2 = 40 if self._sessiontype is bs.DualTeamSession else 0 281 self._series_length_numedit = ConfigNumberEdit( 282 parent=self.root_widget, 283 position=(100, 200 + y_offs + y_offs2), 284 configkey=( 285 'FFA' if self._sessiontype is bs.FreeForAllSession else 'Teams' 286 ) 287 + ' Series Length', 288 displayname=bui.Lstr( 289 resource=self._r 290 + ( 291 '.pointsToWinText' 292 if self._sessiontype is bs.FreeForAllSession 293 else '.seriesLengthText' 294 ) 295 ), 296 minval=1.0, 297 maxval=100.0 if self._sessiontype is bs.FreeForAllSession else 99.0, 298 increment=1.0 if self._sessiontype is bs.FreeForAllSession else 2.0, 299 fallback_value=( 300 24 if self._sessiontype is bs.FreeForAllSession else 7 301 ), 302 f=0, 303 ) 304 305 # Team names/colors. 306 self._custom_colors_names_button: bui.Widget | None 307 if self._sessiontype is bs.DualTeamSession: 308 self._custom_colors_names_button = bui.buttonwidget( 309 parent=self.root_widget, 310 position=(100, 195 + y_offs), 311 size=(290, 35), 312 on_activate_call=bui.WeakCall(self._custom_colors_names_press), 313 autoselect=True, 314 textcolor=(0.8, 0.8, 0.8), 315 label=bui.Lstr(resource='teamNamesColorText'), 316 ) 317 assert bui.app.classic is not None 318 if not bui.app.classic.accounts.have_pro(): 319 bui.imagewidget( 320 parent=self.root_widget, 321 size=(30, 30), 322 position=(95, 202 + y_offs), 323 texture=bui.gettexture('lock'), 324 draw_controller=self._custom_colors_names_button, 325 ) 326 else: 327 self._custom_colors_names_button = None 328 329 # Shuffle. 330 def _cb_callback(val: bool) -> None: 331 self._do_randomize_val = val 332 cfg = bui.app.config 333 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 334 self._do_randomize_val 335 ) 336 cfg.commit() 337 338 if show_shuffle_check_box: 339 self._shuffle_check_box = bui.checkboxwidget( 340 parent=self.root_widget, 341 position=(110, 200), 342 scale=1.0, 343 size=(250, 30), 344 autoselect=True, 345 text=bui.Lstr(resource=f'{self._r}.shuffleGameOrderText'), 346 maxwidth=300, 347 textcolor=(0.8, 0.8, 0.8), 348 value=self._do_randomize_val, 349 on_value_change_call=_cb_callback, 350 ) 351 352 # Show tutorial. 353 show_tutorial = bool(bui.app.config.get('Show Tutorial', True)) 354 355 def _cb_callback_2(val: bool) -> None: 356 cfg = bui.app.config 357 cfg['Show Tutorial'] = val 358 cfg.commit() 359 360 self._show_tutorial_check_box = bui.checkboxwidget( 361 parent=self.root_widget, 362 position=(110, 151), 363 scale=1.0, 364 size=(250, 30), 365 autoselect=True, 366 text=bui.Lstr(resource=f'{self._r}.showTutorialText'), 367 maxwidth=300, 368 textcolor=(0.8, 0.8, 0.8), 369 value=show_tutorial, 370 on_value_change_call=_cb_callback_2, 371 ) 372 373 # Grumble: current autoselect doesn't do a very good job 374 # with checkboxes. 375 if self._custom_colors_names_button is not None: 376 for btn in bottom_row_buttons: 377 bui.widget( 378 edit=btn, down_widget=self._custom_colors_names_button 379 ) 380 if show_shuffle_check_box: 381 bui.widget( 382 edit=self._custom_colors_names_button, 383 down_widget=self._shuffle_check_box, 384 ) 385 bui.widget( 386 edit=self._shuffle_check_box, 387 up_widget=self._custom_colors_names_button, 388 ) 389 else: 390 bui.widget( 391 edit=self._custom_colors_names_button, 392 down_widget=self._show_tutorial_check_box, 393 ) 394 bui.widget( 395 edit=self._show_tutorial_check_box, 396 up_widget=self._custom_colors_names_button, 397 ) 398 399 self._ok_button = bui.buttonwidget( 400 parent=self.root_widget, 401 position=(70, 44), 402 size=(200, 45), 403 scale=1.8, 404 text_res_scale=1.5, 405 on_activate_call=self._on_ok_press, 406 autoselect=True, 407 label=bui.Lstr( 408 resource='okText' if self._selecting_mode else 'playText' 409 ), 410 ) 411 412 bui.widget( 413 edit=self._ok_button, up_widget=self._show_tutorial_check_box 414 ) 415 416 bui.containerwidget( 417 edit=self.root_widget, 418 start_button=self._ok_button, 419 cancel_button=self._cancel_button, 420 selected_child=self._ok_button, 421 ) 422 423 # Update now and once per second. 424 self._update_timer = bui.AppTimer( 425 1.0, bui.WeakCall(self._update), repeat=True 426 ) 427 self._update() 428 429 def _custom_colors_names_press(self) -> None: 430 from bauiv1lib.account import show_sign_in_prompt 431 from bauiv1lib.teamnamescolors import TeamNamesColorsWindow 432 from bauiv1lib.purchase import PurchaseWindow 433 434 plus = bui.app.plus 435 assert plus is not None 436 437 assert bui.app.classic is not None 438 if not bui.app.classic.accounts.have_pro(): 439 if plus.get_v1_account_state() != 'signed_in': 440 show_sign_in_prompt() 441 else: 442 PurchaseWindow(items=['pro']) 443 self._transition_out() 444 return 445 assert self._custom_colors_names_button 446 TeamNamesColorsWindow( 447 scale_origin=( 448 self._custom_colors_names_button.get_screen_space_center() 449 ) 450 ) 451 452 def _does_target_playlist_exist(self) -> bool: 453 if self._playlist == '__default__': 454 return True 455 return self._playlist in bui.app.config.get( 456 self._pvars.config_name + ' Playlists', {} 457 ) 458 459 def _update(self) -> None: 460 # All we do here is make sure our targeted playlist still exists, 461 # and close ourself if not. 462 if not self._does_target_playlist_exist(): 463 self._transition_out() 464 465 def _transition_out(self, transition: str = 'out_scale') -> None: 466 if not self._transitioning_out: 467 self._transitioning_out = True 468 bui.containerwidget(edit=self.root_widget, transition=transition) 469 470 @override 471 def on_popup_cancel(self) -> None: 472 bui.getsound('swish').play() 473 self._transition_out() 474 475 def _on_cancel_press(self) -> None: 476 self._transition_out() 477 478 def _on_ok_press(self) -> None: 479 # no-op if our underlying widget is dead or on its way out. 480 if not self.root_widget or self.root_widget.transitioning_out: 481 return 482 483 # Disallow if our playlist has disappeared. 484 if not self._does_target_playlist_exist(): 485 return 486 487 # Disallow if we have no unlocked games. 488 if not self._have_at_least_one_owned: 489 bui.getsound('error').play() 490 bui.screenmessage( 491 bui.Lstr(resource='playlistNoValidGamesErrorText'), 492 color=(1, 0, 0), 493 ) 494 return 495 496 cfg = bui.app.config 497 cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist 498 499 # Head back to the gather window in playlist-select mode 500 # or start the game in regular mode. 501 if self._selecting_mode: 502 from bauiv1lib.gather import GatherWindow 503 504 if self._sessiontype is bs.FreeForAllSession: 505 typename = 'ffa' 506 elif self._sessiontype is bs.DualTeamSession: 507 typename = 'teams' 508 else: 509 raise RuntimeError('Only teams and ffa currently supported') 510 cfg['Private Party Host Session Type'] = typename 511 bui.getsound('gunCocking').play() 512 assert bui.app.classic is not None 513 # Note: this is a wonky situation where we aren't actually 514 # the main window but we set it on behalf of the main window 515 # that popped us up. 516 bui.app.ui_v1.set_main_window( 517 GatherWindow(transition='in_right'), 518 from_window=False, # Disable this test. 519 ) 520 self._transition_out(transition='out_left') 521 if self._delegate is not None: 522 self._delegate.on_play_options_window_run_game() 523 else: 524 bui.fade_screen(False, endcall=self._run_selected_playlist) 525 bui.lock_all_input() 526 self._transition_out(transition='out_left') 527 if self._delegate is not None: 528 self._delegate.on_play_options_window_run_game() 529 530 cfg.commit() 531 532 def _run_selected_playlist(self) -> None: 533 bui.unlock_all_input() 534 try: 535 bs.new_host_session(self._sessiontype) 536 except Exception: 537 from bascenev1lib import mainmenu 538 539 logging.exception('Error running session %s.', self._sessiontype) 540 541 # Drop back into a main menu session. 542 bs.new_host_session(mainmenu.MainMenuSession)
20class PlayOptionsWindow(PopupWindow): 21 """A popup window for configuring play options.""" 22 23 def __init__( 24 self, 25 sessiontype: type[bs.Session], 26 playlist: str, 27 scale_origin: tuple[float, float], 28 delegate: Any = None, 29 ): 30 # FIXME: Tidy this up. 31 # pylint: disable=too-many-branches 32 # pylint: disable=too-many-statements 33 # pylint: disable=too-many-locals 34 from bascenev1 import filter_playlist, get_map_class 35 from bauiv1lib.playlist import PlaylistTypeVars 36 from bauiv1lib.config import ConfigNumberEdit 37 38 self._r = 'gameListWindow' 39 self._delegate = delegate 40 self._pvars = PlaylistTypeVars(sessiontype) 41 self._transitioning_out = False 42 43 # We behave differently if we're being used for playlist selection 44 # vs starting a game directly (should make this more elegant). 45 classic = bui.app.classic 46 assert classic is not None 47 self._selecting_mode = classic.selecting_private_party_playlist 48 49 self._do_randomize_val = bui.app.config.get( 50 self._pvars.config_name + ' Playlist Randomize', 0 51 ) 52 53 self._sessiontype = sessiontype 54 self._playlist = playlist 55 56 self._width = 500.0 57 self._height = 370.0 - 50.0 58 59 # In teams games, show the custom names/colors button. 60 if self._sessiontype is bs.DualTeamSession: 61 self._height += 50.0 62 63 self._row_height = 45.0 64 65 # Grab our maps to display. 66 mesh_opaque = bui.getmesh('level_select_button_opaque') 67 mesh_transparent = bui.getmesh('level_select_button_transparent') 68 mask_tex = bui.gettexture('mapPreviewMask') 69 70 # Poke into this playlist and see if we can display some of its maps. 71 map_textures = [] 72 map_texture_entries = [] 73 rows = 0 74 columns = 0 75 game_count = 0 76 scl = 0.35 77 c_width_total = 0.0 78 try: 79 max_columns = 5 80 name = playlist 81 if name == '__default__': 82 plst = self._pvars.get_default_list_call() 83 else: 84 try: 85 plst = bui.app.config[ 86 self._pvars.config_name + ' Playlists' 87 ][name] 88 except Exception: 89 print( 90 'ERROR INFO: self._config_name is:', 91 self._pvars.config_name, 92 ) 93 print( 94 'ERROR INFO: playlist names are:', 95 list( 96 bui.app.config[ 97 self._pvars.config_name + ' Playlists' 98 ].keys() 99 ), 100 ) 101 raise 102 plst = filter_playlist( 103 plst, 104 self._sessiontype, 105 remove_unowned=False, 106 mark_unowned=True, 107 name=name, 108 ) 109 game_count = len(plst) 110 for entry in plst: 111 mapname = entry['settings']['map'] 112 maptype: type[bs.Map] | None 113 try: 114 maptype = get_map_class(mapname) 115 except bui.NotFoundError: 116 maptype = None 117 if maptype is not None: 118 tex_name = maptype.get_preview_texture_name() 119 if tex_name is not None: 120 map_textures.append(tex_name) 121 map_texture_entries.append(entry) 122 rows = (max(0, len(map_textures) - 1) // max_columns) + 1 123 columns = min(max_columns, len(map_textures)) 124 125 if len(map_textures) == 1: 126 scl = 1.1 127 elif len(map_textures) == 2: 128 scl = 0.7 129 elif len(map_textures) == 3: 130 scl = 0.55 131 else: 132 scl = 0.35 133 self._row_height = 128.0 * scl 134 c_width_total = scl * 250.0 * columns 135 if map_textures: 136 self._height += self._row_height * rows 137 138 except Exception: 139 logging.exception('Error listing playlist maps.') 140 141 show_shuffle_check_box = game_count > 1 142 143 if show_shuffle_check_box: 144 self._height += 40 145 146 uiscale = bui.app.ui_v1.uiscale 147 scale = ( 148 1.69 149 if uiscale is bui.UIScale.SMALL 150 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.85 151 ) 152 # Creates our _root_widget. 153 super().__init__( 154 position=scale_origin, size=(self._width, self._height), scale=scale 155 ) 156 157 playlist_name: str | bui.Lstr = ( 158 self._pvars.default_list_name 159 if playlist == '__default__' 160 else playlist 161 ) 162 self._title_text = bui.textwidget( 163 parent=self.root_widget, 164 position=(self._width * 0.5, self._height - 89 + 51), 165 size=(0, 0), 166 text=playlist_name, 167 scale=1.4, 168 color=(1, 1, 1), 169 maxwidth=self._width * 0.7, 170 h_align='center', 171 v_align='center', 172 ) 173 174 self._cancel_button = bui.buttonwidget( 175 parent=self.root_widget, 176 position=(25, self._height - 53), 177 size=(50, 50), 178 scale=0.7, 179 label='', 180 color=(0.42, 0.73, 0.2), 181 on_activate_call=self._on_cancel_press, 182 autoselect=True, 183 icon=bui.gettexture('crossOut'), 184 iconscale=1.2, 185 ) 186 187 h_offs_img = self._width * 0.5 - c_width_total * 0.5 188 v_offs_img = self._height - 118 - scl * 125.0 + 50 189 bottom_row_buttons = [] 190 self._have_at_least_one_owned = False 191 192 for row in range(rows): 193 for col in range(columns): 194 tex_index = row * columns + col 195 if tex_index < len(map_textures): 196 tex_name = map_textures[tex_index] 197 h = h_offs_img + scl * 250 * col 198 v = v_offs_img - self._row_height * row 199 entry = map_texture_entries[tex_index] 200 owned = not ( 201 ('is_unowned_map' in entry and entry['is_unowned_map']) 202 or ( 203 'is_unowned_game' in entry 204 and entry['is_unowned_game'] 205 ) 206 ) 207 208 if owned: 209 self._have_at_least_one_owned = True 210 211 try: 212 desc = bui.getclass( 213 entry['type'], subclassof=bs.GameActivity 214 ).get_settings_display_string(entry) 215 if not owned: 216 desc = bui.Lstr( 217 value='${DESC}\n${UNLOCK}', 218 subs=[ 219 ('${DESC}', desc), 220 ( 221 '${UNLOCK}', 222 bui.Lstr( 223 resource='unlockThisInTheStoreText' 224 ), 225 ), 226 ], 227 ) 228 desc_color = (0, 1, 0) if owned else (1, 0, 0) 229 except Exception: 230 desc = bui.Lstr(value='(invalid)') 231 desc_color = (1, 0, 0) 232 233 btn = bui.buttonwidget( 234 parent=self.root_widget, 235 size=(scl * 240.0, scl * 120.0), 236 position=(h, v), 237 texture=bui.gettexture(tex_name if owned else 'empty'), 238 mesh_opaque=mesh_opaque if owned else None, 239 on_activate_call=bui.Call( 240 bui.screenmessage, desc, desc_color 241 ), 242 label='', 243 color=(1, 1, 1), 244 autoselect=True, 245 extra_touch_border_scale=0.0, 246 mesh_transparent=mesh_transparent if owned else None, 247 mask_texture=mask_tex if owned else None, 248 ) 249 if row == 0 and col == 0: 250 bui.widget(edit=self._cancel_button, down_widget=btn) 251 if row == rows - 1: 252 bottom_row_buttons.append(btn) 253 if not owned: 254 # Ewww; buttons don't currently have alpha so in this 255 # case we draw an image over our button with an empty 256 # texture on it. 257 bui.imagewidget( 258 parent=self.root_widget, 259 size=(scl * 260.0, scl * 130.0), 260 position=(h - 10.0 * scl, v - 4.0 * scl), 261 draw_controller=btn, 262 color=(1, 1, 1), 263 texture=bui.gettexture(tex_name), 264 mesh_opaque=mesh_opaque, 265 opacity=0.25, 266 mesh_transparent=mesh_transparent, 267 mask_texture=mask_tex, 268 ) 269 270 bui.imagewidget( 271 parent=self.root_widget, 272 size=(scl * 100, scl * 100), 273 draw_controller=btn, 274 position=(h + scl * 70, v + scl * 10), 275 texture=bui.gettexture('lock'), 276 ) 277 278 y_offs = 50 if show_shuffle_check_box else 0 279 280 # Series Length 281 y_offs2 = 40 if self._sessiontype is bs.DualTeamSession else 0 282 self._series_length_numedit = ConfigNumberEdit( 283 parent=self.root_widget, 284 position=(100, 200 + y_offs + y_offs2), 285 configkey=( 286 'FFA' if self._sessiontype is bs.FreeForAllSession else 'Teams' 287 ) 288 + ' Series Length', 289 displayname=bui.Lstr( 290 resource=self._r 291 + ( 292 '.pointsToWinText' 293 if self._sessiontype is bs.FreeForAllSession 294 else '.seriesLengthText' 295 ) 296 ), 297 minval=1.0, 298 maxval=100.0 if self._sessiontype is bs.FreeForAllSession else 99.0, 299 increment=1.0 if self._sessiontype is bs.FreeForAllSession else 2.0, 300 fallback_value=( 301 24 if self._sessiontype is bs.FreeForAllSession else 7 302 ), 303 f=0, 304 ) 305 306 # Team names/colors. 307 self._custom_colors_names_button: bui.Widget | None 308 if self._sessiontype is bs.DualTeamSession: 309 self._custom_colors_names_button = bui.buttonwidget( 310 parent=self.root_widget, 311 position=(100, 195 + y_offs), 312 size=(290, 35), 313 on_activate_call=bui.WeakCall(self._custom_colors_names_press), 314 autoselect=True, 315 textcolor=(0.8, 0.8, 0.8), 316 label=bui.Lstr(resource='teamNamesColorText'), 317 ) 318 assert bui.app.classic is not None 319 if not bui.app.classic.accounts.have_pro(): 320 bui.imagewidget( 321 parent=self.root_widget, 322 size=(30, 30), 323 position=(95, 202 + y_offs), 324 texture=bui.gettexture('lock'), 325 draw_controller=self._custom_colors_names_button, 326 ) 327 else: 328 self._custom_colors_names_button = None 329 330 # Shuffle. 331 def _cb_callback(val: bool) -> None: 332 self._do_randomize_val = val 333 cfg = bui.app.config 334 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 335 self._do_randomize_val 336 ) 337 cfg.commit() 338 339 if show_shuffle_check_box: 340 self._shuffle_check_box = bui.checkboxwidget( 341 parent=self.root_widget, 342 position=(110, 200), 343 scale=1.0, 344 size=(250, 30), 345 autoselect=True, 346 text=bui.Lstr(resource=f'{self._r}.shuffleGameOrderText'), 347 maxwidth=300, 348 textcolor=(0.8, 0.8, 0.8), 349 value=self._do_randomize_val, 350 on_value_change_call=_cb_callback, 351 ) 352 353 # Show tutorial. 354 show_tutorial = bool(bui.app.config.get('Show Tutorial', True)) 355 356 def _cb_callback_2(val: bool) -> None: 357 cfg = bui.app.config 358 cfg['Show Tutorial'] = val 359 cfg.commit() 360 361 self._show_tutorial_check_box = bui.checkboxwidget( 362 parent=self.root_widget, 363 position=(110, 151), 364 scale=1.0, 365 size=(250, 30), 366 autoselect=True, 367 text=bui.Lstr(resource=f'{self._r}.showTutorialText'), 368 maxwidth=300, 369 textcolor=(0.8, 0.8, 0.8), 370 value=show_tutorial, 371 on_value_change_call=_cb_callback_2, 372 ) 373 374 # Grumble: current autoselect doesn't do a very good job 375 # with checkboxes. 376 if self._custom_colors_names_button is not None: 377 for btn in bottom_row_buttons: 378 bui.widget( 379 edit=btn, down_widget=self._custom_colors_names_button 380 ) 381 if show_shuffle_check_box: 382 bui.widget( 383 edit=self._custom_colors_names_button, 384 down_widget=self._shuffle_check_box, 385 ) 386 bui.widget( 387 edit=self._shuffle_check_box, 388 up_widget=self._custom_colors_names_button, 389 ) 390 else: 391 bui.widget( 392 edit=self._custom_colors_names_button, 393 down_widget=self._show_tutorial_check_box, 394 ) 395 bui.widget( 396 edit=self._show_tutorial_check_box, 397 up_widget=self._custom_colors_names_button, 398 ) 399 400 self._ok_button = bui.buttonwidget( 401 parent=self.root_widget, 402 position=(70, 44), 403 size=(200, 45), 404 scale=1.8, 405 text_res_scale=1.5, 406 on_activate_call=self._on_ok_press, 407 autoselect=True, 408 label=bui.Lstr( 409 resource='okText' if self._selecting_mode else 'playText' 410 ), 411 ) 412 413 bui.widget( 414 edit=self._ok_button, up_widget=self._show_tutorial_check_box 415 ) 416 417 bui.containerwidget( 418 edit=self.root_widget, 419 start_button=self._ok_button, 420 cancel_button=self._cancel_button, 421 selected_child=self._ok_button, 422 ) 423 424 # Update now and once per second. 425 self._update_timer = bui.AppTimer( 426 1.0, bui.WeakCall(self._update), repeat=True 427 ) 428 self._update() 429 430 def _custom_colors_names_press(self) -> None: 431 from bauiv1lib.account import show_sign_in_prompt 432 from bauiv1lib.teamnamescolors import TeamNamesColorsWindow 433 from bauiv1lib.purchase import PurchaseWindow 434 435 plus = bui.app.plus 436 assert plus is not None 437 438 assert bui.app.classic is not None 439 if not bui.app.classic.accounts.have_pro(): 440 if plus.get_v1_account_state() != 'signed_in': 441 show_sign_in_prompt() 442 else: 443 PurchaseWindow(items=['pro']) 444 self._transition_out() 445 return 446 assert self._custom_colors_names_button 447 TeamNamesColorsWindow( 448 scale_origin=( 449 self._custom_colors_names_button.get_screen_space_center() 450 ) 451 ) 452 453 def _does_target_playlist_exist(self) -> bool: 454 if self._playlist == '__default__': 455 return True 456 return self._playlist in bui.app.config.get( 457 self._pvars.config_name + ' Playlists', {} 458 ) 459 460 def _update(self) -> None: 461 # All we do here is make sure our targeted playlist still exists, 462 # and close ourself if not. 463 if not self._does_target_playlist_exist(): 464 self._transition_out() 465 466 def _transition_out(self, transition: str = 'out_scale') -> None: 467 if not self._transitioning_out: 468 self._transitioning_out = True 469 bui.containerwidget(edit=self.root_widget, transition=transition) 470 471 @override 472 def on_popup_cancel(self) -> None: 473 bui.getsound('swish').play() 474 self._transition_out() 475 476 def _on_cancel_press(self) -> None: 477 self._transition_out() 478 479 def _on_ok_press(self) -> None: 480 # no-op if our underlying widget is dead or on its way out. 481 if not self.root_widget or self.root_widget.transitioning_out: 482 return 483 484 # Disallow if our playlist has disappeared. 485 if not self._does_target_playlist_exist(): 486 return 487 488 # Disallow if we have no unlocked games. 489 if not self._have_at_least_one_owned: 490 bui.getsound('error').play() 491 bui.screenmessage( 492 bui.Lstr(resource='playlistNoValidGamesErrorText'), 493 color=(1, 0, 0), 494 ) 495 return 496 497 cfg = bui.app.config 498 cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist 499 500 # Head back to the gather window in playlist-select mode 501 # or start the game in regular mode. 502 if self._selecting_mode: 503 from bauiv1lib.gather import GatherWindow 504 505 if self._sessiontype is bs.FreeForAllSession: 506 typename = 'ffa' 507 elif self._sessiontype is bs.DualTeamSession: 508 typename = 'teams' 509 else: 510 raise RuntimeError('Only teams and ffa currently supported') 511 cfg['Private Party Host Session Type'] = typename 512 bui.getsound('gunCocking').play() 513 assert bui.app.classic is not None 514 # Note: this is a wonky situation where we aren't actually 515 # the main window but we set it on behalf of the main window 516 # that popped us up. 517 bui.app.ui_v1.set_main_window( 518 GatherWindow(transition='in_right'), 519 from_window=False, # Disable this test. 520 ) 521 self._transition_out(transition='out_left') 522 if self._delegate is not None: 523 self._delegate.on_play_options_window_run_game() 524 else: 525 bui.fade_screen(False, endcall=self._run_selected_playlist) 526 bui.lock_all_input() 527 self._transition_out(transition='out_left') 528 if self._delegate is not None: 529 self._delegate.on_play_options_window_run_game() 530 531 cfg.commit() 532 533 def _run_selected_playlist(self) -> None: 534 bui.unlock_all_input() 535 try: 536 bs.new_host_session(self._sessiontype) 537 except Exception: 538 from bascenev1lib import mainmenu 539 540 logging.exception('Error running session %s.', self._sessiontype) 541 542 # Drop back into a main menu session. 543 bs.new_host_session(mainmenu.MainMenuSession)
A popup window for configuring play options.
PlayOptionsWindow( sessiontype: type[bascenev1.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None)
23 def __init__( 24 self, 25 sessiontype: type[bs.Session], 26 playlist: str, 27 scale_origin: tuple[float, float], 28 delegate: Any = None, 29 ): 30 # FIXME: Tidy this up. 31 # pylint: disable=too-many-branches 32 # pylint: disable=too-many-statements 33 # pylint: disable=too-many-locals 34 from bascenev1 import filter_playlist, get_map_class 35 from bauiv1lib.playlist import PlaylistTypeVars 36 from bauiv1lib.config import ConfigNumberEdit 37 38 self._r = 'gameListWindow' 39 self._delegate = delegate 40 self._pvars = PlaylistTypeVars(sessiontype) 41 self._transitioning_out = False 42 43 # We behave differently if we're being used for playlist selection 44 # vs starting a game directly (should make this more elegant). 45 classic = bui.app.classic 46 assert classic is not None 47 self._selecting_mode = classic.selecting_private_party_playlist 48 49 self._do_randomize_val = bui.app.config.get( 50 self._pvars.config_name + ' Playlist Randomize', 0 51 ) 52 53 self._sessiontype = sessiontype 54 self._playlist = playlist 55 56 self._width = 500.0 57 self._height = 370.0 - 50.0 58 59 # In teams games, show the custom names/colors button. 60 if self._sessiontype is bs.DualTeamSession: 61 self._height += 50.0 62 63 self._row_height = 45.0 64 65 # Grab our maps to display. 66 mesh_opaque = bui.getmesh('level_select_button_opaque') 67 mesh_transparent = bui.getmesh('level_select_button_transparent') 68 mask_tex = bui.gettexture('mapPreviewMask') 69 70 # Poke into this playlist and see if we can display some of its maps. 71 map_textures = [] 72 map_texture_entries = [] 73 rows = 0 74 columns = 0 75 game_count = 0 76 scl = 0.35 77 c_width_total = 0.0 78 try: 79 max_columns = 5 80 name = playlist 81 if name == '__default__': 82 plst = self._pvars.get_default_list_call() 83 else: 84 try: 85 plst = bui.app.config[ 86 self._pvars.config_name + ' Playlists' 87 ][name] 88 except Exception: 89 print( 90 'ERROR INFO: self._config_name is:', 91 self._pvars.config_name, 92 ) 93 print( 94 'ERROR INFO: playlist names are:', 95 list( 96 bui.app.config[ 97 self._pvars.config_name + ' Playlists' 98 ].keys() 99 ), 100 ) 101 raise 102 plst = filter_playlist( 103 plst, 104 self._sessiontype, 105 remove_unowned=False, 106 mark_unowned=True, 107 name=name, 108 ) 109 game_count = len(plst) 110 for entry in plst: 111 mapname = entry['settings']['map'] 112 maptype: type[bs.Map] | None 113 try: 114 maptype = get_map_class(mapname) 115 except bui.NotFoundError: 116 maptype = None 117 if maptype is not None: 118 tex_name = maptype.get_preview_texture_name() 119 if tex_name is not None: 120 map_textures.append(tex_name) 121 map_texture_entries.append(entry) 122 rows = (max(0, len(map_textures) - 1) // max_columns) + 1 123 columns = min(max_columns, len(map_textures)) 124 125 if len(map_textures) == 1: 126 scl = 1.1 127 elif len(map_textures) == 2: 128 scl = 0.7 129 elif len(map_textures) == 3: 130 scl = 0.55 131 else: 132 scl = 0.35 133 self._row_height = 128.0 * scl 134 c_width_total = scl * 250.0 * columns 135 if map_textures: 136 self._height += self._row_height * rows 137 138 except Exception: 139 logging.exception('Error listing playlist maps.') 140 141 show_shuffle_check_box = game_count > 1 142 143 if show_shuffle_check_box: 144 self._height += 40 145 146 uiscale = bui.app.ui_v1.uiscale 147 scale = ( 148 1.69 149 if uiscale is bui.UIScale.SMALL 150 else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.85 151 ) 152 # Creates our _root_widget. 153 super().__init__( 154 position=scale_origin, size=(self._width, self._height), scale=scale 155 ) 156 157 playlist_name: str | bui.Lstr = ( 158 self._pvars.default_list_name 159 if playlist == '__default__' 160 else playlist 161 ) 162 self._title_text = bui.textwidget( 163 parent=self.root_widget, 164 position=(self._width * 0.5, self._height - 89 + 51), 165 size=(0, 0), 166 text=playlist_name, 167 scale=1.4, 168 color=(1, 1, 1), 169 maxwidth=self._width * 0.7, 170 h_align='center', 171 v_align='center', 172 ) 173 174 self._cancel_button = bui.buttonwidget( 175 parent=self.root_widget, 176 position=(25, self._height - 53), 177 size=(50, 50), 178 scale=0.7, 179 label='', 180 color=(0.42, 0.73, 0.2), 181 on_activate_call=self._on_cancel_press, 182 autoselect=True, 183 icon=bui.gettexture('crossOut'), 184 iconscale=1.2, 185 ) 186 187 h_offs_img = self._width * 0.5 - c_width_total * 0.5 188 v_offs_img = self._height - 118 - scl * 125.0 + 50 189 bottom_row_buttons = [] 190 self._have_at_least_one_owned = False 191 192 for row in range(rows): 193 for col in range(columns): 194 tex_index = row * columns + col 195 if tex_index < len(map_textures): 196 tex_name = map_textures[tex_index] 197 h = h_offs_img + scl * 250 * col 198 v = v_offs_img - self._row_height * row 199 entry = map_texture_entries[tex_index] 200 owned = not ( 201 ('is_unowned_map' in entry and entry['is_unowned_map']) 202 or ( 203 'is_unowned_game' in entry 204 and entry['is_unowned_game'] 205 ) 206 ) 207 208 if owned: 209 self._have_at_least_one_owned = True 210 211 try: 212 desc = bui.getclass( 213 entry['type'], subclassof=bs.GameActivity 214 ).get_settings_display_string(entry) 215 if not owned: 216 desc = bui.Lstr( 217 value='${DESC}\n${UNLOCK}', 218 subs=[ 219 ('${DESC}', desc), 220 ( 221 '${UNLOCK}', 222 bui.Lstr( 223 resource='unlockThisInTheStoreText' 224 ), 225 ), 226 ], 227 ) 228 desc_color = (0, 1, 0) if owned else (1, 0, 0) 229 except Exception: 230 desc = bui.Lstr(value='(invalid)') 231 desc_color = (1, 0, 0) 232 233 btn = bui.buttonwidget( 234 parent=self.root_widget, 235 size=(scl * 240.0, scl * 120.0), 236 position=(h, v), 237 texture=bui.gettexture(tex_name if owned else 'empty'), 238 mesh_opaque=mesh_opaque if owned else None, 239 on_activate_call=bui.Call( 240 bui.screenmessage, desc, desc_color 241 ), 242 label='', 243 color=(1, 1, 1), 244 autoselect=True, 245 extra_touch_border_scale=0.0, 246 mesh_transparent=mesh_transparent if owned else None, 247 mask_texture=mask_tex if owned else None, 248 ) 249 if row == 0 and col == 0: 250 bui.widget(edit=self._cancel_button, down_widget=btn) 251 if row == rows - 1: 252 bottom_row_buttons.append(btn) 253 if not owned: 254 # Ewww; buttons don't currently have alpha so in this 255 # case we draw an image over our button with an empty 256 # texture on it. 257 bui.imagewidget( 258 parent=self.root_widget, 259 size=(scl * 260.0, scl * 130.0), 260 position=(h - 10.0 * scl, v - 4.0 * scl), 261 draw_controller=btn, 262 color=(1, 1, 1), 263 texture=bui.gettexture(tex_name), 264 mesh_opaque=mesh_opaque, 265 opacity=0.25, 266 mesh_transparent=mesh_transparent, 267 mask_texture=mask_tex, 268 ) 269 270 bui.imagewidget( 271 parent=self.root_widget, 272 size=(scl * 100, scl * 100), 273 draw_controller=btn, 274 position=(h + scl * 70, v + scl * 10), 275 texture=bui.gettexture('lock'), 276 ) 277 278 y_offs = 50 if show_shuffle_check_box else 0 279 280 # Series Length 281 y_offs2 = 40 if self._sessiontype is bs.DualTeamSession else 0 282 self._series_length_numedit = ConfigNumberEdit( 283 parent=self.root_widget, 284 position=(100, 200 + y_offs + y_offs2), 285 configkey=( 286 'FFA' if self._sessiontype is bs.FreeForAllSession else 'Teams' 287 ) 288 + ' Series Length', 289 displayname=bui.Lstr( 290 resource=self._r 291 + ( 292 '.pointsToWinText' 293 if self._sessiontype is bs.FreeForAllSession 294 else '.seriesLengthText' 295 ) 296 ), 297 minval=1.0, 298 maxval=100.0 if self._sessiontype is bs.FreeForAllSession else 99.0, 299 increment=1.0 if self._sessiontype is bs.FreeForAllSession else 2.0, 300 fallback_value=( 301 24 if self._sessiontype is bs.FreeForAllSession else 7 302 ), 303 f=0, 304 ) 305 306 # Team names/colors. 307 self._custom_colors_names_button: bui.Widget | None 308 if self._sessiontype is bs.DualTeamSession: 309 self._custom_colors_names_button = bui.buttonwidget( 310 parent=self.root_widget, 311 position=(100, 195 + y_offs), 312 size=(290, 35), 313 on_activate_call=bui.WeakCall(self._custom_colors_names_press), 314 autoselect=True, 315 textcolor=(0.8, 0.8, 0.8), 316 label=bui.Lstr(resource='teamNamesColorText'), 317 ) 318 assert bui.app.classic is not None 319 if not bui.app.classic.accounts.have_pro(): 320 bui.imagewidget( 321 parent=self.root_widget, 322 size=(30, 30), 323 position=(95, 202 + y_offs), 324 texture=bui.gettexture('lock'), 325 draw_controller=self._custom_colors_names_button, 326 ) 327 else: 328 self._custom_colors_names_button = None 329 330 # Shuffle. 331 def _cb_callback(val: bool) -> None: 332 self._do_randomize_val = val 333 cfg = bui.app.config 334 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 335 self._do_randomize_val 336 ) 337 cfg.commit() 338 339 if show_shuffle_check_box: 340 self._shuffle_check_box = bui.checkboxwidget( 341 parent=self.root_widget, 342 position=(110, 200), 343 scale=1.0, 344 size=(250, 30), 345 autoselect=True, 346 text=bui.Lstr(resource=f'{self._r}.shuffleGameOrderText'), 347 maxwidth=300, 348 textcolor=(0.8, 0.8, 0.8), 349 value=self._do_randomize_val, 350 on_value_change_call=_cb_callback, 351 ) 352 353 # Show tutorial. 354 show_tutorial = bool(bui.app.config.get('Show Tutorial', True)) 355 356 def _cb_callback_2(val: bool) -> None: 357 cfg = bui.app.config 358 cfg['Show Tutorial'] = val 359 cfg.commit() 360 361 self._show_tutorial_check_box = bui.checkboxwidget( 362 parent=self.root_widget, 363 position=(110, 151), 364 scale=1.0, 365 size=(250, 30), 366 autoselect=True, 367 text=bui.Lstr(resource=f'{self._r}.showTutorialText'), 368 maxwidth=300, 369 textcolor=(0.8, 0.8, 0.8), 370 value=show_tutorial, 371 on_value_change_call=_cb_callback_2, 372 ) 373 374 # Grumble: current autoselect doesn't do a very good job 375 # with checkboxes. 376 if self._custom_colors_names_button is not None: 377 for btn in bottom_row_buttons: 378 bui.widget( 379 edit=btn, down_widget=self._custom_colors_names_button 380 ) 381 if show_shuffle_check_box: 382 bui.widget( 383 edit=self._custom_colors_names_button, 384 down_widget=self._shuffle_check_box, 385 ) 386 bui.widget( 387 edit=self._shuffle_check_box, 388 up_widget=self._custom_colors_names_button, 389 ) 390 else: 391 bui.widget( 392 edit=self._custom_colors_names_button, 393 down_widget=self._show_tutorial_check_box, 394 ) 395 bui.widget( 396 edit=self._show_tutorial_check_box, 397 up_widget=self._custom_colors_names_button, 398 ) 399 400 self._ok_button = bui.buttonwidget( 401 parent=self.root_widget, 402 position=(70, 44), 403 size=(200, 45), 404 scale=1.8, 405 text_res_scale=1.5, 406 on_activate_call=self._on_ok_press, 407 autoselect=True, 408 label=bui.Lstr( 409 resource='okText' if self._selecting_mode else 'playText' 410 ), 411 ) 412 413 bui.widget( 414 edit=self._ok_button, up_widget=self._show_tutorial_check_box 415 ) 416 417 bui.containerwidget( 418 edit=self.root_widget, 419 start_button=self._ok_button, 420 cancel_button=self._cancel_button, 421 selected_child=self._ok_button, 422 ) 423 424 # Update now and once per second. 425 self._update_timer = bui.AppTimer( 426 1.0, bui.WeakCall(self._update), repeat=True 427 ) 428 self._update()
@override
def
on_popup_cancel(self) -> None:
471 @override 472 def on_popup_cancel(self) -> None: 473 bui.getsound('swish').play() 474 self._transition_out()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.