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 sessiontype: type[bs.Session], 27 playlist: str, 28 scale_origin: tuple[float, float], 29 delegate: Any = None, 30 playlist_select_context: PlaylistSelectContext | None = None, 31 ): 32 # FIXME: Tidy this up. 33 # pylint: disable=too-many-branches 34 # pylint: disable=too-many-statements 35 # pylint: disable=too-many-locals 36 from bascenev1 import filter_playlist, get_map_class 37 from bauiv1lib.playlist import PlaylistTypeVars 38 from bauiv1lib.config import ConfigNumberEdit 39 40 self._r = 'gameListWindow' 41 self._delegate = delegate 42 self._pvars = PlaylistTypeVars(sessiontype) 43 self._transitioning_out = False 44 45 self._playlist_select_context = playlist_select_context 46 47 self._do_randomize_val = bui.app.config.get( 48 self._pvars.config_name + ' Playlist Randomize', 0 49 ) 50 51 self._sessiontype = sessiontype 52 self._playlist = playlist 53 54 self._width = 500.0 55 self._height = 370.0 - 50.0 56 57 # In teams games, show the custom names/colors button. 58 if self._sessiontype is bs.DualTeamSession: 59 self._height += 50.0 60 61 self._row_height = 45.0 62 63 # Grab our maps to display. 64 mesh_opaque = bui.getmesh('level_select_button_opaque') 65 mesh_transparent = bui.getmesh('level_select_button_transparent') 66 mask_tex = bui.gettexture('mapPreviewMask') 67 68 # Poke into this playlist and see if we can display some of its 69 # 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=( 409 'okText' 410 if self._playlist_select_context is not None 411 else 'playText' 412 ) 413 ), 414 ) 415 416 bui.widget( 417 edit=self._ok_button, up_widget=self._show_tutorial_check_box 418 ) 419 420 bui.containerwidget( 421 edit=self.root_widget, 422 start_button=self._ok_button, 423 cancel_button=self._cancel_button, 424 selected_child=self._ok_button, 425 ) 426 427 # Update now and once per second. 428 self._update_timer = bui.AppTimer( 429 1.0, bui.WeakCall(self._update), repeat=True 430 ) 431 self._update() 432 433 def _custom_colors_names_press(self) -> None: 434 from bauiv1lib.account import show_sign_in_prompt 435 from bauiv1lib.teamnamescolors import TeamNamesColorsWindow 436 from bauiv1lib.purchase import PurchaseWindow 437 438 plus = bui.app.plus 439 assert plus is not None 440 441 assert bui.app.classic is not None 442 if not bui.app.classic.accounts.have_pro(): 443 if plus.get_v1_account_state() != 'signed_in': 444 show_sign_in_prompt() 445 else: 446 PurchaseWindow(items=['pro']) 447 self._transition_out() 448 return 449 assert self._custom_colors_names_button 450 TeamNamesColorsWindow( 451 scale_origin=( 452 self._custom_colors_names_button.get_screen_space_center() 453 ) 454 ) 455 456 def _does_target_playlist_exist(self) -> bool: 457 if self._playlist == '__default__': 458 return True 459 return self._playlist in bui.app.config.get( 460 self._pvars.config_name + ' Playlists', {} 461 ) 462 463 def _update(self) -> None: 464 # All we do here is make sure our targeted playlist still exists, 465 # and close ourself if not. 466 if not self._does_target_playlist_exist(): 467 self._transition_out() 468 469 def _transition_out(self, transition: str = 'out_scale') -> None: 470 if not self._transitioning_out: 471 self._transitioning_out = True 472 bui.containerwidget(edit=self.root_widget, transition=transition) 473 474 @override 475 def on_popup_cancel(self) -> None: 476 bui.getsound('swish').play() 477 self._transition_out() 478 479 def _on_cancel_press(self) -> None: 480 self._transition_out() 481 482 def _on_ok_press(self) -> None: 483 # no-op if our underlying widget is dead or on its way out. 484 if not self.root_widget or self.root_widget.transitioning_out: 485 return 486 487 # Disallow if our playlist has disappeared. 488 if not self._does_target_playlist_exist(): 489 return 490 491 # Disallow if we have no unlocked games. 492 if not self._have_at_least_one_owned: 493 bui.getsound('error').play() 494 bui.screenmessage( 495 bui.Lstr(resource='playlistNoValidGamesErrorText'), 496 color=(1, 0, 0), 497 ) 498 return 499 500 cfg = bui.app.config 501 cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist 502 503 # Head back to the gather window in playlist-select mode or 504 # start the game in regular mode. 505 if self._playlist_select_context is not None: 506 # from bauiv1lib.gather import GatherWindow 507 508 if self._sessiontype is bs.FreeForAllSession: 509 typename = 'ffa' 510 elif self._sessiontype is bs.DualTeamSession: 511 typename = 'teams' 512 else: 513 raise RuntimeError('Only teams and ffa currently supported') 514 cfg['Private Party Host Session Type'] = typename 515 bui.getsound('gunCocking').play() 516 517 self._transition_out(transition='out_left') 518 if self._delegate is not None: 519 self._delegate.on_play_options_window_run_game() 520 else: 521 bui.fade_screen(False, endcall=self._run_selected_playlist) 522 bui.lock_all_input() 523 self._transition_out(transition='out_left') 524 if self._delegate is not None: 525 self._delegate.on_play_options_window_run_game() 526 527 cfg.commit() 528 529 def _run_selected_playlist(self) -> None: 530 bui.unlock_all_input() 531 try: 532 bs.new_host_session(self._sessiontype) 533 except Exception: 534 from bascenev1lib import mainmenu 535 536 logging.exception('Error running session %s.', self._sessiontype) 537 538 # Drop back into a main menu session. 539 bs.new_host_session(mainmenu.MainMenuSession)
22class PlayOptionsWindow(PopupWindow): 23 """A popup window for configuring play options.""" 24 25 def __init__( 26 self, 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 try: 533 bs.new_host_session(self._sessiontype) 534 except Exception: 535 from bascenev1lib import mainmenu 536 537 logging.exception('Error running session %s.', self._sessiontype) 538 539 # Drop back into a main menu session. 540 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 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()
@override
def
on_popup_cancel(self) -> None:
475 @override 476 def on_popup_cancel(self) -> None: 477 bui.getsound('swish').play() 478 self._transition_out()
Called when the popup is canceled.
Cancels can occur due to clicking outside the window, hitting escape, etc.