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

Called when the popup is canceled.

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