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

A popup window for configuring play options.

PlayOptionsWindow( sessiontype: type[bascenev1._session.Session], playlist: str, scale_origin: tuple[float, float], delegate: Any = None)
 23    def __init__(
 24        self,
 25        sessiontype: type[bs.Session],
 26        playlist: str,
 27        scale_origin: tuple[float, float],
 28        delegate: Any = None,
 29    ):
 30        # FIXME: Tidy this up.
 31        # pylint: disable=too-many-branches
 32        # pylint: disable=too-many-statements
 33        # pylint: disable=too-many-locals
 34        from bascenev1 import filter_playlist, get_map_class
 35        from bauiv1lib.playlist import PlaylistTypeVars
 36
 37        self._r = 'gameListWindow'
 38        self._delegate = delegate
 39        self._pvars = PlaylistTypeVars(sessiontype)
 40        self._transitioning_out = False
 41
 42        # We behave differently if we're being used for playlist selection
 43        # vs starting a game directly (should make this more elegant).
 44        assert bui.app.classic is not None
 45        self._selecting_mode = bui.app.ui_v1.selecting_private_party_playlist
 46
 47        self._do_randomize_val = bui.app.config.get(
 48            self._pvars.config_name + ' Playlist Randomize', 0
 49        )
 50
 51        self._sessiontype = sessiontype
 52        self._playlist = playlist
 53
 54        self._width = 500.0
 55        self._height = 330.0 - 50.0
 56
 57        # In teams games, show the custom names/colors button.
 58        if self._sessiontype is bs.DualTeamSession:
 59            self._height += 50.0
 60
 61        self._row_height = 45.0
 62
 63        # Grab our maps to display.
 64        mesh_opaque = bui.getmesh('level_select_button_opaque')
 65        mesh_transparent = bui.getmesh('level_select_button_transparent')
 66        mask_tex = bui.gettexture('mapPreviewMask')
 67
 68        # Poke into this playlist and see if we can display some of its maps.
 69        map_textures = []
 70        map_texture_entries = []
 71        rows = 0
 72        columns = 0
 73        game_count = 0
 74        scl = 0.35
 75        c_width_total = 0.0
 76        try:
 77            max_columns = 5
 78            name = playlist
 79            if name == '__default__':
 80                plst = self._pvars.get_default_list_call()
 81            else:
 82                try:
 83                    plst = bui.app.config[
 84                        self._pvars.config_name + ' Playlists'
 85                    ][name]
 86                except Exception:
 87                    print(
 88                        'ERROR INFO: self._config_name is:',
 89                        self._pvars.config_name,
 90                    )
 91                    print(
 92                        'ERROR INFO: playlist names are:',
 93                        list(
 94                            bui.app.config[
 95                                self._pvars.config_name + ' Playlists'
 96                            ].keys()
 97                        ),
 98                    )
 99                    raise
