Пример #1
0
    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
Пример #2
0
    def __init__(self,
                 sessiontype: Type[ba.Session],
                 existing_playlist_name: str = None,
                 transition: str = 'in_right',
                 playlist: List[Dict[str, Any]] = None,
                 playlist_name: str = None):
        from ba.internal import preload_map_preview_media, filter_playlist
        from bastd.ui.playlist import PlaylistTypeVars
        from bastd.ui.playlist.edit import PlaylistEditWindow

        appconfig = ba.app.config

        # Since we may be showing our map list momentarily,
        # lets go ahead and preload all map preview textures.
        preload_map_preview_media()
        self._sessiontype = sessiontype

        self._editing_game = False
        self._editing_game_type: Optional[Type[ba.GameActivity]] = None
        self._pvars = PlaylistTypeVars(sessiontype)
        self._existing_playlist_name = existing_playlist_name
        self._config_name_full = self._pvars.config_name + ' Playlists'

        # Make sure config exists.
        if self._config_name_full not in appconfig:
            appconfig[self._config_name_full] = {}

        self._selected_index = 0
        if existing_playlist_name:
            self._name = existing_playlist_name

            # Filter out invalid games.
            self._playlist = filter_playlist(
                appconfig[self._pvars.config_name +
                          ' Playlists'][existing_playlist_name],
                sessiontype=sessiontype,
                remove_unowned=False)
            self._edit_ui_selection = None
        else:
            if playlist is not None:
                self._playlist = playlist
            else:
                self._playlist = []
            if playlist_name is not None:
                self._name = playlist_name
            else:

                # Find a good unused name.
                i = 1
                while True:
                    self._name = (
                        self._pvars.default_new_list_name.evaluate() +
                        ((' ' + str(i)) if i > 1 else ''))
                    if self._name not in appconfig[self._pvars.config_name +
                                                   ' Playlists']:
                        break
                    i += 1

            # Also we want it to start with 'add' highlighted since its empty
            # and that's all they can do.
            self._edit_ui_selection = 'add_button'

        ba.app.ui.set_main_menu_window(
            PlaylistEditWindow(editcontroller=self,
                               transition=transition).get_root_widget())
Пример #3
0
    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 (getclass, have_pro,
                                 get_default_teams_playlist,
                                 get_default_free_for_all_playlist,
                                 filter_playlist)
        from ba.internal import get_map_class
        from bastd.ui.playlist import PlaylistTypeVars

        self._r = 'gameListWindow'
        self._delegate = delegate
        self._pvars = PlaylistTypeVars(sessiontype)
        self._transitioning_out = False

        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__':
                if self._sessiontype is ba.FreeForAllSession:
                    plst = get_default_free_for_all_playlist()
                elif self._sessiontype is ba.DualTeamSession:
                    plst = get_default_teams_playlist()
                else:
                    raise Exception('unrecognized session-type: ' +
                                    str(self._sessiontype))
            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 Exception:
                    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.
        scale = (1.69 if ba.app.small_ui else 1.1 if ba.app.med_ui 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_config_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 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.
        try:
            show_tutorial = ba.app.config['Show Tutorial']
        except Exception:
            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._play_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_play_press,
            autoselect=True,
            label=ba.Lstr(resource='playText'))

        ba.widget(edit=self._play_button,
                  up_widget=self._show_tutorial_check_box)

        ba.containerwidget(edit=self.root_widget,
                           start_button=self._play_button,
                           cancel_button=self._cancel_button,
                           selected_child=self._play_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()
Пример #4
0
    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.main_window = 'Team Game Select'
            ba.set_analytics_screen('Teams Window')
        elif issubclass(sessiontype, ba.FreeForAllSession):
            ba.app.main_window = '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

        # 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()

        # Get the current selection (if any).
        self._selected_playlist = ba.app.config.get(self._pvars.config_name +
                                                    ' Playlist Selection')

        uiscale = ba.app.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.heading_color,
            h_align='center',
            v_align='center')
        if uiscale is ba.UIScale.SMALL and ba.app.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.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.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 _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
Пример #6
0
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)
Пример #7
0
    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)
Пример #8
0
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}.')