def _build_hosting_config(self) -> HostingConfig: from bastd.ui.playlist import PlaylistTypeVars from ba.internal import filter_playlist hcfg = HostingConfig() cfg = ba.app.config sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') if not isinstance(sessiontypestr, str): raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') hcfg.session_type = sessiontypestr sessiontype: Type[ba.Session] if hcfg.session_type == 'ffa': sessiontype = ba.FreeForAllSession elif hcfg.session_type == 'teams': sessiontype = ba.DualTeamSession else: raise RuntimeError('fInvalid sessiontype: {hcfg.session_type}') pvars = PlaylistTypeVars(sessiontype) playlist_name = ba.app.config.get( f'{pvars.config_name} Playlist Selection') if not isinstance(playlist_name, str): playlist_name = '__default__' hcfg.playlist_name = (pvars.default_list_name.evaluate() if playlist_name == '__default__' else playlist_name) if playlist_name == '__default__': playlist = pvars.get_default_list_call() else: playlist = cfg[f'{pvars.config_name} Playlists'][playlist_name] hcfg.playlist = filter_playlist(playlist, sessiontype) randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') if not isinstance(randomize, bool): randomize = False hcfg.randomize = randomize tutorial = cfg.get('Show Tutorial') if not isinstance(tutorial, bool): tutorial = False hcfg.tutorial = tutorial if hcfg.session_type == 'teams': hcfg.custom_team_names = copy.copy(cfg.get('Custom Team Names')) hcfg.custom_team_colors = copy.copy(cfg.get('Custom Team Colors')) return hcfg
class PlayOptionsWindow(popup.PopupWindow): """A popup window for configuring play options.""" def __init__(self, sessiontype: type[ba.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None): # FIXME: Tidy this up. # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-locals from ba.internal import get_map_class, getclass, filter_playlist from bastd.ui.playlist import PlaylistTypeVars self._r = 'gameListWindow' self._delegate = delegate self._pvars = PlaylistTypeVars(sessiontype) self._transitioning_out = False # We behave differently if we're being used for playlist selection # vs starting a game directly (should make this more elegant). self._selecting_mode = ba.app.ui.selecting_private_party_playlist self._do_randomize_val = (ba.app.config.get( self._pvars.config_name + ' Playlist Randomize', 0)) self._sessiontype = sessiontype self._playlist = playlist self._width = 500.0 self._height = 330.0 - 50.0 # In teams games, show the custom names/colors button. if self._sessiontype is ba.DualTeamSession: self._height += 50.0 self._row_height = 45.0 # Grab our maps to display. model_opaque = ba.getmodel('level_select_button_opaque') model_transparent = ba.getmodel('level_select_button_transparent') mask_tex = ba.gettexture('mapPreviewMask') # Poke into this playlist and see if we can display some of its maps. map_textures = [] map_texture_entries = [] rows = 0 columns = 0 game_count = 0 scl = 0.35 c_width_total = 0.0 try: max_columns = 5 name = playlist if name == '__default__': plst = self._pvars.get_default_list_call() else: try: plst = ba.app.config[self._pvars.config_name + ' Playlists'][name] except Exception: print('ERROR INFO: self._config_name is:', self._pvars.config_name) print( 'ERROR INFO: playlist names are:', list(ba.app.config[self._pvars.config_name + ' Playlists'].keys())) raise plst = filter_playlist(plst, self._sessiontype, remove_unowned=False, mark_unowned=True) game_count = len(plst) for entry in plst: mapname = entry['settings']['map'] maptype: Optional[type[ba.Map]] try: maptype = get_map_class(mapname) except ba.NotFoundError: maptype = None if maptype is not None: tex_name = maptype.get_preview_texture_name() if tex_name is not None: map_textures.append(tex_name) map_texture_entries.append(entry) rows = (max(0, len(map_textures) - 1) // max_columns) + 1 columns = min(max_columns, len(map_textures)) if len(map_textures) == 1: scl = 1.1 elif len(map_textures) == 2: scl = 0.7 elif len(map_textures) == 3: scl = 0.55 else: scl = 0.35 self._row_height = 128.0 * scl c_width_total = scl * 250.0 * columns if map_textures: self._height += self._row_height * rows except Exception: ba.print_exception('Error listing playlist maps.') show_shuffle_check_box = game_count > 1 if show_shuffle_check_box: self._height += 40 # Creates our _root_widget. uiscale = ba.app.ui.uiscale scale = (1.69 if uiscale is ba.UIScale.SMALL else 1.1 if uiscale is ba.UIScale.MEDIUM else 0.85) super().__init__(position=scale_origin, size=(self._width, self._height), scale=scale) playlist_name: Union[str, ba.Lstr] = (self._pvars.default_list_name if playlist == '__default__' else playlist) self._title_text = ba.textwidget(parent=self.root_widget, position=(self._width * 0.5, self._height - 89 + 51), size=(0, 0), text=playlist_name, scale=1.4, color=(1, 1, 1), maxwidth=self._width * 0.7, h_align='center', v_align='center') self._cancel_button = ba.buttonwidget( parent=self.root_widget, position=(25, self._height - 53), size=(50, 50), scale=0.7, label='', color=(0.42, 0.73, 0.2), on_activate_call=self._on_cancel_press, autoselect=True, icon=ba.gettexture('crossOut'), iconscale=1.2) h_offs_img = self._width * 0.5 - c_width_total * 0.5 v_offs_img = self._height - 118 - scl * 125.0 + 50 bottom_row_buttons = [] self._have_at_least_one_owned = False for row in range(rows): for col in range(columns): tex_index = row * columns + col if tex_index < len(map_textures): tex_name = map_textures[tex_index] h = h_offs_img + scl * 250 * col v = v_offs_img - self._row_height * row entry = map_texture_entries[tex_index] owned = not (('is_unowned_map' in entry and entry['is_unowned_map']) or ('is_unowned_game' in entry and entry['is_unowned_game'])) if owned: self._have_at_least_one_owned = True try: desc = getclass(entry['type'], subclassof=ba.GameActivity ).get_settings_display_string(entry) if not owned: desc = ba.Lstr( value='${DESC}\n${UNLOCK}', subs=[ ('${DESC}', desc), ('${UNLOCK}', ba.Lstr( resource='unlockThisInTheStoreText')) ]) desc_color = (0, 1, 0) if owned else (1, 0, 0) except Exception: desc = ba.Lstr(value='(invalid)') desc_color = (1, 0, 0) btn = ba.buttonwidget( parent=self.root_widget, size=(scl * 240.0, scl * 120.0), position=(h, v), texture=ba.gettexture(tex_name if owned else 'empty'), model_opaque=model_opaque if owned else None, on_activate_call=ba.Call(ba.screenmessage, desc, desc_color), label='', color=(1, 1, 1), autoselect=True, extra_touch_border_scale=0.0, model_transparent=model_transparent if owned else None, mask_texture=mask_tex if owned else None) if row == 0 and col == 0: ba.widget(edit=self._cancel_button, down_widget=btn) if row == rows - 1: bottom_row_buttons.append(btn) if not owned: # Ewww; buttons don't currently have alpha so in this # case we draw an image over our button with an empty # texture on it. ba.imagewidget(parent=self.root_widget, size=(scl * 260.0, scl * 130.0), position=(h - 10.0 * scl, v - 4.0 * scl), draw_controller=btn, color=(1, 1, 1), texture=ba.gettexture(tex_name), model_opaque=model_opaque, opacity=0.25, model_transparent=model_transparent, mask_texture=mask_tex) ba.imagewidget(parent=self.root_widget, size=(scl * 100, scl * 100), draw_controller=btn, position=(h + scl * 70, v + scl * 10), texture=ba.gettexture('lock')) # Team names/colors. self._custom_colors_names_button: Optional[ba.Widget] if self._sessiontype is ba.DualTeamSession: y_offs = 50 if show_shuffle_check_box else 0 self._custom_colors_names_button = ba.buttonwidget( parent=self.root_widget, position=(100, 200 + y_offs), size=(290, 35), on_activate_call=ba.WeakCall(self._custom_colors_names_press), autoselect=True, textcolor=(0.8, 0.8, 0.8), label=ba.Lstr(resource='teamNamesColorText')) if not ba.app.accounts_v1.have_pro(): ba.imagewidget( parent=self.root_widget, size=(30, 30), position=(95, 202 + y_offs), texture=ba.gettexture('lock'), draw_controller=self._custom_colors_names_button) else: self._custom_colors_names_button = None # Shuffle. def _cb_callback(val: bool) -> None: self._do_randomize_val = val cfg = ba.app.config cfg[self._pvars.config_name + ' Playlist Randomize'] = self._do_randomize_val cfg.commit() if show_shuffle_check_box: self._shuffle_check_box = ba.checkboxwidget( parent=self.root_widget, position=(110, 200), scale=1.0, size=(250, 30), autoselect=True, text=ba.Lstr(resource=self._r + '.shuffleGameOrderText'), maxwidth=300, textcolor=(0.8, 0.8, 0.8), value=self._do_randomize_val, on_value_change_call=_cb_callback) # Show tutorial. show_tutorial = bool(ba.app.config.get('Show Tutorial', True)) def _cb_callback_2(val: bool) -> None: cfg = ba.app.config cfg['Show Tutorial'] = val cfg.commit() self._show_tutorial_check_box = ba.checkboxwidget( parent=self.root_widget, position=(110, 151), scale=1.0, size=(250, 30), autoselect=True, text=ba.Lstr(resource=self._r + '.showTutorialText'), maxwidth=300, textcolor=(0.8, 0.8, 0.8), value=show_tutorial, on_value_change_call=_cb_callback_2) # Grumble: current autoselect doesn't do a very good job # with checkboxes. if self._custom_colors_names_button is not None: for btn in bottom_row_buttons: ba.widget(edit=btn, down_widget=self._custom_colors_names_button) if show_shuffle_check_box: ba.widget(edit=self._custom_colors_names_button, down_widget=self._shuffle_check_box) ba.widget(edit=self._shuffle_check_box, up_widget=self._custom_colors_names_button) else: ba.widget(edit=self._custom_colors_names_button, down_widget=self._show_tutorial_check_box) ba.widget(edit=self._show_tutorial_check_box, up_widget=self._custom_colors_names_button) self._ok_button = ba.buttonwidget( parent=self.root_widget, position=(70, 44), size=(200, 45), scale=1.8, text_res_scale=1.5, on_activate_call=self._on_ok_press, autoselect=True, label=ba.Lstr( resource='okText' if self._selecting_mode else 'playText')) ba.widget(edit=self._ok_button, up_widget=self._show_tutorial_check_box) ba.containerwidget(edit=self.root_widget, start_button=self._ok_button, cancel_button=self._cancel_button, selected_child=self._ok_button) # Update now and once per second. self._update_timer = ba.Timer(1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True) self._update() def _custom_colors_names_press(self) -> None: from bastd.ui.account import show_sign_in_prompt from bastd.ui.teamnamescolors import TeamNamesColorsWindow from bastd.ui.purchase import PurchaseWindow if not ba.app.accounts_v1.have_pro(): if _ba.get_v1_account_state() != 'signed_in': show_sign_in_prompt() else: PurchaseWindow(items=['pro']) self._transition_out() return assert self._custom_colors_names_button TeamNamesColorsWindow(scale_origin=self._custom_colors_names_button. get_screen_space_center()) def _does_target_playlist_exist(self) -> bool: if self._playlist == '__default__': return True return self._playlist in ba.app.config.get( self._pvars.config_name + ' Playlists', {}) def _update(self) -> None: # All we do here is make sure our targeted playlist still exists, # and close ourself if not. if not self._does_target_playlist_exist(): self._transition_out() def _transition_out(self, transition: str = 'out_scale') -> None: if not self._transitioning_out: self._transitioning_out = True ba.containerwidget(edit=self.root_widget, transition=transition) def on_popup_cancel(self) -> None: ba.playsound(ba.getsound('swish')) self._transition_out() def _on_cancel_press(self) -> None: self._transition_out() def _on_ok_press(self) -> None: # Disallow if our playlist has disappeared. if not self._does_target_playlist_exist(): return # Disallow if we have no unlocked games. if not self._have_at_least_one_owned: ba.playsound(ba.getsound('error')) ba.screenmessage(ba.Lstr(resource='playlistNoValidGamesErrorText'), color=(1, 0, 0)) return cfg = ba.app.config cfg[self._pvars.config_name + ' Playlist Selection'] = self._playlist # Head back to the gather window in playlist-select mode # or start the game in regular mode. if self._selecting_mode: from bastd.ui.gather import GatherWindow if self._sessiontype is ba.FreeForAllSession: typename = 'ffa' elif self._sessiontype is ba.DualTeamSession: typename = 'teams' else: raise RuntimeError('Only teams and ffa currently supported') cfg['Private Party Host Session Type'] = typename ba.playsound(ba.getsound('gunCocking')) ba.app.ui.set_main_menu_window( GatherWindow(transition='in_right').get_root_widget()) self._transition_out(transition='out_left') if self._delegate is not None: self._delegate.on_play_options_window_run_game() else: _ba.fade_screen(False, endcall=self._run_selected_playlist) _ba.lock_all_input() self._transition_out(transition='out_left') if self._delegate is not None: self._delegate.on_play_options_window_run_game() cfg.commit() def _run_selected_playlist(self) -> None: _ba.unlock_all_input() try: _ba.new_host_session(self._sessiontype) except Exception: from bastd import mainmenu ba.print_exception('exception running session', self._sessiontype) # Drop back into a main menu session. _ba.new_host_session(mainmenu.MainMenuSession)
def _build_hosting_config(self) -> PrivateHostingConfig: # pylint: disable=too-many-branches from bastd.ui.playlist import PlaylistTypeVars from ba.internal import filter_playlist hcfg = PrivateHostingConfig() cfg = ba.app.config sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa') if not isinstance(sessiontypestr, str): raise RuntimeError(f'Invalid sessiontype {sessiontypestr}') hcfg.session_type = sessiontypestr sessiontype: type[ba.Session] if hcfg.session_type == 'ffa': sessiontype = ba.FreeForAllSession elif hcfg.session_type == 'teams': sessiontype = ba.DualTeamSession else: raise RuntimeError('fInvalid sessiontype: {hcfg.session_type}') pvars = PlaylistTypeVars(sessiontype) playlist_name = ba.app.config.get( f'{pvars.config_name} Playlist Selection') if not isinstance(playlist_name, str): playlist_name = '__default__' hcfg.playlist_name = (pvars.default_list_name.evaluate() if playlist_name == '__default__' else playlist_name) playlist: Optional[list[dict[str, Any]]] = None if playlist_name != '__default__': playlist = (cfg.get(f'{pvars.config_name} Playlists', {}).get(playlist_name)) if playlist is None: playlist = pvars.get_default_list_call() hcfg.playlist = filter_playlist(playlist, sessiontype) randomize = cfg.get(f'{pvars.config_name} Playlist Randomize') if not isinstance(randomize, bool): randomize = False hcfg.randomize = randomize tutorial = cfg.get('Show Tutorial') if not isinstance(tutorial, bool): tutorial = True hcfg.tutorial = tutorial if hcfg.session_type == 'teams': ctn: Optional[list[str]] = cfg.get('Custom Team Names') if ctn is not None: if (isinstance(ctn, (list, tuple)) and len(ctn) == 2 and all(isinstance(x, str) for x in ctn)): hcfg.custom_team_names = (ctn[0], ctn[1]) else: print(f'Found invalid custom-team-names data: {ctn}') ctc: Optional[list[list[float]]] = cfg.get('Custom Team Colors') if ctc is not None: if (isinstance(ctc, (list, tuple)) and len(ctc) == 2 and all(isinstance(x, (list, tuple)) for x in ctc) and all(len(x) == 3 for x in ctc)): hcfg.custom_team_colors = ((ctc[0][0], ctc[0][1], ctc[0][2]), (ctc[1][0], ctc[1][1], ctc[1][2])) else: print(f'Found invalid custom-team-colors data: {ctc}') return hcfg
class PlaylistBrowserWindow(ba.Window): """Window for starting teams games.""" def __init__(self, sessiontype: type[ba.Session], transition: Optional[str] = 'in_right', origin_widget: ba.Widget = None): # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bastd.ui.playlist import PlaylistTypeVars # If they provided an origin-widget, scale up from that. scale_origin: Optional[tuple[float, float]] if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None # Store state for when we exit the next game. if issubclass(sessiontype, ba.DualTeamSession): ba.app.ui.set_main_menu_location('Team Game Select') ba.set_analytics_screen('Teams Window') elif issubclass(sessiontype, ba.FreeForAllSession): ba.app.ui.set_main_menu_location('Free-for-All Game Select') ba.set_analytics_screen('FreeForAll Window') else: raise TypeError(f'Invalid sessiontype: {sessiontype}.') self._pvars = PlaylistTypeVars(sessiontype) self._sessiontype = sessiontype self._customize_button: Optional[ba.Widget] = None self._sub_width: Optional[float] = None self._sub_height: Optional[float] = None self._ensure_standard_playlists_exist() # Get the current selection (if any). self._selected_playlist = ba.app.config.get(self._pvars.config_name + ' Playlist Selection') uiscale = ba.app.ui.uiscale self._width = 900 if uiscale is ba.UIScale.SMALL else 800 x_inset = 50 if uiscale is ba.UIScale.SMALL else 0 self._height = (480 if uiscale is ba.UIScale.SMALL else 510 if uiscale is ba.UIScale.MEDIUM else 580) top_extra = 20 if uiscale is ba.UIScale.SMALL else 0 super().__init__(root_widget=ba.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_full', scale_origin_stack_offset=scale_origin, scale=(1.69 if uiscale is ba.UIScale.SMALL else 1.05 if uiscale is ba.UIScale.MEDIUM else 0.9), stack_offset=(0, -26) if uiscale is ba.UIScale.SMALL else (0, 0))) self._back_button: Optional[ba.Widget] = ba.buttonwidget( parent=self._root_widget, position=(59 + x_inset, self._height - 70), size=(120, 60), scale=1.0, on_activate_call=self._on_back_press, autoselect=True, label=ba.Lstr(resource='backText'), button_type='back') ba.containerwidget(edit=self._root_widget, cancel_button=self._back_button) txt = self._title_text = ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), text=self._pvars.window_title_name, scale=1.3, res_scale=1.5, color=ba.app.ui.heading_color, h_align='center', v_align='center') if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: ba.textwidget(edit=txt, text='') ba.buttonwidget(edit=self._back_button, button_type='backSmall', size=(60, 54), position=(59 + x_inset, self._height - 67), label=ba.charstr(ba.SpecialChar.BACK)) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: self._back_button.delete() self._back_button = None ba.containerwidget(edit=self._root_widget, on_cancel_call=self._on_back_press) scroll_offs = 33 else: scroll_offs = 0 self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = (self._height - (146 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars else 136)) self._scrollwidget = ba.scrollwidget( parent=self._root_widget, highlight=False, size=(self._scroll_width, self._scroll_height), position=((self._width - self._scroll_width) * 0.5, 65 + scroll_offs)) ba.containerwidget(edit=self._scrollwidget, claims_left_right=True) self._subcontainer: Optional[ba.Widget] = None self._config_name_full = self._pvars.config_name + ' Playlists' self._last_config = None # Update now and once per second. # (this should do our initial refresh) self._update() self._update_timer = ba.Timer(1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True) def _ensure_standard_playlists_exist(self) -> None: # On new installations, go ahead and create a few playlists # besides the hard-coded default one: if not _ba.get_account_misc_val('madeStandardPlaylists', False): _ba.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': 'Free-for-All', 'playlistName': ba.Lstr( resource='singleGamePlaylistNameText').evaluate().replace( '${GAME}', ba.Lstr(translate=('gameNames', 'Death Match')).evaluate()), 'playlist': [ { 'type': 'bs_death_match.DeathMatchGame', 'settings': { 'Epic Mode': False, 'Kills to Win Per Player': 10, 'Respawn Times': 1.0, 'Time Limit': 300, 'map': 'Doom Shroom' } }, { 'type': 'bs_death_match.DeathMatchGame', 'settings': { 'Epic Mode': False, 'Kills to Win Per Player': 10, 'Respawn Times': 1.0, 'Time Limit': 300, 'map': 'Crag Castle' } }, ] }) _ba.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': 'Team Tournament', 'playlistName': ba.Lstr( resource='singleGamePlaylistNameText').evaluate().replace( '${GAME}', ba.Lstr(translate=('gameNames', 'Capture the Flag')).evaluate()), 'playlist': [ { 'type': 'bs_capture_the_flag.CTFGame', 'settings': { 'map': 'Bridgit', 'Score to Win': 3, 'Flag Idle Return Time': 30, 'Flag Touch Return Time': 0, 'Respawn Times': 1.0, 'Time Limit': 600, 'Epic Mode': False } }, { 'type': 'bs_capture_the_flag.CTFGame', 'settings': { 'map': 'Roundabout', 'Score to Win': 2, 'Flag Idle Return Time': 30, 'Flag Touch Return Time': 0, 'Respawn Times': 1.0, 'Time Limit': 600, 'Epic Mode': False } }, { 'type': 'bs_capture_the_flag.CTFGame', 'settings': { 'map': 'Tip Top', 'Score to Win': 2, 'Flag Idle Return Time': 30, 'Flag Touch Return Time': 3, 'Respawn Times': 1.0, 'Time Limit': 300, 'Epic Mode': False } }, ] }) _ba.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': 'Team Tournament', 'playlistName': ba.Lstr(translate=('playlistNames', 'Just Sports')).evaluate(), 'playlist': [ { 'type': 'bs_hockey.HockeyGame', 'settings': { 'Time Limit': 0, 'map': 'Hockey Stadium', 'Score to Win': 1, 'Respawn Times': 1.0 } }, { 'type': 'bs_football.FootballTeamGame', 'settings': { 'Time Limit': 0, 'map': 'Football Stadium', 'Score to Win': 21, 'Respawn Times': 1.0 } }, ] }) _ba.add_transaction({ 'type': 'ADD_PLAYLIST', 'playlistType': 'Free-for-All', 'playlistName': ba.Lstr(translate=('playlistNames', 'Just Epic')).evaluate(), 'playlist': [{ 'type': 'bs_elimination.EliminationGame', 'settings': { 'Time Limit': 120, 'map': 'Tip Top', 'Respawn Times': 1.0, 'Lives Per Player': 1, 'Epic Mode': 1 } }] }) _ba.add_transaction({ 'type': 'SET_MISC_VAL', 'name': 'madeStandardPlaylists', 'value': True }) _ba.run_transactions() def _refresh(self) -> None: # FIXME: Should tidy this up. # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-nested-blocks from efro.util import asserttype from ba.internal import get_map_class, filter_playlist if not self._root_widget: return if self._subcontainer is not None: self._save_state() self._subcontainer.delete() # Make sure config exists. if self._config_name_full not in ba.app.config: ba.app.config[self._config_name_full] = {} items = list(ba.app.config[self._config_name_full].items()) # Make sure everything is unicode. items = [(i[0].decode(), i[1]) if not isinstance(i[0], str) else i for i in items] items.sort(key=lambda x2: asserttype(x2[0], str).lower()) items = [['__default__', None]] + items # default is always first count = len(items) columns = 3 rows = int(math.ceil(float(count) / columns)) button_width = 230 button_height = 230 button_buffer_h = -3 button_buffer_v = 0 self._sub_width = self._scroll_width self._sub_height = 40 + rows * (button_height + 2 * button_buffer_v) + 90 assert self._sub_width is not None assert self._sub_height is not None self._subcontainer = ba.containerwidget(parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False) children = self._subcontainer.get_children() for child in children: child.delete() ba.textwidget(parent=self._subcontainer, text=ba.Lstr(resource='playlistsText'), position=(40, self._sub_height - 26), size=(0, 0), scale=1.0, maxwidth=400, color=ba.app.ui.title_color, h_align='left', v_align='center') index = 0 appconfig = ba.app.config model_opaque = ba.getmodel('level_select_button_opaque') model_transparent = ba.getmodel('level_select_button_transparent') mask_tex = ba.gettexture('mapPreviewMask') h_offs = 225 if count == 1 else 115 if count == 2 else 0 h_offs_bottom = 0 uiscale = ba.app.ui.uiscale for y in range(rows): for x in range(columns): name = items[index][0] assert name is not None pos = (x * (button_width + 2 * button_buffer_h) + button_buffer_h + 8 + h_offs, self._sub_height - 47 - (y + 1) * (button_height + 2 * button_buffer_v)) btn = ba.buttonwidget(parent=self._subcontainer, button_type='square', size=(button_width, button_height), autoselect=True, label='', position=pos) if (x == 0 and ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL): ba.widget( edit=btn, left_widget=_ba.get_special_widget('back_button')) if (x == columns - 1 and ba.app.ui.use_toolbars and uiscale is ba.UIScale.SMALL): ba.widget( edit=btn, right_widget=_ba.get_special_widget('party_button')) ba.buttonwidget( edit=btn, on_activate_call=ba.Call(self._on_playlist_press, btn, name), on_select_call=ba.Call(self._on_playlist_select, name)) ba.widget(edit=btn, show_buffer_top=50, show_buffer_bottom=50) if self._selected_playlist == name: ba.containerwidget(edit=self._subcontainer, selected_child=btn, visible_child=btn) if self._back_button is not None: if y == 0: ba.widget(edit=btn, up_widget=self._back_button) if x == 0: ba.widget(edit=btn, left_widget=self._back_button) print_name: Optional[Union[str, ba.Lstr]] if name == '__default__': print_name = self._pvars.default_list_name else: print_name = name ba.textwidget(parent=self._subcontainer, text=print_name, position=(pos[0] + button_width * 0.5, pos[1] + button_height * 0.79), size=(0, 0), scale=button_width * 0.003, maxwidth=button_width * 0.7, draw_controller=btn, h_align='center', v_align='center') # Poke into this playlist and see if we can display some of # its maps. map_images = [] try: map_textures = [] map_texture_entries = [] if name == '__default__': playlist = self._pvars.get_default_list_call() else: if name not in appconfig[self._pvars.config_name + ' Playlists']: print( 'NOT FOUND ERR', appconfig[self._pvars.config_name + ' Playlists']) playlist = appconfig[self._pvars.config_name + ' Playlists'][name] playlist = filter_playlist(playlist, self._sessiontype, remove_unowned=False, mark_unowned=True) for entry in playlist: mapname = entry['settings']['map'] maptype: Optional[type[ba.Map]] try: maptype = get_map_class(mapname) except ba.NotFoundError: maptype = None if maptype is not None: tex_name = maptype.get_preview_texture_name() if tex_name is not None: map_textures.append(tex_name) map_texture_entries.append(entry) if len(map_textures) >= 6: break if len(map_textures) > 4: img_rows = 3 img_columns = 2 scl = 0.33 h_offs_img = 30 v_offs_img = 126 elif len(map_textures) > 2: img_rows = 2 img_columns = 2 scl = 0.35 h_offs_img = 24 v_offs_img = 110 elif len(map_textures) > 1: img_rows = 2 img_columns = 1 scl = 0.5 h_offs_img = 47 v_offs_img = 105 else: img_rows = 1 img_columns = 1 scl = 0.75 h_offs_img = 20 v_offs_img = 65 v = None for row in range(img_rows): for col in range(img_columns): tex_index = row * img_columns + col if tex_index < len(map_textures): entry = map_texture_entries[tex_index] owned = not (('is_unowned_map' in entry and entry['is_unowned_map']) or ('is_unowned_game' in entry and entry['is_unowned_game'])) tex_name = map_textures[tex_index] h = pos[0] + h_offs_img + scl * 250 * col v = pos[1] + v_offs_img - scl * 130 * row map_images.append( ba.imagewidget( parent=self._subcontainer, size=(scl * 250.0, scl * 125.0), position=(h, v), texture=ba.gettexture(tex_name), opacity=1.0 if owned else 0.25, draw_controller=btn, model_opaque=model_opaque, model_transparent=model_transparent, mask_texture=mask_tex)) if not owned: ba.imagewidget( parent=self._subcontainer, size=(scl * 100.0, scl * 100.0), position=(h + scl * 75, v + scl * 10), texture=ba.gettexture('lock'), draw_controller=btn) if v is not None: v -= scl * 130.0 except Exception: ba.print_exception('Error listing playlist maps.') if not map_images: ba.textwidget(parent=self._subcontainer, text='???', scale=1.5, size=(0, 0), color=(1, 1, 1, 0.5), h_align='center', v_align='center', draw_controller=btn, position=(pos[0] + button_width * 0.5, pos[1] + button_height * 0.5)) index += 1 if index >= count: break if index >= count: break self._customize_button = btn = ba.buttonwidget( parent=self._subcontainer, size=(100, 30), position=(34 + h_offs_bottom, 50), text_scale=0.6, label=ba.Lstr(resource='customizeText'), on_activate_call=self._on_customize_press, color=(0.54, 0.52, 0.67), textcolor=(0.7, 0.65, 0.7), autoselect=True) ba.widget(edit=btn, show_buffer_top=22, show_buffer_bottom=28) self._restore_state() def on_play_options_window_run_game(self) -> None: """(internal)""" if not self._root_widget: return ba.containerwidget(edit=self._root_widget, transition='out_left') def _on_playlist_select(self, playlist_name: str) -> None: self._selected_playlist = playlist_name def _update(self) -> None: # make sure config exists if self._config_name_full not in ba.app.config: ba.app.config[self._config_name_full] = {} cfg = ba.app.config[self._config_name_full] if cfg != self._last_config: self._last_config = copy.deepcopy(cfg) self._refresh() def _on_playlist_press(self, button: ba.Widget, playlist_name: str) -> None: # pylint: disable=cyclic-import from bastd.ui.playoptions import PlayOptionsWindow # Make sure the target playlist still exists. exists = (playlist_name == '__default__' or playlist_name in ba.app.config.get( self._config_name_full, {})) if not exists: return self._save_state() PlayOptionsWindow(sessiontype=self._sessiontype, scale_origin=button.get_screen_space_center(), playlist=playlist_name, delegate=self) def _on_customize_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.playlist.customizebrowser import ( PlaylistCustomizeBrowserWindow) self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') ba.app.ui.set_main_menu_window( PlaylistCustomizeBrowserWindow( origin_widget=self._customize_button, sessiontype=self._sessiontype).get_root_widget()) def _on_back_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.play import PlayWindow # Store our selected playlist if that's changed. if self._selected_playlist is not None: prev_sel = ba.app.config.get(self._pvars.config_name + ' Playlist Selection') if self._selected_playlist != prev_sel: cfg = ba.app.config cfg[self._pvars.config_name + ' Playlist Selection'] = self._selected_playlist cfg.commit() self._save_state() ba.containerwidget(edit=self._root_widget, transition=self._transition_out) ba.app.ui.set_main_menu_window( PlayWindow(transition='in_left').get_root_widget()) def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() if sel == self._back_button: sel_name = 'Back' elif sel == self._scrollwidget: assert self._subcontainer is not None subsel = self._subcontainer.get_selected_child() if subsel == self._customize_button: sel_name = 'Customize' else: sel_name = 'Scroll' else: raise Exception('unrecognized selected widget') ba.app.ui.window_states[type(self)] = sel_name except Exception: ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: try: sel_name = ba.app.ui.window_states.get(type(self)) if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget elif sel_name == 'Customize': sel = self._scrollwidget ba.containerwidget(edit=self._subcontainer, selected_child=self._customize_button, visible_child=self._customize_button) else: sel = self._scrollwidget ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception(f'Error restoring state for {self}.')