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