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

Called when the popup is canceled.

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