bauiv1lib.playoptions

Provides a window for configuring play options.

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

A popup window for configuring play options.

PlayOptionsWindow( *, sessiontype: type[bascenev1.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None, playlist_select_context: bauiv1lib.play.PlaylistSelectContext | None = None)
 25    def __init__(
 26        self,
 27        *,
 28        sessiontype: type[bs.Session],
 29        playlist: str,
 30        scale_origin: tuple[float, float],
 31        delegate: Any = None,
 32        playlist_select_context: PlaylistSelectContext | None = None,
 33    ):
 34        # FIXME: Tidy this up.
 35        # pylint: disable=too-many-branches
 36        # pylint: disable=too-many-statements
 37        # pylint: disable=too-many-locals
 38        from bascenev1 import filter_playlist, get_map_class
 39        from bauiv1lib.playlist import PlaylistTypeVars
 40        from bauiv1lib.config import ConfigNumberEdit
 41
 42        self._r = 'gameListWindow'
 43        self._delegate = delegate
 44        self._pvars = PlaylistTypeVars(sessiontype)
 45        self._transitioning_out = False
 46
 47        self._playlist_select_context = playlist_select_context
 48
 49        self._do_randomize_val = bui.app.config.get(
 50            self._pvars.config_name + ' Playlist Randomize', 0
 51        )
 52
 53        self._sessiontype = sessiontype
 54        self._playlist = playlist
 55
 56        self._width = 500.0
 57        self._height = 370.0 - 50.0
 58
 59        # In teams games, show the custom names/colors button.
 60        if self._sessiontype is bs.DualTeamSession:
 61            self._height += 50.0
 62
 63        self._row_height = 45.0
 64
 65        # Grab our maps to display.
 66        mesh_opaque = bui.getmesh('level_select_button_opaque')
 67        mesh_transparent = bui.getmesh('level_select_button_transparent')
 68        mask_tex = bui.gettexture('mapPreviewMask')
 69
 70        # Poke into this playlist and see if we can display some of its
 71        # maps.
 72        map_textures = []
 73        map_texture_entries = []
 74        rows = 0
 75        columns = 0
 76        game_count = 0
 77        scl = 0.35
 78        c_width_total = 0.0
 79        try:
 80            max_columns = 5
 81            name = playlist
 82            if name == '__default__':
 83                plst = self._pvars.get_default_list_call()
 84            else:
 85                try:
 86                    plst = bui.app.config[
 87                        self._pvars.config_name + ' Playlists'
 88                    ][name]
 89                except Exception:
 90                    print(
 91                        'ERROR INFO: self._config_name is:',
 92                        self._pvars.config_name,
 93                    )
 94                    print(
 95                        'ERROR INFO: playlist names are:',
 96                        list(
 97                            bui.app.config[
 98                                self._pvars.config_name + ' Playlists'
 99                            ].keys()
100                        ),
101                    )
102                    raise
103            plst = filter_playlist(
104                plst,
105                self._sessiontype,
106                remove_unowned=False,
107                mark_unowned=True,
108                name=name,
109            )
110            game_count = len(plst)
111            for entry in plst:
112                mapname = entry['settings']['map']
113                maptype: type[bs.Map] | None
114                try:
115                    maptype = get_map_class(mapname)
116                except bui.NotFoundError:
117                    maptype = None
118                if maptype is not None:
119                    tex_name = maptype.get_preview_texture_name()
120                    if tex_name is not None:
121                        map_textures.append(tex_name)
122                        map_texture_entries.append(entry)
123            rows = (max(0, len(map_textures) - 1) // max_columns) + 1
124            columns = min(max_columns, len(map_textures))
125
126            if len(map_textures) == 1:
127                scl = 1.1
128            elif len(map_textures) == 2:
129                scl = 0.7
130            elif len(map_textures) == 3:
131                scl = 0.55
132            else:
133                scl = 0.35
134            self._row_height = 128.0 * scl
135            c_width_total = scl * 250.0 * columns
136            if map_textures:
137                self._height += self._row_height * rows
138
139        except Exception:
140            logging.exception('Error listing playlist maps.')
141
142        show_shuffle_check_box = game_count > 1
143
144        if show_shuffle_check_box:
145            self._height += 40
146
147        uiscale = bui.app.ui_v1.uiscale
148        scale = (
149            1.69
150            if uiscale is bui.UIScale.SMALL
151            else 1.1 if uiscale is bui.UIScale.MEDIUM else 0.85
152        )
153        # Creates our _root_widget.
154        super().__init__(
155            position=scale_origin, size=(self._width, self._height), scale=scale
156        )
157
158        playlist_name: str | bui.Lstr = (
159            self._pvars.default_list_name
160            if playlist == '__default__'
161            else playlist
162        )
163        self._title_text = bui.textwidget(
164            parent=self.root_widget,
165            position=(self._width * 0.5, self._height - 89 + 51),
166            size=(0, 0),
167            text=playlist_name,
168            scale=1.4,
169            color=(1, 1, 1),
170            maxwidth=self._width * 0.7,
171            h_align='center',
172            v_align='center',
173        )
174
175        self._cancel_button = bui.buttonwidget(
176            parent=self.root_widget,
177            position=(25, self._height - 53),
178            size=(50, 50),
179            scale=0.7,
180            label='',
181            color=(0.42, 0.73, 0.2),
182            on_activate_call=self._on_cancel_press,
183            autoselect=True,
184            icon=bui.gettexture('crossOut'),
185            iconscale=1.2,
186        )
187
188        h_offs_img = self._width * 0.5 - c_width_total * 0.5
189        v_offs_img = self._height - 118 - scl * 125.0 + 50
190        bottom_row_buttons = []
191        self._have_at_least_one_owned = False
192
193        for row in range(rows):
194            for col in range(columns):
195                tex_index = row * columns + col
196                if tex_index < len(map_textures):
197                    tex_name = map_textures[tex_index]
198                    h = h_offs_img + scl * 250 * col
199                    v = v_offs_img - self._row_height * row
200                    entry = map_texture_entries[tex_index]
201                    owned = not (
202                        ('is_unowned_map' in entry and entry['is_unowned_map'])
203                        or (
204                            'is_unowned_game' in entry
205                            and entry['is_unowned_game']
206                        )
207                    )
208
209                    if owned:
210                        self._have_at_least_one_owned = True
211
212                    try:
213                        desc = bui.getclass(
214                            entry['type'], subclassof=bs.GameActivity
215                        ).get_settings_display_string(entry)
216                        if not owned:
217                            desc = bui.Lstr(
218                                value='${DESC}\n${UNLOCK}',
219                                subs=[
220                                    ('${DESC}', desc),
221                                    (
222                                        '${UNLOCK}',
223                                        bui.Lstr(
224                                            resource='unlockThisInTheStoreText'
225                                        ),
226                                    ),
227                                ],
228                            )
229                        desc_color = (0, 1, 0) if owned else (1, 0, 0)
230                    except Exception:
231                        desc = bui.Lstr(value='(invalid)')
232                        desc_color = (1, 0, 0)
233
234                    btn = bui.buttonwidget(
235                        parent=self.root_widget,
236                        size=(scl * 240.0, scl * 120.0),
237                        position=(h, v),
238                        texture=bui.gettexture(tex_name if owned else 'empty'),
239                        mesh_opaque=mesh_opaque if owned else None,
240                        on_activate_call=bui.Call(
241                            bui.screenmessage, desc, desc_color
242                        ),
243                        label='',
244                        color=(1, 1, 1),
245                        autoselect=True,
246                        extra_touch_border_scale=0.0,
247                        mesh_transparent=mesh_transparent if owned else None,
248                        mask_texture=mask_tex if owned else None,
249                    )
250                    if row == 0 and col == 0:
251                        bui.widget(edit=self._cancel_button, down_widget=btn)
252                    if row == rows - 1:
253                        bottom_row_buttons.append(btn)
254                    if not owned:
255                        # Ewww; buttons don't currently have alpha so in this
256                        # case we draw an image over our button with an empty
257                        # texture on it.
258                        bui.imagewidget(
259                            parent=self.root_widget,
260                            size=(scl * 260.0, scl * 130.0),
261                            position=(h - 10.0 * scl, v - 4.0 * scl),
262                            draw_controller=btn,
263                            color=(1, 1, 1),
264                            texture=bui.gettexture(tex_name),
265                            mesh_opaque=mesh_opaque,
266                            opacity=0.25,
267                            mesh_transparent=mesh_transparent,
268                            mask_texture=mask_tex,
269                        )
270
271                        bui.imagewidget(
272                            parent=self.root_widget,
273                            size=(scl * 100, scl * 100),
274                            draw_controller=btn,
275                            position=(h + scl * 70, v + scl * 10),
276                            texture=bui.gettexture('lock'),
277                        )
278
279        y_offs = 50 if show_shuffle_check_box else 0
280
281        # Series Length
282        y_offs2 = 40 if self._sessiontype is bs.DualTeamSession else 0
283        self._series_length_numedit = ConfigNumberEdit(
284            parent=self.root_widget,
285            position=(100, 200 + y_offs + y_offs2),
286            configkey=(
287                'FFA' if self._sessiontype is bs.FreeForAllSession else 'Teams'
288            )
289            + ' Series Length',
290            displayname=bui.Lstr(
291                resource=self._r
292                + (
293                    '.pointsToWinText'
294                    if self._sessiontype is bs.FreeForAllSession
295                    else '.seriesLengthText'
296                )
297            ),
298            minval=1.0,
299            maxval=100.0 if self._sessiontype is bs.FreeForAllSession else 99.0,
300            increment=1.0 if self._sessiontype is bs.FreeForAllSession else 2.0,
301            fallback_value=(
302                24 if self._sessiontype is bs.FreeForAllSession else 7
303            ),
304            f=0,
305        )
306
307        # Team names/colors.
308        self._custom_colors_names_button: bui.Widget | None
309        if self._sessiontype is bs.DualTeamSession:
310            self._custom_colors_names_button = bui.buttonwidget(
311                parent=self.root_widget,
312                position=(100, 195 + y_offs),
313                size=(290, 35),
314                on_activate_call=bui.WeakCall(self._custom_colors_names_press),
315                autoselect=True,
316                textcolor=(0.8, 0.8, 0.8),
317                label=bui.Lstr(resource='teamNamesColorText'),
318            )
319            assert bui.app.classic is not None
320            if not bui.app.classic.accounts.have_pro():
321                bui.imagewidget(
322                    parent=self.root_widget,
323                    size=(30, 30),
324                    position=(95, 202 + y_offs),
325                    texture=bui.gettexture('lock'),
326                    draw_controller=self._custom_colors_names_button,
327                )
328        else:
329            self._custom_colors_names_button = None
330
331        # Shuffle.
332        def _cb_callback(val: bool) -> None:
333            self._do_randomize_val = val
334            cfg = bui.app.config
335            cfg[self._pvars.config_name + ' Playlist Randomize'] = (
336                self._do_randomize_val
337            )
338            cfg.commit()
339
340        if show_shuffle_check_box:
341            self._shuffle_check_box = bui.checkboxwidget(
342                parent=self.root_widget,
343                position=(110, 200),
344                scale=1.0,
345                size=(250, 30),
346                autoselect=True,
347                text=bui.Lstr(resource=f'{self._r}.shuffleGameOrderText'),
348                maxwidth=300,
349                textcolor=(0.8, 0.8, 0.8),
350                value=self._do_randomize_val,
351                on_value_change_call=_cb_callback,
352            )
353
354        # Show tutorial.
355        show_tutorial = bool(bui.app.config.get('Show Tutorial', True))
356
357        def _cb_callback_2(val: bool) -> None:
358            cfg = bui.app.config
359            cfg['Show Tutorial'] = val
360            cfg.commit()
361
362        self._show_tutorial_check_box = bui.checkboxwidget(
363            parent=self.root_widget,
364            position=(110, 151),
365            scale=1.0,
366            size=(250, 30),
367            autoselect=True,
368            text=bui.Lstr(resource=f'{self._r}.showTutorialText'),
369            maxwidth=300,
370            textcolor=(0.8, 0.8, 0.8),
371            value=show_tutorial,
372            on_value_change_call=_cb_callback_2,
373        )
374
375        # Grumble: current autoselect doesn't do a very good job
376        # with checkboxes.
377        if self._custom_colors_names_button is not None:
378            for btn in bottom_row_buttons:
379                bui.widget(
380                    edit=btn, down_widget=self._custom_colors_names_button
381                )
382            if show_shuffle_check_box:
383                bui.widget(
384                    edit=self._custom_colors_names_button,
385                    down_widget=self._shuffle_check_box,
386                )
387                bui.widget(
388                    edit=self._shuffle_check_box,
389                    up_widget=self._custom_colors_names_button,
390                )
391            else:
392                bui.widget(
393                    edit=self._custom_colors_names_button,
394                    down_widget=self._show_tutorial_check_box,
395                )
396                bui.widget(
397                    edit=self._show_tutorial_check_box,
398                    up_widget=self._custom_colors_names_button,
399                )
400
401        self._ok_button = bui.buttonwidget(
402            parent=self.root_widget,
403            position=(70, 44),
404            size=(200, 45),
405            scale=1.8,
406            text_res_scale=1.5,
407            on_activate_call=self._on_ok_press,
408            autoselect=True,
409            label=bui.Lstr(
410                resource=(
411                    'okText'
412                    if self._playlist_select_context is not None
413                    else 'playText'
414                )
415            ),
416        )
417
418        bui.widget(
419            edit=self._ok_button, up_widget=self._show_tutorial_check_box
420        )
421
422        bui.containerwidget(
423            edit=self.root_widget,
424            start_button=self._ok_button,
425            cancel_button=self._cancel_button,
426            selected_child=self._ok_button,
427        )
428
429        # Update now and once per second.
430        self._update_timer = bui.AppTimer(
431            1.0, bui.WeakCall(self._update), repeat=True
432        )
433        self._update()
@override
def on_popup_cancel(self) -> None:
476    @override
477    def on_popup_cancel(self) -> None:
478        bui.getsound('swish').play()
479        self._transition_out()

Called when the popup is canceled.

Cancels can occur due to clicking outside the window, hitting escape, etc.