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