100            plst = filter_playlist(
101                plst,
102                self._sessiontype,
103                remove_unowned=False,
104                mark_unowned=True,
105                name=name,
106            )
107            game_count = len(plst)
108            for entry in plst:
109                mapname = entry['settings']['map']
110                maptype: type[bs.Map] | None
111                try:
112                    maptype = get_map_class(mapname)
113                except bui.NotFoundError:
114                    maptype = None
115                if maptype is not None:
116                    tex_name = maptype.get_preview_texture_name()
117                    if tex_name is not None:
118                        map_textures.append(tex_name)
119                        map_texture_entries.append(entry)
120            rows = (max(0, len(map_textures) - 1) // max_columns) + 1
121            columns = min(max_columns, len(map_textures))
122
123            if len(map_textures) == 1:
124                scl = 1.1
125            elif len(map_textures) == 2:
126                scl = 0.7
127            elif len(map_textures) == 3:
128                scl = 0.55
129            else:
130                scl = 0.35
131            self._row_height = 128.0 * scl
132            c_width_total = scl * 250.0 * columns
133            if map_textures:
134                self._height += self._row_height * rows
135
136        except Exception:
137            logging.exception('Error listing playlist maps.')
138
139        show_shuffle_check_box = game_count > 1
140
141        if show_shuffle_check_box:
142            self._height += 40
143
144        # Creates our _root_widget.
145        uiscale = bui.app.ui_v1.uiscale
146        scale = (
147            1.69
148            if uiscale is bui.UIScale.SMALL
149            else 1.1
150            if uiscale is bui.UIScale.MEDIUM
151            else 0.85
152        )
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        # Team names/colors.
279        self._custom_colors_names_button: bui.Widget | None
280        if self._sessiontype is bs.DualTeamSession:
281            y_offs = 50 if show_shuffle_check_box else 0
282            self._custom_colors_names_button = bui.buttonwidget(
283                parent=self.root_widget,
284                position=(100, 200 + y_offs),
285                size=(290, 35),
286                on_activate_call=bui.WeakCall(self._custom_colors_names_press),
287                autoselect=True,
288                textcolor=(0.8, 0.8, 0.8),
289                label=bui.Lstr(resource='teamNamesColorText'),
290            )
291            assert bui.app.classic is not None
292            if not bui.app.classic.accounts.have_pro():
293                bui.imagewidget(
294                    parent=self.root_widget,
295                    size=(30, 30),
296                    position=(95, 202 + y_offs),
297                    texture=bui.gettexture('lock'),
298                    draw_controller=self._custom_colors_names_button,
299                )
300        else:
301            self._custom_colors_names_button = None
302
303        # Shuffle.
304        def _cb_callback(val: bool) -> None:
305            self._do_randomize_val = val
306            cfg = bui.app.config
307            cfg[
308                self._pvars.config_name + ' Playlist Randomize'
309            ] = self._do_randomize_val
310            cfg.commit()
311
312        if show_shuffle_check_box:
313            self._shuffle_check_box = bui.checkboxwidget(
314                parent=self.root_widget,
315                position=(110, 200),
316                scale=1.0,
317                size=(250, 30),
318                autoselect=True,
319                text=bui.Lstr(resource=self._r + '.shuffleGameOrderText'),
320                maxwidth=300,
321                textcolor=(0.8, 0.8, 0.8),
322                value=self._do_randomize_val,
323                on_value_change_call=_cb_callback,
324            )
325
326        # Show tutorial.
327        show_tutorial = bool(bui.app.config.get('Show Tutorial', True))
328
329        def _cb_callback_2(val: bool) -> None:
330            cfg = bui.app.config
331            cfg['Show Tutorial'] = val
332            cfg.commit()
333
334        self._show_tutorial_check_box = bui.checkboxwidget(
335            parent=self.root_widget,
336            position=(110, 151),
337            scale=1.0,
338            size=(250, 30),
339            autoselect=True,
340            text=bui.Lstr(resource=self._r + '.showTutorialText'),
341            maxwidth=300,
342            textcolor=(0.8, 0.8, 0.8),
343            value=show_tutorial,
344            on_value_change_call=_cb_callback_2,
345        )
346
347        # Grumble: current autoselect doesn't do a very good job
348        # with checkboxes.
349        if self._custom_colors_names_button is not None:
350            for btn in bottom_row_buttons:
351                bui.widget(
352                    edit=btn, down_widget=self._custom_colors_names_button
353                )
354            if show_shuffle_check_box:
355                bui.widget(
356                    edit=self._custom_colors_names_button,
357                    down_widget=self._shuffle_check_box,
358                )
359                bui.widget(
360                    edit=self._shuffle_check_box,
361                    up_widget=self._custom_colors_names_button,
362                )
363            else:
364                bui.widget(
365                    edit=self._custom_colors_names_button,
366                    down_widget=self._show_tutorial_check_box,
367                )
368                bui.widget(
369                    edit=self._show_tutorial_check_box,
370                    up_widget=self._custom_colors_names_button,
371                )
372
373        self._ok_button = bui.buttonwidget(
374            parent=self.root_widget,
375            position=(70, 44),
376            size=(200, 45),
377            scale=1.8,
378            text_res_scale=1.5,
379            on_activate_call=self._on_ok_press,
380            autoselect=True,
381            label=bui.Lstr(
382                resource='okText' if self._selecting_mode else 'playText'
383            ),
384        )
385
386        bui.widget(
387            edit=self._ok_button, up_widget=self._show_tutorial_check_box
388        )
389
390        bui.containerwidget(
391            edit=self.root_widget,
392            start_button=self._ok_button,
393            cancel_button=self._cancel_button,
394            selected_child=self._ok_button,
395        )
396
397        # Update now and once per second.
398        self._update_timer = bui.AppTimer(
399            1.0, bui.WeakCall(self._update), repeat=True
400        )
401        self._update()
def on_popup_cancel(self) -> None:
444    def on_popup_cancel(self) -> None:
445        bui.getsound('swish').play()
446        self._transition_out()

Called when the popup is canceled.

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