bauiv1lib.coop.browser

UI for browsing available co-op levels/games/etc.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""UI for browsing available co-op levels/games/etc."""
   4# FIXME: Break this up.
   5# pylint: disable=too-many-lines
   6
   7from __future__ import annotations
   8
   9import logging
  10from typing import TYPE_CHECKING, override
  11
  12import bauiv1 as bui
  13
  14if TYPE_CHECKING:
  15    from typing import Any
  16
  17    from bauiv1lib.coop.tournamentbutton import TournamentButton
  18
  19HARD_REQUIRES_PRO = False
  20
  21
  22class CoopBrowserWindow(bui.MainWindow):
  23    """Window for browsing co-op levels/games/etc."""
  24
  25    def __init__(
  26        self,
  27        transition: str | None = 'in_right',
  28        origin_widget: bui.Widget | None = None,
  29    ):
  30        # pylint: disable=too-many-statements
  31        # pylint: disable=cyclic-import
  32
  33        plus = bui.app.plus
  34        assert plus is not None
  35
  36        # Preload some modules we use in a background thread so we won't
  37        # have a visual hitch when the user taps them.
  38        bui.app.threadpool.submit_no_wait(self._preload_modules)
  39
  40        bui.set_analytics_screen('Coop Window')
  41
  42        app = bui.app
  43        classic = app.classic
  44        assert classic is not None
  45        cfg = app.config
  46
  47        # Quick note to players that tourneys won't work in ballistica
  48        # core builds. (need to split the word so it won't get subbed
  49        # out)
  50        if 'ballistica' + 'kit' == bui.appname() and bui.do_once():
  51            bui.apptimer(
  52                1.0,
  53                lambda: bui.screenmessage(
  54                    bui.Lstr(resource='noTournamentsInTestBuildText'),
  55                    color=(1, 1, 0),
  56                ),
  57            )
  58
  59        # Try to recreate the same number of buttons we had last time so our
  60        # re-selection code works.
  61        self._tournament_button_count = app.config.get('Tournament Rows', 0)
  62        assert isinstance(self._tournament_button_count, int)
  63
  64        self.star_tex = bui.gettexture('star')
  65        self.lsbt = bui.getmesh('level_select_button_transparent')
  66        self.lsbo = bui.getmesh('level_select_button_opaque')
  67        self.a_outline_tex = bui.gettexture('achievementOutline')
  68        self.a_outline_mesh = bui.getmesh('achievementOutline')
  69        self._campaign_sub_container: bui.Widget | None = None
  70        self._tournament_info_button: bui.Widget | None = None
  71        self._easy_button: bui.Widget | None = None
  72        self._hard_button: bui.Widget | None = None
  73        self._hard_button_lock_image: bui.Widget | None = None
  74        self._campaign_percent_text: bui.Widget | None = None
  75
  76        uiscale = app.ui_v1.uiscale
  77        self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120
  78        self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
  79        self._height = (
  80            600
  81            if uiscale is bui.UIScale.SMALL
  82            else 730 if uiscale is bui.UIScale.MEDIUM else 800
  83        )
  84        self._r = 'coopSelectWindow'
  85        top_extra = 0 if uiscale is bui.UIScale.SMALL else 0
  86
  87        self._tourney_data_up_to_date = False
  88
  89        self._campaign_difficulty = plus.get_v1_account_misc_val(
  90            'campaignDifficulty', 'easy'
  91        )
  92
  93        if (
  94            self._campaign_difficulty == 'hard'
  95            and HARD_REQUIRES_PRO
  96            and not classic.accounts.have_pro_options()
  97        ):
  98            plus.add_v1_account_transaction(
  99                {
 100                    'type': 'SET_MISC_VAL',
 101                    'name': 'campaignDifficulty',
 102                    'value': 'easy',
 103                }
 104            )
 105            self._campaign_difficulty = 'easy'
 106
 107        super().__init__(
 108            root_widget=bui.containerwidget(
 109                size=(self._width, self._height + top_extra),
 110                toolbar_visibility='menu_full',
 111                stack_offset=(
 112                    (0, -8)
 113                    if uiscale is bui.UIScale.SMALL
 114                    else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 115                ),
 116                scale=(
 117                    1.28
 118                    if uiscale is bui.UIScale.SMALL
 119                    else 0.8 if uiscale is bui.UIScale.MEDIUM else 0.75
 120                ),
 121            ),
 122            transition=transition,
 123            origin_widget=origin_widget,
 124        )
 125
 126        if uiscale is bui.UIScale.SMALL:
 127            self._back_button = bui.get_special_widget('back_button')
 128            bui.containerwidget(
 129                edit=self._root_widget, on_cancel_call=self.main_window_back
 130            )
 131        else:
 132            self._back_button = bui.buttonwidget(
 133                parent=self._root_widget,
 134                position=(75 + x_inset, self._height - 87),
 135                size=(120, 60),
 136                scale=1.2,
 137                autoselect=True,
 138                label=bui.Lstr(resource='backText'),
 139                button_type='back',
 140                on_activate_call=self.main_window_back,
 141            )
 142            bui.buttonwidget(
 143                edit=self._back_button,
 144                button_type='backSmall',
 145                size=(60, 50),
 146                position=(
 147                    75 + x_inset,
 148                    self._height - 87 + 6,
 149                ),
 150                label=bui.charstr(bui.SpecialChar.BACK),
 151            )
 152            bui.containerwidget(
 153                edit=self._root_widget, cancel_button=self._back_button
 154            )
 155
 156        # self._league_rank_button: LeagueRankButton | None
 157        # self._store_button: StoreButton | None
 158        # self._store_button_widget: bui.Widget | None
 159        # self._league_rank_button_widget: bui.Widget | None
 160
 161        # if not app.ui_v1.use_toolbars:
 162        #     prb = self._league_rank_button = LeagueRankButton(
 163        #         parent=self._root_widget,
 164        #         position=(
 165        #             self._width - (282 + x_inset),
 166        #             self._height
 167        #             - 85
 168        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
 169        #         ),
 170        #         size=(100, 60),
 171        #         color=(0.4, 0.4, 0.9),
 172        #         textcolor=(0.9, 0.9, 2.0),
 173        #         scale=1.05,
 174        #         on_activate_call=bui.WeakCall(
 175        # self._switch_to_league_rankings),
 176        #     )
 177        #     self._league_rank_button_widget = prb.get_button()
 178
 179        #     sbtn = self._store_button = StoreButton(
 180        #         parent=self._root_widget,
 181        #         position=(
 182        #             self._width - (170 + x_inset),
 183        #             self._height
 184        #             - 85
 185        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
 186        #         ),
 187        #         size=(100, 60),
 188        #         color=(0.6, 0.4, 0.7),
 189        #         show_tickets=True,
 190        #         button_type='square',
 191        #         sale_scale=0.85,
 192        #         textcolor=(0.9, 0.7, 1.0),
 193        #         scale=1.05,
 194        #         on_activate_call=bui.WeakCall(self._switch_to_score, None),
 195        #     )
 196        #     self._store_button_widget = sbtn.get_button()
 197        #     assert self._back_button is not None
 198        #     bui.widget(
 199        #         edit=self._back_button,
 200        #         right_widget=self._league_rank_button_widget,
 201        #     )
 202        #     bui.widget(
 203        #         edit=self._league_rank_button_widget,
 204        #         left_widget=self._back_button,
 205        #     )
 206        # else:
 207        # self._league_rank_button = None
 208        # self._store_button = None
 209        # self._store_button_widget = None
 210        # self._league_rank_button_widget = None
 211
 212        # Move our corner buttons dynamically to keep them out of the way of
 213        # the party icon :-(
 214        # self._update_corner_button_positions()
 215        # self._update_corner_button_positions_timer = bui.AppTimer(
 216        #     1.0, bui.WeakCall(
 217        # self._update_corner_button_positions), repeat=True
 218        # )
 219
 220        self._last_tournament_query_time: float | None = None
 221        self._last_tournament_query_response_time: float | None = None
 222        self._doing_tournament_query = False
 223
 224        self._selected_campaign_level = cfg.get(
 225            'Selected Coop Campaign Level', None
 226        )
 227        self._selected_custom_level = cfg.get(
 228            'Selected Coop Custom Level', None
 229        )
 230
 231        # Don't want initial construction affecting our last-selected.
 232        self._do_selection_callbacks = False
 233        v = self._height - 95
 234        txt = bui.textwidget(
 235            parent=self._root_widget,
 236            position=(
 237                self._width * 0.5,
 238                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
 239            ),
 240            size=(0, 0),
 241            text=bui.Lstr(
 242                resource='playModes.singlePlayerCoopText',
 243                fallback_resource='playModes.coopText',
 244            ),
 245            h_align='center',
 246            color=app.ui_v1.title_color,
 247            scale=1.5,
 248            maxwidth=500,
 249            v_align='center',
 250        )
 251
 252        if uiscale is bui.UIScale.SMALL:
 253            bui.textwidget(edit=txt, text='')
 254
 255        self._selected_row = cfg.get('Selected Coop Row', None)
 256
 257        self._scroll_width = self._width - (130 + 2 * x_inset)
 258        self._scroll_height = self._height - (
 259            219 if uiscale is bui.UIScale.SMALL else 160
 260        )
 261
 262        self._subcontainerwidth = 800.0
 263        self._subcontainerheight = 1400.0
 264
 265        self._scrollwidget = bui.scrollwidget(
 266            parent=self._root_widget,
 267            highlight=False,
 268            position=(
 269                (65 + x_inset, 120)
 270                if uiscale is bui.UIScale.SMALL
 271                else (65 + x_inset, 70)
 272            ),
 273            size=(self._scroll_width, self._scroll_height),
 274            simple_culling_v=10.0,
 275            claims_left_right=True,
 276            selection_loops_to_parent=True,
 277        )
 278        self._subcontainer: bui.Widget | None = None
 279
 280        # Take note of our account state; we'll refresh later if this changes.
 281        self._account_state_num = plus.get_v1_account_state_num()
 282
 283        # Same for fg/bg state.
 284        self._fg_state = app.fg_state
 285
 286        self._refresh()
 287        self._restore_state()
 288
 289        # Even though we might display cached tournament data immediately, we
 290        # don't consider it valid until we've pinged.
 291        # the server for an update
 292        self._tourney_data_up_to_date = False
 293
 294        # If we've got a cached tournament list for our account and info for
 295        # each one of those tournaments, go ahead and display it as a
 296        # starting point.
 297        if (
 298            classic.accounts.account_tournament_list is not None
 299            and classic.accounts.account_tournament_list[0]
 300            == plus.get_v1_account_state_num()
 301            and all(
 302                t_id in classic.accounts.tournament_info
 303                for t_id in classic.accounts.account_tournament_list[1]
 304            )
 305        ):
 306            tourney_data = [
 307                classic.accounts.tournament_info[t_id]
 308                for t_id in classic.accounts.account_tournament_list[1]
 309            ]
 310            self._update_for_data(tourney_data)
 311
 312        # This will pull new data periodically, update timers, etc.
 313        self._update_timer = bui.AppTimer(
 314            1.0, bui.WeakCall(self._update), repeat=True
 315        )
 316        self._update()
 317
 318    @override
 319    def get_main_window_state(self) -> bui.MainWindowState:
 320        # Support recreating our window for back/refresh purposes.
 321        cls = type(self)
 322        return bui.BasicMainWindowState(
 323            create_call=lambda transition, origin_widget: cls(
 324                transition=transition, origin_widget=origin_widget
 325            )
 326        )
 327
 328    @override
 329    def on_main_window_close(self) -> None:
 330        self._save_state()
 331
 332    @staticmethod
 333    def _preload_modules() -> None:
 334        """Preload modules we use; avoids hitches (called in bg thread)."""
 335        # pylint: disable=cyclic-import
 336        import bauiv1lib.purchase as _unused1
 337        import bauiv1lib.coop.gamebutton as _unused2
 338        import bauiv1lib.confirm as _unused3
 339        import bauiv1lib.account as _unused4
 340        import bauiv1lib.league.rankwindow as _unused5
 341        import bauiv1lib.store.browser as _unused6
 342        import bauiv1lib.account.viewer as _unused7
 343        import bauiv1lib.tournamentscores as _unused8
 344        import bauiv1lib.tournamententry as _unused9
 345        import bauiv1lib.play as _unused10
 346        import bauiv1lib.coop.tournamentbutton as _unused11
 347
 348    def _update(self) -> None:
 349        plus = bui.app.plus
 350        assert plus is not None
 351
 352        # Do nothing if we've somehow outlived our actual UI.
 353        if not self._root_widget:
 354            return
 355
 356        cur_time = bui.apptime()
 357
 358        # If its been a while since we got a tournament update, consider
 359        # the data invalid (prevents us from joining tournaments if our
 360        # internet connection goes down for a while).
 361        if (
 362            self._last_tournament_query_response_time is None
 363            or bui.apptime() - self._last_tournament_query_response_time
 364            > 60.0 * 2
 365        ):
 366            self._tourney_data_up_to_date = False
 367
 368        # If our account state has changed, do a full request.
 369        account_state_num = plus.get_v1_account_state_num()
 370        if account_state_num != self._account_state_num:
 371            self._account_state_num = account_state_num
 372            self._save_state()
 373            self._refresh()
 374
 375            # Also encourage a new tournament query since this will clear out
 376            # our current results.
 377            if not self._doing_tournament_query:
 378                self._last_tournament_query_time = None
 379
 380        # If we've been backgrounded/foregrounded, invalidate our
 381        # tournament entries (they will be refreshed below asap).
 382        if self._fg_state != bui.app.fg_state:
 383            self._tourney_data_up_to_date = False
 384
 385        # Send off a new tournament query if its been long enough or whatnot.
 386        if not self._doing_tournament_query and (
 387            self._last_tournament_query_time is None
 388            or cur_time - self._last_tournament_query_time > 30.0
 389            or self._fg_state != bui.app.fg_state
 390        ):
 391            self._fg_state = bui.app.fg_state
 392            self._last_tournament_query_time = cur_time
 393            self._doing_tournament_query = True
 394            plus.tournament_query(
 395                args={'source': 'coop window refresh', 'numScores': 1},
 396                callback=bui.WeakCall(self._on_tournament_query_response),
 397            )
 398
 399        # Decrement time on our tournament buttons.
 400        ads_enabled = plus.have_incentivized_ad()
 401        for tbtn in self._tournament_buttons:
 402            tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
 403            if tbtn.time_remaining_value_text is not None:
 404                bui.textwidget(
 405                    edit=tbtn.time_remaining_value_text,
 406                    text=(
 407                        bui.timestring(tbtn.time_remaining, centi=False)
 408                        if (
 409                            tbtn.has_time_remaining
 410                            and self._tourney_data_up_to_date
 411                        )
 412                        else '-'
 413                    ),
 414                )
 415
 416            # Also adjust the ad icon visibility.
 417            if tbtn.allow_ads and plus.has_video_ads():
 418                bui.imagewidget(
 419                    edit=tbtn.entry_fee_ad_image,
 420                    opacity=1.0 if ads_enabled else 0.25,
 421                )
 422                bui.textwidget(
 423                    edit=tbtn.entry_fee_text_remaining,
 424                    color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
 425                )
 426
 427        self._update_hard_mode_lock_image()
 428
 429    def _update_hard_mode_lock_image(self) -> None:
 430        assert bui.app.classic is not None
 431        try:
 432            bui.imagewidget(
 433                edit=self._hard_button_lock_image,
 434                opacity=(
 435                    0.0
 436                    if (
 437                        (not HARD_REQUIRES_PRO)
 438                        or bui.app.classic.accounts.have_pro_options()
 439                    )
 440                    else 1.0
 441                ),
 442            )
 443        except Exception:
 444            logging.exception('Error updating campaign lock.')
 445
 446    def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
 447        # If the number of tournaments or challenges in the data differs
 448        # from our current arrangement, refresh with the new number.
 449        if (data is None and self._tournament_button_count != 0) or (
 450            data is not None and (len(data) != self._tournament_button_count)
 451        ):
 452            self._tournament_button_count = len(data) if data is not None else 0
 453            bui.app.config['Tournament Rows'] = self._tournament_button_count
 454            self._refresh()
 455
 456        # Update all of our tourney buttons based on whats in data.
 457        for i, tbtn in enumerate(self._tournament_buttons):
 458            assert data is not None
 459            tbtn.update_for_data(data[i])
 460
 461    def _on_tournament_query_response(
 462        self, data: dict[str, Any] | None
 463    ) -> None:
 464        plus = bui.app.plus
 465        assert plus is not None
 466
 467        assert bui.app.classic is not None
 468        accounts = bui.app.classic.accounts
 469        if data is not None:
 470            tournament_data = data['t']  # This used to be the whole payload.
 471            self._last_tournament_query_response_time = bui.apptime()
 472        else:
 473            tournament_data = None
 474
 475        # Keep our cached tourney info up to date.
 476        if data is not None:
 477            self._tourney_data_up_to_date = True
 478            accounts.cache_tournament_info(tournament_data)
 479
 480            # Also cache the current tourney list/order for this account.
 481            accounts.account_tournament_list = (
 482                plus.get_v1_account_state_num(),
 483                [e['tournamentID'] for e in tournament_data],
 484            )
 485
 486        self._doing_tournament_query = False
 487        self._update_for_data(tournament_data)
 488
 489    def _set_campaign_difficulty(self, difficulty: str) -> None:
 490        # pylint: disable=cyclic-import
 491        from bauiv1lib.purchase import PurchaseWindow
 492
 493        plus = bui.app.plus
 494        assert plus is not None
 495
 496        assert bui.app.classic is not None
 497        if difficulty != self._campaign_difficulty:
 498            if (
 499                difficulty == 'hard'
 500                and HARD_REQUIRES_PRO
 501                and not bui.app.classic.accounts.have_pro_options()
 502            ):
 503                PurchaseWindow(items=['pro'])
 504                return
 505            bui.getsound('gunCocking').play()
 506            if difficulty not in ('easy', 'hard'):
 507                print('ERROR: invalid campaign difficulty:', difficulty)
 508                difficulty = 'easy'
 509            self._campaign_difficulty = difficulty
 510            plus.add_v1_account_transaction(
 511                {
 512                    'type': 'SET_MISC_VAL',
 513                    'name': 'campaignDifficulty',
 514                    'value': difficulty,
 515                }
 516            )
 517            self._refresh_campaign_row()
 518        else:
 519            bui.getsound('click01').play()
 520
 521    def _refresh_campaign_row(self) -> None:
 522        # pylint: disable=too-many-locals
 523        # pylint: disable=too-many-statements
 524        # pylint: disable=cyclic-import
 525        from bauiv1lib.coop.gamebutton import GameButton
 526
 527        parent_widget = self._campaign_sub_container
 528
 529        # Clear out anything in the parent widget already.
 530        assert parent_widget is not None
 531        for child in parent_widget.get_children():
 532            child.delete()
 533
 534        next_widget_down = self._tournament_info_button
 535
 536        h = 0
 537        v2 = -2
 538        sel_color = (0.75, 0.85, 0.5)
 539        sel_color_hard = (0.4, 0.7, 0.2)
 540        un_sel_color = (0.5, 0.5, 0.5)
 541        sel_textcolor = (2, 2, 0.8)
 542        un_sel_textcolor = (0.6, 0.6, 0.6)
 543        self._easy_button = bui.buttonwidget(
 544            parent=parent_widget,
 545            position=(h + 30, v2 + 105),
 546            size=(120, 70),
 547            label=bui.Lstr(resource='difficultyEasyText'),
 548            button_type='square',
 549            autoselect=True,
 550            enable_sound=False,
 551            on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'),
 552            on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'),
 553            color=(
 554                sel_color
 555                if self._campaign_difficulty == 'easy'
 556                else un_sel_color
 557            ),
 558            textcolor=(
 559                sel_textcolor
 560                if self._campaign_difficulty == 'easy'
 561                else un_sel_textcolor
 562            ),
 563        )
 564        bui.widget(edit=self._easy_button, show_buffer_left=100)
 565        if self._selected_campaign_level == 'easyButton':
 566            bui.containerwidget(
 567                edit=parent_widget,
 568                selected_child=self._easy_button,
 569                visible_child=self._easy_button,
 570            )
 571        lock_tex = bui.gettexture('lock')
 572
 573        self._hard_button = bui.buttonwidget(
 574            parent=parent_widget,
 575            position=(h + 30, v2 + 32),
 576            size=(120, 70),
 577            label=bui.Lstr(resource='difficultyHardText'),
 578            button_type='square',
 579            autoselect=True,
 580            enable_sound=False,
 581            on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'),
 582            on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'),
 583            color=(
 584                sel_color_hard
 585                if self._campaign_difficulty == 'hard'
 586                else un_sel_color
 587            ),
 588            textcolor=(
 589                sel_textcolor
 590                if self._campaign_difficulty == 'hard'
 591                else un_sel_textcolor
 592            ),
 593        )
 594        self._hard_button_lock_image = bui.imagewidget(
 595            parent=parent_widget,
 596            size=(30, 30),
 597            draw_controller=self._hard_button,
 598            position=(h + 30 - 10, v2 + 32 + 70 - 35),
 599            texture=lock_tex,
 600        )
 601        self._update_hard_mode_lock_image()
 602        bui.widget(edit=self._hard_button, show_buffer_left=100)
 603        if self._selected_campaign_level == 'hardButton':
 604            bui.containerwidget(
 605                edit=parent_widget,
 606                selected_child=self._hard_button,
 607                visible_child=self._hard_button,
 608            )
 609
 610        bui.widget(edit=self._hard_button, down_widget=next_widget_down)
 611        h_spacing = 200
 612        campaign_buttons = []
 613        if self._campaign_difficulty == 'easy':
 614            campaignname = 'Easy'
 615        else:
 616            campaignname = 'Default'
 617        items = [
 618            campaignname + ':Onslaught Training',
 619            campaignname + ':Rookie Onslaught',
 620            campaignname + ':Rookie Football',
 621            campaignname + ':Pro Onslaught',
 622            campaignname + ':Pro Football',
 623            campaignname + ':Pro Runaround',
 624            campaignname + ':Uber Onslaught',
 625            campaignname + ':Uber Football',
 626            campaignname + ':Uber Runaround',
 627        ]
 628        items += [campaignname + ':The Last Stand']
 629        if self._selected_campaign_level is None:
 630            self._selected_campaign_level = items[0]
 631        h = 150
 632        for i in items:
 633            is_last_sel = i == self._selected_campaign_level
 634            campaign_buttons.append(
 635                GameButton(
 636                    self, parent_widget, i, h, v2, is_last_sel, 'campaign'
 637                ).get_button()
 638            )
 639            h += h_spacing
 640
 641        bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button)
 642
 643        # bui.widget(edit=self._easy_button)
 644        # if self._back_button is not None:
 645        bui.widget(
 646            edit=self._easy_button,
 647            left_widget=self._back_button,
 648            up_widget=self._back_button,
 649        )
 650        bui.widget(edit=self._hard_button, left_widget=self._back_button)
 651        for btn in campaign_buttons:
 652            bui.widget(
 653                edit=btn,
 654                up_widget=self._back_button,
 655            )
 656        for btn in campaign_buttons:
 657            bui.widget(edit=btn, down_widget=next_widget_down)
 658
 659        # Update our existing percent-complete text.
 660        assert bui.app.classic is not None
 661        campaign = bui.app.classic.getcampaign(campaignname)
 662        levels = campaign.levels
 663        levels_complete = sum((1 if l.complete else 0) for l in levels)
 664
 665        # Last level cant be completed; hence the -1.
 666        progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 667        p_str = str(int(progress * 100.0)) + '%'
 668
 669        self._campaign_percent_text = bui.textwidget(
 670            edit=self._campaign_percent_text,
 671            text=bui.Lstr(
 672                value='${C} (${P})',
 673                subs=[
 674                    ('${C}', bui.Lstr(resource=f'{self._r}.campaignText')),
 675                    ('${P}', p_str),
 676                ],
 677            ),
 678        )
 679
 680    def _on_tournament_info_press(self) -> None:
 681        # pylint: disable=cyclic-import
 682        from bauiv1lib.confirm import ConfirmWindow
 683
 684        txt = bui.Lstr(resource=f'{self._r}.tournamentInfoText')
 685        ConfirmWindow(
 686            txt,
 687            cancel_button=False,
 688            width=550,
 689            height=260,
 690            origin_widget=self._tournament_info_button,
 691        )
 692
 693    def _refresh(self) -> None:
 694        # pylint: disable=too-many-statements
 695        # pylint: disable=too-many-branches
 696        # pylint: disable=too-many-locals
 697        # pylint: disable=cyclic-import
 698        from bauiv1lib.coop.gamebutton import GameButton
 699        from bauiv1lib.coop.tournamentbutton import TournamentButton
 700
 701        plus = bui.app.plus
 702        assert plus is not None
 703        assert bui.app.classic is not None
 704
 705        # (Re)create the sub-container if need be.
 706        if self._subcontainer is not None:
 707            self._subcontainer.delete()
 708
 709        tourney_row_height = 200
 710        self._subcontainerheight = (
 711            700 + self._tournament_button_count * tourney_row_height
 712        )
 713
 714        self._subcontainer = bui.containerwidget(
 715            parent=self._scrollwidget,
 716            size=(self._subcontainerwidth, self._subcontainerheight),
 717            background=False,
 718            claims_left_right=True,
 719            selection_loops_to_parent=True,
 720        )
 721
 722        bui.containerwidget(
 723            edit=self._root_widget, selected_child=self._scrollwidget
 724        )
 725
 726        w_parent = self._subcontainer
 727        h_base = 6
 728
 729        v = self._subcontainerheight - 90
 730
 731        self._campaign_percent_text = bui.textwidget(
 732            parent=w_parent,
 733            position=(h_base + 27, v + 30),
 734            size=(0, 0),
 735            text='',
 736            h_align='left',
 737            v_align='center',
 738            color=bui.app.ui_v1.title_color,
 739            scale=1.1,
 740        )
 741
 742        row_v_show_buffer = 80
 743        v -= 198
 744
 745        h_scroll = bui.hscrollwidget(
 746            parent=w_parent,
 747            size=(self._scroll_width - 10, 205),
 748            position=(-5, v),
 749            simple_culling_h=70,
 750            highlight=False,
 751            border_opacity=0.0,
 752            color=(0.45, 0.4, 0.5),
 753            on_select_call=lambda: self._on_row_selected('campaign'),
 754        )
 755        self._campaign_h_scroll = h_scroll
 756        bui.widget(
 757            edit=h_scroll,
 758            show_buffer_top=row_v_show_buffer,
 759            show_buffer_bottom=row_v_show_buffer,
 760            autoselect=True,
 761        )
 762        if self._selected_row == 'campaign':
 763            bui.containerwidget(
 764                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 765            )
 766        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 767        self._campaign_sub_container = bui.containerwidget(
 768            parent=h_scroll, size=(180 + 200 * 10, 200), background=False
 769        )
 770
 771        # Tournaments
 772
 773        self._tournament_buttons: list[TournamentButton] = []
 774
 775        v -= 53
 776        # FIXME shouldn't use hard-coded strings here.
 777        txt = bui.Lstr(
 778            resource='tournamentsText', fallback_resource='tournamentText'
 779        ).evaluate()
 780        t_width = bui.get_string_width(txt, suppress_warning=True)
 781        bui.textwidget(
 782            parent=w_parent,
 783            position=(h_base + 27, v + 30),
 784            size=(0, 0),
 785            text=txt,
 786            h_align='left',
 787            v_align='center',
 788            color=bui.app.ui_v1.title_color,
 789            scale=1.1,
 790        )
 791        self._tournament_info_button = bui.buttonwidget(
 792            parent=w_parent,
 793            label='?',
 794            size=(20, 20),
 795            text_scale=0.6,
 796            position=(h_base + 27 + t_width * 1.1 + 15, v + 18),
 797            button_type='square',
 798            color=(0.6, 0.5, 0.65),
 799            textcolor=(0.7, 0.6, 0.75),
 800            autoselect=True,
 801            up_widget=self._campaign_h_scroll,
 802            left_widget=self._back_button,
 803            on_activate_call=self._on_tournament_info_press,
 804        )
 805        bui.widget(
 806            edit=self._tournament_info_button,
 807            right_widget=self._tournament_info_button,
 808        )
 809
 810        # Say 'unavailable' if there are zero tournaments, and if we're
 811        # not signed in add that as well (that's probably why we see no
 812        # tournaments).
 813        if self._tournament_button_count == 0:
 814            unavailable_text = bui.Lstr(resource='unavailableText')
 815            if plus.get_v1_account_state() != 'signed_in':
 816                unavailable_text = bui.Lstr(
 817                    value='${A} (${B})',
 818                    subs=[
 819                        ('${A}', unavailable_text),
 820                        ('${B}', bui.Lstr(resource='notSignedInText')),
 821                    ],
 822                )
 823            bui.textwidget(
 824                parent=w_parent,
 825                position=(h_base + 47, v),
 826                size=(0, 0),
 827                text=unavailable_text,
 828                h_align='left',
 829                v_align='center',
 830                color=bui.app.ui_v1.title_color,
 831                scale=0.9,
 832            )
 833            v -= 40
 834        v -= 198
 835
 836        tournament_h_scroll = None
 837        if self._tournament_button_count > 0:
 838            for i in range(self._tournament_button_count):
 839                tournament_h_scroll = h_scroll = bui.hscrollwidget(
 840                    parent=w_parent,
 841                    size=(self._scroll_width - 10, 205),
 842                    position=(-5, v),
 843                    highlight=False,
 844                    border_opacity=0.0,
 845                    color=(0.45, 0.4, 0.5),
 846                    on_select_call=bui.Call(
 847                        self._on_row_selected, 'tournament' + str(i + 1)
 848                    ),
 849                )
 850                bui.widget(
 851                    edit=h_scroll,
 852                    show_buffer_top=row_v_show_buffer,
 853                    show_buffer_bottom=row_v_show_buffer,
 854                    autoselect=True,
 855                )
 856                if self._selected_row == 'tournament' + str(i + 1):
 857                    bui.containerwidget(
 858                        edit=w_parent,
 859                        selected_child=h_scroll,
 860                        visible_child=h_scroll,
 861                    )
 862                bui.containerwidget(edit=h_scroll, claims_left_right=True)
 863                sc2 = bui.containerwidget(
 864                    parent=h_scroll,
 865                    size=(self._scroll_width - 24, 200),
 866                    background=False,
 867                )
 868                h = 0
 869                v2 = -2
 870                is_last_sel = True
 871                self._tournament_buttons.append(
 872                    TournamentButton(
 873                        sc2,
 874                        h,
 875                        v2,
 876                        is_last_sel,
 877                        on_pressed=bui.WeakCall(self.run_tournament),
 878                    )
 879                )
 880                v -= 200
 881
 882        # Custom Games. (called 'Practice' in UI these days).
 883        v -= 50
 884        bui.textwidget(
 885            parent=w_parent,
 886            position=(h_base + 27, v + 30 + 198),
 887            size=(0, 0),
 888            text=bui.Lstr(
 889                resource='practiceText',
 890                fallback_resource='coopSelectWindow.customText',
 891            ),
 892            h_align='left',
 893            v_align='center',
 894            color=bui.app.ui_v1.title_color,
 895            scale=1.1,
 896        )
 897
 898        items = [
 899            'Challenges:Infinite Onslaught',
 900            'Challenges:Infinite Runaround',
 901            'Challenges:Ninja Fight',
 902            'Challenges:Pro Ninja Fight',
 903            'Challenges:Meteor Shower',
 904            'Challenges:Target Practice B',
 905            'Challenges:Target Practice',
 906        ]
 907
 908        # Show easter-egg-hunt either if its easter or we own it.
 909        if plus.get_v1_account_misc_read_val(
 910            'easter', False
 911        ) or plus.get_v1_account_product_purchased('games.easter_egg_hunt'):
 912            items = [
 913                'Challenges:Easter Egg Hunt',
 914                'Challenges:Pro Easter Egg Hunt',
 915            ] + items
 916
 917        # If we've defined custom games, put them at the beginning.
 918        if bui.app.classic.custom_coop_practice_games:
 919            items = bui.app.classic.custom_coop_practice_games + items
 920
 921        self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget(
 922            parent=w_parent,
 923            size=(self._scroll_width - 10, 205),
 924            position=(-5, v),
 925            highlight=False,
 926            border_opacity=0.0,
 927            color=(0.45, 0.4, 0.5),
 928            on_select_call=bui.Call(self._on_row_selected, 'custom'),
 929        )
 930        bui.widget(
 931            edit=h_scroll,
 932            show_buffer_top=row_v_show_buffer,
 933            show_buffer_bottom=1.5 * row_v_show_buffer,
 934            autoselect=True,
 935        )
 936        if self._selected_row == 'custom':
 937            bui.containerwidget(
 938                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 939            )
 940        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 941        sc2 = bui.containerwidget(
 942            parent=h_scroll,
 943            size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200),
 944            background=False,
 945        )
 946        h_spacing = 200
 947        self._custom_buttons: list[GameButton] = []
 948        h = 0
 949        v2 = -2
 950        for item in items:
 951            is_last_sel = item == self._selected_custom_level
 952            self._custom_buttons.append(
 953                GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')
 954            )
 955            h += h_spacing
 956
 957        # We can't fill in our campaign row until tourney buttons are in place.
 958        # (for wiring up)
 959        self._refresh_campaign_row()
 960
 961        for i, tbutton in enumerate(self._tournament_buttons):
 962            bui.widget(
 963                edit=tbutton.button,
 964                up_widget=(
 965                    self._tournament_info_button
 966                    if i == 0
 967                    else self._tournament_buttons[i - 1].button
 968                ),
 969                down_widget=(
 970                    self._tournament_buttons[(i + 1)].button
 971                    if i + 1 < len(self._tournament_buttons)
 972                    else custom_h_scroll
 973                ),
 974                left_widget=self._back_button,
 975            )
 976            bui.widget(
 977                edit=tbutton.more_scores_button,
 978                down_widget=(
 979                    self._tournament_buttons[(i + 1)].current_leader_name_text
 980                    if i + 1 < len(self._tournament_buttons)
 981                    else custom_h_scroll
 982                ),
 983            )
 984            bui.widget(
 985                edit=tbutton.current_leader_name_text,
 986                up_widget=(
 987                    self._tournament_info_button
 988                    if i == 0
 989                    else self._tournament_buttons[i - 1].more_scores_button
 990                ),
 991            )
 992
 993        for i, btn in enumerate(self._custom_buttons):
 994            try:
 995                bui.widget(
 996                    edit=btn.get_button(),
 997                    up_widget=(
 998                        tournament_h_scroll
 999                        if self._tournament_buttons
1000                        else self._tournament_info_button
1001                    ),
1002                )
1003                if i == 0:
1004                    bui.widget(
1005                        edit=btn.get_button(), left_widget=self._back_button
1006                    )
1007
1008            except Exception:
1009                logging.exception('Error wiring up custom buttons.')
1010
1011        # There's probably several 'onSelected' callbacks pushed onto the
1012        # event queue.. we need to push ours too so we're enabled *after* them.
1013        bui.pushcall(self._enable_selectable_callback)
1014
1015    def _on_row_selected(self, row: str) -> None:
1016        if self._do_selection_callbacks:
1017            if self._selected_row != row:
1018                self._selected_row = row
1019
1020    def _enable_selectable_callback(self) -> None:
1021        self._do_selection_callbacks = True
1022
1023    def is_tourney_data_up_to_date(self) -> bool:
1024        """Return whether our tourney data is up to date."""
1025        return self._tourney_data_up_to_date
1026
1027    def run_game(self, game: str) -> None:
1028        """Run the provided game."""
1029        # pylint: disable=too-many-branches
1030        # pylint: disable=cyclic-import
1031        from bauiv1lib.confirm import ConfirmWindow
1032        from bauiv1lib.purchase import PurchaseWindow
1033        from bauiv1lib.account.signin import show_sign_in_prompt
1034
1035        plus = bui.app.plus
1036        assert plus is not None
1037
1038        assert bui.app.classic is not None
1039
1040        args: dict[str, Any] = {}
1041
1042        if game == 'Easy:The Last Stand':
1043            ConfirmWindow(
1044                bui.Lstr(
1045                    resource='difficultyHardUnlockOnlyText',
1046                    fallback_resource='difficultyHardOnlyText',
1047                ),
1048                cancel_button=False,
1049                width=460,
1050                height=130,
1051            )
1052            return
1053
1054        # Infinite onslaught/runaround require pro; bring up a store link
1055        # if need be.
1056        if (
1057            game
1058            in (
1059                'Challenges:Infinite Runaround',
1060                'Challenges:Infinite Onslaught',
1061            )
1062            and not bui.app.classic.accounts.have_pro()
1063        ):
1064            if plus.get_v1_account_state() != 'signed_in':
1065                show_sign_in_prompt()
1066            else:
1067                PurchaseWindow(items=['pro'])
1068            return
1069
1070        required_purchase: str | None
1071        if game in ['Challenges:Meteor Shower']:
1072            required_purchase = 'games.meteor_shower'
1073        elif game in [
1074            'Challenges:Target Practice',
1075            'Challenges:Target Practice B',
1076        ]:
1077            required_purchase = 'games.target_practice'
1078        elif game in ['Challenges:Ninja Fight']:
1079            required_purchase = 'games.ninja_fight'
1080        elif game in ['Challenges:Pro Ninja Fight']:
1081            required_purchase = 'games.ninja_fight'
1082        elif game in [
1083            'Challenges:Easter Egg Hunt',
1084            'Challenges:Pro Easter Egg Hunt',
1085        ]:
1086            required_purchase = 'games.easter_egg_hunt'
1087        else:
1088            required_purchase = None
1089
1090        if (
1091            required_purchase is not None
1092            and not plus.get_v1_account_product_purchased(required_purchase)
1093        ):
1094            if plus.get_v1_account_state() != 'signed_in':
1095                show_sign_in_prompt()
1096            else:
1097                PurchaseWindow(items=[required_purchase])
1098            return
1099
1100        self._save_state()
1101
1102        if bui.app.classic.launch_coop_game(game, args=args):
1103            bui.containerwidget(edit=self._root_widget, transition='out_left')
1104
1105    def run_tournament(self, tournament_button: TournamentButton) -> None:
1106        """Run the provided tournament game."""
1107        from bauiv1lib.account.signin import show_sign_in_prompt
1108        from bauiv1lib.tournamententry import TournamentEntryWindow
1109
1110        plus = bui.app.plus
1111        assert plus is not None
1112
1113        if plus.get_v1_account_state() != 'signed_in':
1114            show_sign_in_prompt()
1115            return
1116
1117        if bui.workspaces_in_use():
1118            bui.screenmessage(
1119                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1120                color=(1, 0, 0),
1121            )
1122            bui.getsound('error').play()
1123            return
1124
1125        if not self._tourney_data_up_to_date:
1126            bui.screenmessage(
1127                bui.Lstr(resource='tournamentCheckingStateText'),
1128                color=(1, 1, 0),
1129            )
1130            bui.getsound('error').play()
1131            return
1132
1133        if tournament_button.tournament_id is None:
1134            bui.screenmessage(
1135                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1136                color=(1, 0, 0),
1137            )
1138            bui.getsound('error').play()
1139            return
1140
1141        if tournament_button.required_league is not None:
1142            bui.screenmessage(
1143                bui.Lstr(
1144                    resource='league.tournamentLeagueText',
1145                    subs=[
1146                        (
1147                            '${NAME}',
1148                            bui.Lstr(
1149                                translate=(
1150                                    'leagueNames',
1151                                    tournament_button.required_league,
1152                                )
1153                            ),
1154                        )
1155                    ],
1156                ),
1157                color=(1, 0, 0),
1158            )
1159            bui.getsound('error').play()
1160            return
1161
1162        if tournament_button.time_remaining <= 0:
1163            bui.screenmessage(
1164                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1165            )
1166            bui.getsound('error').play()
1167            return
1168
1169        self._save_state()
1170
1171        assert tournament_button.tournament_id is not None
1172        TournamentEntryWindow(
1173            tournament_id=tournament_button.tournament_id,
1174            position=tournament_button.button.get_screen_space_center(),
1175        )
1176
1177    def _save_state(self) -> None:
1178        cfg = bui.app.config
1179        try:
1180            sel = self._root_widget.get_selected_child()
1181            if sel == self._back_button:
1182                sel_name = 'Back'
1183            # elif sel == self._store_button_widget:
1184            #     sel_name = 'Store'
1185            # elif sel == self._league_rank_button_widget:
1186            #     sel_name = 'PowerRanking'
1187            elif sel == self._scrollwidget:
1188                sel_name = 'Scroll'
1189            else:
1190                raise ValueError('unrecognized selection')
1191            assert bui.app.classic is not None
1192            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
1193        except Exception:
1194            logging.exception('Error saving state for %s.', self)
1195
1196        cfg['Selected Coop Row'] = self._selected_row
1197        cfg['Selected Coop Custom Level'] = self._selected_custom_level
1198        cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
1199        cfg.commit()
1200
1201    def _restore_state(self) -> None:
1202        try:
1203            assert bui.app.classic is not None
1204            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
1205                'sel_name'
1206            )
1207            if sel_name == 'Back':
1208                sel = self._back_button
1209            elif sel_name == 'Scroll':
1210                sel = self._scrollwidget
1211            # elif sel_name == 'PowerRanking':
1212            #     sel = self._league_rank_button_widget
1213            # elif sel_name == 'Store':
1214            #     sel = self._store_button_widget
1215            else:
1216                sel = self._scrollwidget
1217            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1218        except Exception:
1219            logging.exception('Error restoring state for %s.', self)
1220
1221    def sel_change(self, row: str, game: str) -> None:
1222        """(internal)"""
1223        if self._do_selection_callbacks:
1224            if row == 'custom':
1225                self._selected_custom_level = game
1226            elif row == 'campaign':
1227                self._selected_campaign_level = game
HARD_REQUIRES_PRO = False
class CoopBrowserWindow(bauiv1._uitypes.MainWindow):
  23class CoopBrowserWindow(bui.MainWindow):
  24    """Window for browsing co-op levels/games/etc."""
  25
  26    def __init__(
  27        self,
  28        transition: str | None = 'in_right',
  29        origin_widget: bui.Widget | None = None,
  30    ):
  31        # pylint: disable=too-many-statements
  32        # pylint: disable=cyclic-import
  33
  34        plus = bui.app.plus
  35        assert plus is not None
  36
  37        # Preload some modules we use in a background thread so we won't
  38        # have a visual hitch when the user taps them.
  39        bui.app.threadpool.submit_no_wait(self._preload_modules)
  40
  41        bui.set_analytics_screen('Coop Window')
  42
  43        app = bui.app
  44        classic = app.classic
  45        assert classic is not None
  46        cfg = app.config
  47
  48        # Quick note to players that tourneys won't work in ballistica
  49        # core builds. (need to split the word so it won't get subbed
  50        # out)
  51        if 'ballistica' + 'kit' == bui.appname() and bui.do_once():
  52            bui.apptimer(
  53                1.0,
  54                lambda: bui.screenmessage(
  55                    bui.Lstr(resource='noTournamentsInTestBuildText'),
  56                    color=(1, 1, 0),
  57                ),
  58            )
  59
  60        # Try to recreate the same number of buttons we had last time so our
  61        # re-selection code works.
  62        self._tournament_button_count = app.config.get('Tournament Rows', 0)
  63        assert isinstance(self._tournament_button_count, int)
  64
  65        self.star_tex = bui.gettexture('star')
  66        self.lsbt = bui.getmesh('level_select_button_transparent')
  67        self.lsbo = bui.getmesh('level_select_button_opaque')
  68        self.a_outline_tex = bui.gettexture('achievementOutline')
  69        self.a_outline_mesh = bui.getmesh('achievementOutline')
  70        self._campaign_sub_container: bui.Widget | None = None
  71        self._tournament_info_button: bui.Widget | None = None
  72        self._easy_button: bui.Widget | None = None
  73        self._hard_button: bui.Widget | None = None
  74        self._hard_button_lock_image: bui.Widget | None = None
  75        self._campaign_percent_text: bui.Widget | None = None
  76
  77        uiscale = app.ui_v1.uiscale
  78        self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120
  79        self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
  80        self._height = (
  81            600
  82            if uiscale is bui.UIScale.SMALL
  83            else 730 if uiscale is bui.UIScale.MEDIUM else 800
  84        )
  85        self._r = 'coopSelectWindow'
  86        top_extra = 0 if uiscale is bui.UIScale.SMALL else 0
  87
  88        self._tourney_data_up_to_date = False
  89
  90        self._campaign_difficulty = plus.get_v1_account_misc_val(
  91            'campaignDifficulty', 'easy'
  92        )
  93
  94        if (
  95            self._campaign_difficulty == 'hard'
  96            and HARD_REQUIRES_PRO
  97            and not classic.accounts.have_pro_options()
  98        ):
  99            plus.add_v1_account_transaction(
 100                {
 101                    'type': 'SET_MISC_VAL',
 102                    'name': 'campaignDifficulty',
 103                    'value': 'easy',
 104                }
 105            )
 106            self._campaign_difficulty = 'easy'
 107
 108        super().__init__(
 109            root_widget=bui.containerwidget(
 110                size=(self._width, self._height + top_extra),
 111                toolbar_visibility='menu_full',
 112                stack_offset=(
 113                    (0, -8)
 114                    if uiscale is bui.UIScale.SMALL
 115                    else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0)
 116                ),
 117                scale=(
 118                    1.28
 119                    if uiscale is bui.UIScale.SMALL
 120                    else 0.8 if uiscale is bui.UIScale.MEDIUM else 0.75
 121                ),
 122            ),
 123            transition=transition,
 124            origin_widget=origin_widget,
 125        )
 126
 127        if uiscale is bui.UIScale.SMALL:
 128            self._back_button = bui.get_special_widget('back_button')
 129            bui.containerwidget(
 130                edit=self._root_widget, on_cancel_call=self.main_window_back
 131            )
 132        else:
 133            self._back_button = bui.buttonwidget(
 134                parent=self._root_widget,
 135                position=(75 + x_inset, self._height - 87),
 136                size=(120, 60),
 137                scale=1.2,
 138                autoselect=True,
 139                label=bui.Lstr(resource='backText'),
 140                button_type='back',
 141                on_activate_call=self.main_window_back,
 142            )
 143            bui.buttonwidget(
 144                edit=self._back_button,
 145                button_type='backSmall',
 146                size=(60, 50),
 147                position=(
 148                    75 + x_inset,
 149                    self._height - 87 + 6,
 150                ),
 151                label=bui.charstr(bui.SpecialChar.BACK),
 152            )
 153            bui.containerwidget(
 154                edit=self._root_widget, cancel_button=self._back_button
 155            )
 156
 157        # self._league_rank_button: LeagueRankButton | None
 158        # self._store_button: StoreButton | None
 159        # self._store_button_widget: bui.Widget | None
 160        # self._league_rank_button_widget: bui.Widget | None
 161
 162        # if not app.ui_v1.use_toolbars:
 163        #     prb = self._league_rank_button = LeagueRankButton(
 164        #         parent=self._root_widget,
 165        #         position=(
 166        #             self._width - (282 + x_inset),
 167        #             self._height
 168        #             - 85
 169        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
 170        #         ),
 171        #         size=(100, 60),
 172        #         color=(0.4, 0.4, 0.9),
 173        #         textcolor=(0.9, 0.9, 2.0),
 174        #         scale=1.05,
 175        #         on_activate_call=bui.WeakCall(
 176        # self._switch_to_league_rankings),
 177        #     )
 178        #     self._league_rank_button_widget = prb.get_button()
 179
 180        #     sbtn = self._store_button = StoreButton(
 181        #         parent=self._root_widget,
 182        #         position=(
 183        #             self._width - (170 + x_inset),
 184        #             self._height
 185        #             - 85
 186        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
 187        #         ),
 188        #         size=(100, 60),
 189        #         color=(0.6, 0.4, 0.7),
 190        #         show_tickets=True,
 191        #         button_type='square',
 192        #         sale_scale=0.85,
 193        #         textcolor=(0.9, 0.7, 1.0),
 194        #         scale=1.05,
 195        #         on_activate_call=bui.WeakCall(self._switch_to_score, None),
 196        #     )
 197        #     self._store_button_widget = sbtn.get_button()
 198        #     assert self._back_button is not None
 199        #     bui.widget(
 200        #         edit=self._back_button,
 201        #         right_widget=self._league_rank_button_widget,
 202        #     )
 203        #     bui.widget(
 204        #         edit=self._league_rank_button_widget,
 205        #         left_widget=self._back_button,
 206        #     )
 207        # else:
 208        # self._league_rank_button = None
 209        # self._store_button = None
 210        # self._store_button_widget = None
 211        # self._league_rank_button_widget = None
 212
 213        # Move our corner buttons dynamically to keep them out of the way of
 214        # the party icon :-(
 215        # self._update_corner_button_positions()
 216        # self._update_corner_button_positions_timer = bui.AppTimer(
 217        #     1.0, bui.WeakCall(
 218        # self._update_corner_button_positions), repeat=True
 219        # )
 220
 221        self._last_tournament_query_time: float | None = None
 222        self._last_tournament_query_response_time: float | None = None
 223        self._doing_tournament_query = False
 224
 225        self._selected_campaign_level = cfg.get(
 226            'Selected Coop Campaign Level', None
 227        )
 228        self._selected_custom_level = cfg.get(
 229            'Selected Coop Custom Level', None
 230        )
 231
 232        # Don't want initial construction affecting our last-selected.
 233        self._do_selection_callbacks = False
 234        v = self._height - 95
 235        txt = bui.textwidget(
 236            parent=self._root_widget,
 237            position=(
 238                self._width * 0.5,
 239                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
 240            ),
 241            size=(0, 0),
 242            text=bui.Lstr(
 243                resource='playModes.singlePlayerCoopText',
 244                fallback_resource='playModes.coopText',
 245            ),
 246            h_align='center',
 247            color=app.ui_v1.title_color,
 248            scale=1.5,
 249            maxwidth=500,
 250            v_align='center',
 251        )
 252
 253        if uiscale is bui.UIScale.SMALL:
 254            bui.textwidget(edit=txt, text='')
 255
 256        self._selected_row = cfg.get('Selected Coop Row', None)
 257
 258        self._scroll_width = self._width - (130 + 2 * x_inset)
 259        self._scroll_height = self._height - (
 260            219 if uiscale is bui.UIScale.SMALL else 160
 261        )
 262
 263        self._subcontainerwidth = 800.0
 264        self._subcontainerheight = 1400.0
 265
 266        self._scrollwidget = bui.scrollwidget(
 267            parent=self._root_widget,
 268            highlight=False,
 269            position=(
 270                (65 + x_inset, 120)
 271                if uiscale is bui.UIScale.SMALL
 272                else (65 + x_inset, 70)
 273            ),
 274            size=(self._scroll_width, self._scroll_height),
 275            simple_culling_v=10.0,
 276            claims_left_right=True,
 277            selection_loops_to_parent=True,
 278        )
 279        self._subcontainer: bui.Widget | None = None
 280
 281        # Take note of our account state; we'll refresh later if this changes.
 282        self._account_state_num = plus.get_v1_account_state_num()
 283
 284        # Same for fg/bg state.
 285        self._fg_state = app.fg_state
 286
 287        self._refresh()
 288        self._restore_state()
 289
 290        # Even though we might display cached tournament data immediately, we
 291        # don't consider it valid until we've pinged.
 292        # the server for an update
 293        self._tourney_data_up_to_date = False
 294
 295        # If we've got a cached tournament list for our account and info for
 296        # each one of those tournaments, go ahead and display it as a
 297        # starting point.
 298        if (
 299            classic.accounts.account_tournament_list is not None
 300            and classic.accounts.account_tournament_list[0]
 301            == plus.get_v1_account_state_num()
 302            and all(
 303                t_id in classic.accounts.tournament_info
 304                for t_id in classic.accounts.account_tournament_list[1]
 305            )
 306        ):
 307            tourney_data = [
 308                classic.accounts.tournament_info[t_id]
 309                for t_id in classic.accounts.account_tournament_list[1]
 310            ]
 311            self._update_for_data(tourney_data)
 312
 313        # This will pull new data periodically, update timers, etc.
 314        self._update_timer = bui.AppTimer(
 315            1.0, bui.WeakCall(self._update), repeat=True
 316        )
 317        self._update()
 318
 319    @override
 320    def get_main_window_state(self) -> bui.MainWindowState:
 321        # Support recreating our window for back/refresh purposes.
 322        cls = type(self)
 323        return bui.BasicMainWindowState(
 324            create_call=lambda transition, origin_widget: cls(
 325                transition=transition, origin_widget=origin_widget
 326            )
 327        )
 328
 329    @override
 330    def on_main_window_close(self) -> None:
 331        self._save_state()
 332
 333    @staticmethod
 334    def _preload_modules() -> None:
 335        """Preload modules we use; avoids hitches (called in bg thread)."""
 336        # pylint: disable=cyclic-import
 337        import bauiv1lib.purchase as _unused1
 338        import bauiv1lib.coop.gamebutton as _unused2
 339        import bauiv1lib.confirm as _unused3
 340        import bauiv1lib.account as _unused4
 341        import bauiv1lib.league.rankwindow as _unused5
 342        import bauiv1lib.store.browser as _unused6
 343        import bauiv1lib.account.viewer as _unused7
 344        import bauiv1lib.tournamentscores as _unused8
 345        import bauiv1lib.tournamententry as _unused9
 346        import bauiv1lib.play as _unused10
 347        import bauiv1lib.coop.tournamentbutton as _unused11
 348
 349    def _update(self) -> None:
 350        plus = bui.app.plus
 351        assert plus is not None
 352
 353        # Do nothing if we've somehow outlived our actual UI.
 354        if not self._root_widget:
 355            return
 356
 357        cur_time = bui.apptime()
 358
 359        # If its been a while since we got a tournament update, consider
 360        # the data invalid (prevents us from joining tournaments if our
 361        # internet connection goes down for a while).
 362        if (
 363            self._last_tournament_query_response_time is None
 364            or bui.apptime() - self._last_tournament_query_response_time
 365            > 60.0 * 2
 366        ):
 367            self._tourney_data_up_to_date = False
 368
 369        # If our account state has changed, do a full request.
 370        account_state_num = plus.get_v1_account_state_num()
 371        if account_state_num != self._account_state_num:
 372            self._account_state_num = account_state_num
 373            self._save_state()
 374            self._refresh()
 375
 376            # Also encourage a new tournament query since this will clear out
 377            # our current results.
 378            if not self._doing_tournament_query:
 379                self._last_tournament_query_time = None
 380
 381        # If we've been backgrounded/foregrounded, invalidate our
 382        # tournament entries (they will be refreshed below asap).
 383        if self._fg_state != bui.app.fg_state:
 384            self._tourney_data_up_to_date = False
 385
 386        # Send off a new tournament query if its been long enough or whatnot.
 387        if not self._doing_tournament_query and (
 388            self._last_tournament_query_time is None
 389            or cur_time - self._last_tournament_query_time > 30.0
 390            or self._fg_state != bui.app.fg_state
 391        ):
 392            self._fg_state = bui.app.fg_state
 393            self._last_tournament_query_time = cur_time
 394            self._doing_tournament_query = True
 395            plus.tournament_query(
 396                args={'source': 'coop window refresh', 'numScores': 1},
 397                callback=bui.WeakCall(self._on_tournament_query_response),
 398            )
 399
 400        # Decrement time on our tournament buttons.
 401        ads_enabled = plus.have_incentivized_ad()
 402        for tbtn in self._tournament_buttons:
 403            tbtn.time_remaining = max(0, tbtn.time_remaining - 1)
 404            if tbtn.time_remaining_value_text is not None:
 405                bui.textwidget(
 406                    edit=tbtn.time_remaining_value_text,
 407                    text=(
 408                        bui.timestring(tbtn.time_remaining, centi=False)
 409                        if (
 410                            tbtn.has_time_remaining
 411                            and self._tourney_data_up_to_date
 412                        )
 413                        else '-'
 414                    ),
 415                )
 416
 417            # Also adjust the ad icon visibility.
 418            if tbtn.allow_ads and plus.has_video_ads():
 419                bui.imagewidget(
 420                    edit=tbtn.entry_fee_ad_image,
 421                    opacity=1.0 if ads_enabled else 0.25,
 422                )
 423                bui.textwidget(
 424                    edit=tbtn.entry_fee_text_remaining,
 425                    color=(0.6, 0.6, 0.6, 1 if ads_enabled else 0.2),
 426                )
 427
 428        self._update_hard_mode_lock_image()
 429
 430    def _update_hard_mode_lock_image(self) -> None:
 431        assert bui.app.classic is not None
 432        try:
 433            bui.imagewidget(
 434                edit=self._hard_button_lock_image,
 435                opacity=(
 436                    0.0
 437                    if (
 438                        (not HARD_REQUIRES_PRO)
 439                        or bui.app.classic.accounts.have_pro_options()
 440                    )
 441                    else 1.0
 442                ),
 443            )
 444        except Exception:
 445            logging.exception('Error updating campaign lock.')
 446
 447    def _update_for_data(self, data: list[dict[str, Any]] | None) -> None:
 448        # If the number of tournaments or challenges in the data differs
 449        # from our current arrangement, refresh with the new number.
 450        if (data is None and self._tournament_button_count != 0) or (
 451            data is not None and (len(data) != self._tournament_button_count)
 452        ):
 453            self._tournament_button_count = len(data) if data is not None else 0
 454            bui.app.config['Tournament Rows'] = self._tournament_button_count
 455            self._refresh()
 456
 457        # Update all of our tourney buttons based on whats in data.
 458        for i, tbtn in enumerate(self._tournament_buttons):
 459            assert data is not None
 460            tbtn.update_for_data(data[i])
 461
 462    def _on_tournament_query_response(
 463        self, data: dict[str, Any] | None
 464    ) -> None:
 465        plus = bui.app.plus
 466        assert plus is not None
 467
 468        assert bui.app.classic is not None
 469        accounts = bui.app.classic.accounts
 470        if data is not None:
 471            tournament_data = data['t']  # This used to be the whole payload.
 472            self._last_tournament_query_response_time = bui.apptime()
 473        else:
 474            tournament_data = None
 475
 476        # Keep our cached tourney info up to date.
 477        if data is not None:
 478            self._tourney_data_up_to_date = True
 479            accounts.cache_tournament_info(tournament_data)
 480
 481            # Also cache the current tourney list/order for this account.
 482            accounts.account_tournament_list = (
 483                plus.get_v1_account_state_num(),
 484                [e['tournamentID'] for e in tournament_data],
 485            )
 486
 487        self._doing_tournament_query = False
 488        self._update_for_data(tournament_data)
 489
 490    def _set_campaign_difficulty(self, difficulty: str) -> None:
 491        # pylint: disable=cyclic-import
 492        from bauiv1lib.purchase import PurchaseWindow
 493
 494        plus = bui.app.plus
 495        assert plus is not None
 496
 497        assert bui.app.classic is not None
 498        if difficulty != self._campaign_difficulty:
 499            if (
 500                difficulty == 'hard'
 501                and HARD_REQUIRES_PRO
 502                and not bui.app.classic.accounts.have_pro_options()
 503            ):
 504                PurchaseWindow(items=['pro'])
 505                return
 506            bui.getsound('gunCocking').play()
 507            if difficulty not in ('easy', 'hard'):
 508                print('ERROR: invalid campaign difficulty:', difficulty)
 509                difficulty = 'easy'
 510            self._campaign_difficulty = difficulty
 511            plus.add_v1_account_transaction(
 512                {
 513                    'type': 'SET_MISC_VAL',
 514                    'name': 'campaignDifficulty',
 515                    'value': difficulty,
 516                }
 517            )
 518            self._refresh_campaign_row()
 519        else:
 520            bui.getsound('click01').play()
 521
 522    def _refresh_campaign_row(self) -> None:
 523        # pylint: disable=too-many-locals
 524        # pylint: disable=too-many-statements
 525        # pylint: disable=cyclic-import
 526        from bauiv1lib.coop.gamebutton import GameButton
 527
 528        parent_widget = self._campaign_sub_container
 529
 530        # Clear out anything in the parent widget already.
 531        assert parent_widget is not None
 532        for child in parent_widget.get_children():
 533            child.delete()
 534
 535        next_widget_down = self._tournament_info_button
 536
 537        h = 0
 538        v2 = -2
 539        sel_color = (0.75, 0.85, 0.5)
 540        sel_color_hard = (0.4, 0.7, 0.2)
 541        un_sel_color = (0.5, 0.5, 0.5)
 542        sel_textcolor = (2, 2, 0.8)
 543        un_sel_textcolor = (0.6, 0.6, 0.6)
 544        self._easy_button = bui.buttonwidget(
 545            parent=parent_widget,
 546            position=(h + 30, v2 + 105),
 547            size=(120, 70),
 548            label=bui.Lstr(resource='difficultyEasyText'),
 549            button_type='square',
 550            autoselect=True,
 551            enable_sound=False,
 552            on_activate_call=bui.Call(self._set_campaign_difficulty, 'easy'),
 553            on_select_call=bui.Call(self.sel_change, 'campaign', 'easyButton'),
 554            color=(
 555                sel_color
 556                if self._campaign_difficulty == 'easy'
 557                else un_sel_color
 558            ),
 559            textcolor=(
 560                sel_textcolor
 561                if self._campaign_difficulty == 'easy'
 562                else un_sel_textcolor
 563            ),
 564        )
 565        bui.widget(edit=self._easy_button, show_buffer_left=100)
 566        if self._selected_campaign_level == 'easyButton':
 567            bui.containerwidget(
 568                edit=parent_widget,
 569                selected_child=self._easy_button,
 570                visible_child=self._easy_button,
 571            )
 572        lock_tex = bui.gettexture('lock')
 573
 574        self._hard_button = bui.buttonwidget(
 575            parent=parent_widget,
 576            position=(h + 30, v2 + 32),
 577            size=(120, 70),
 578            label=bui.Lstr(resource='difficultyHardText'),
 579            button_type='square',
 580            autoselect=True,
 581            enable_sound=False,
 582            on_activate_call=bui.Call(self._set_campaign_difficulty, 'hard'),
 583            on_select_call=bui.Call(self.sel_change, 'campaign', 'hardButton'),
 584            color=(
 585                sel_color_hard
 586                if self._campaign_difficulty == 'hard'
 587                else un_sel_color
 588            ),
 589            textcolor=(
 590                sel_textcolor
 591                if self._campaign_difficulty == 'hard'
 592                else un_sel_textcolor
 593            ),
 594        )
 595        self._hard_button_lock_image = bui.imagewidget(
 596            parent=parent_widget,
 597            size=(30, 30),
 598            draw_controller=self._hard_button,
 599            position=(h + 30 - 10, v2 + 32 + 70 - 35),
 600            texture=lock_tex,
 601        )
 602        self._update_hard_mode_lock_image()
 603        bui.widget(edit=self._hard_button, show_buffer_left=100)
 604        if self._selected_campaign_level == 'hardButton':
 605            bui.containerwidget(
 606                edit=parent_widget,
 607                selected_child=self._hard_button,
 608                visible_child=self._hard_button,
 609            )
 610
 611        bui.widget(edit=self._hard_button, down_widget=next_widget_down)
 612        h_spacing = 200
 613        campaign_buttons = []
 614        if self._campaign_difficulty == 'easy':
 615            campaignname = 'Easy'
 616        else:
 617            campaignname = 'Default'
 618        items = [
 619            campaignname + ':Onslaught Training',
 620            campaignname + ':Rookie Onslaught',
 621            campaignname + ':Rookie Football',
 622            campaignname + ':Pro Onslaught',
 623            campaignname + ':Pro Football',
 624            campaignname + ':Pro Runaround',
 625            campaignname + ':Uber Onslaught',
 626            campaignname + ':Uber Football',
 627            campaignname + ':Uber Runaround',
 628        ]
 629        items += [campaignname + ':The Last Stand']
 630        if self._selected_campaign_level is None:
 631            self._selected_campaign_level = items[0]
 632        h = 150
 633        for i in items:
 634            is_last_sel = i == self._selected_campaign_level
 635            campaign_buttons.append(
 636                GameButton(
 637                    self, parent_widget, i, h, v2, is_last_sel, 'campaign'
 638                ).get_button()
 639            )
 640            h += h_spacing
 641
 642        bui.widget(edit=campaign_buttons[0], left_widget=self._easy_button)
 643
 644        # bui.widget(edit=self._easy_button)
 645        # if self._back_button is not None:
 646        bui.widget(
 647            edit=self._easy_button,
 648            left_widget=self._back_button,
 649            up_widget=self._back_button,
 650        )
 651        bui.widget(edit=self._hard_button, left_widget=self._back_button)
 652        for btn in campaign_buttons:
 653            bui.widget(
 654                edit=btn,
 655                up_widget=self._back_button,
 656            )
 657        for btn in campaign_buttons:
 658            bui.widget(edit=btn, down_widget=next_widget_down)
 659
 660        # Update our existing percent-complete text.
 661        assert bui.app.classic is not None
 662        campaign = bui.app.classic.getcampaign(campaignname)
 663        levels = campaign.levels
 664        levels_complete = sum((1 if l.complete else 0) for l in levels)
 665
 666        # Last level cant be completed; hence the -1.
 667        progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 668        p_str = str(int(progress * 100.0)) + '%'
 669
 670        self._campaign_percent_text = bui.textwidget(
 671            edit=self._campaign_percent_text,
 672            text=bui.Lstr(
 673                value='${C} (${P})',
 674                subs=[
 675                    ('${C}', bui.Lstr(resource=f'{self._r}.campaignText')),
 676                    ('${P}', p_str),
 677                ],
 678            ),
 679        )
 680
 681    def _on_tournament_info_press(self) -> None:
 682        # pylint: disable=cyclic-import
 683        from bauiv1lib.confirm import ConfirmWindow
 684
 685        txt = bui.Lstr(resource=f'{self._r}.tournamentInfoText')
 686        ConfirmWindow(
 687            txt,
 688            cancel_button=False,
 689            width=550,
 690            height=260,
 691            origin_widget=self._tournament_info_button,
 692        )
 693
 694    def _refresh(self) -> None:
 695        # pylint: disable=too-many-statements
 696        # pylint: disable=too-many-branches
 697        # pylint: disable=too-many-locals
 698        # pylint: disable=cyclic-import
 699        from bauiv1lib.coop.gamebutton import GameButton
 700        from bauiv1lib.coop.tournamentbutton import TournamentButton
 701
 702        plus = bui.app.plus
 703        assert plus is not None
 704        assert bui.app.classic is not None
 705
 706        # (Re)create the sub-container if need be.
 707        if self._subcontainer is not None:
 708            self._subcontainer.delete()
 709
 710        tourney_row_height = 200
 711        self._subcontainerheight = (
 712            700 + self._tournament_button_count * tourney_row_height
 713        )
 714
 715        self._subcontainer = bui.containerwidget(
 716            parent=self._scrollwidget,
 717            size=(self._subcontainerwidth, self._subcontainerheight),
 718            background=False,
 719            claims_left_right=True,
 720            selection_loops_to_parent=True,
 721        )
 722
 723        bui.containerwidget(
 724            edit=self._root_widget, selected_child=self._scrollwidget
 725        )
 726
 727        w_parent = self._subcontainer
 728        h_base = 6
 729
 730        v = self._subcontainerheight - 90
 731
 732        self._campaign_percent_text = bui.textwidget(
 733            parent=w_parent,
 734            position=(h_base + 27, v + 30),
 735            size=(0, 0),
 736            text='',
 737            h_align='left',
 738            v_align='center',
 739            color=bui.app.ui_v1.title_color,
 740            scale=1.1,
 741        )
 742
 743        row_v_show_buffer = 80
 744        v -= 198
 745
 746        h_scroll = bui.hscrollwidget(
 747            parent=w_parent,
 748            size=(self._scroll_width - 10, 205),
 749            position=(-5, v),
 750            simple_culling_h=70,
 751            highlight=False,
 752            border_opacity=0.0,
 753            color=(0.45, 0.4, 0.5),
 754            on_select_call=lambda: self._on_row_selected('campaign'),
 755        )
 756        self._campaign_h_scroll = h_scroll
 757        bui.widget(
 758            edit=h_scroll,
 759            show_buffer_top=row_v_show_buffer,
 760            show_buffer_bottom=row_v_show_buffer,
 761            autoselect=True,
 762        )
 763        if self._selected_row == 'campaign':
 764            bui.containerwidget(
 765                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 766            )
 767        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 768        self._campaign_sub_container = bui.containerwidget(
 769            parent=h_scroll, size=(180 + 200 * 10, 200), background=False
 770        )
 771
 772        # Tournaments
 773
 774        self._tournament_buttons: list[TournamentButton] = []
 775
 776        v -= 53
 777        # FIXME shouldn't use hard-coded strings here.
 778        txt = bui.Lstr(
 779            resource='tournamentsText', fallback_resource='tournamentText'
 780        ).evaluate()
 781        t_width = bui.get_string_width(txt, suppress_warning=True)
 782        bui.textwidget(
 783            parent=w_parent,
 784            position=(h_base + 27, v + 30),
 785            size=(0, 0),
 786            text=txt,
 787            h_align='left',
 788            v_align='center',
 789            color=bui.app.ui_v1.title_color,
 790            scale=1.1,
 791        )
 792        self._tournament_info_button = bui.buttonwidget(
 793            parent=w_parent,
 794            label='?',
 795            size=(20, 20),
 796            text_scale=0.6,
 797            position=(h_base + 27 + t_width * 1.1 + 15, v + 18),
 798            button_type='square',
 799            color=(0.6, 0.5, 0.65),
 800            textcolor=(0.7, 0.6, 0.75),
 801            autoselect=True,
 802            up_widget=self._campaign_h_scroll,
 803            left_widget=self._back_button,
 804            on_activate_call=self._on_tournament_info_press,
 805        )
 806        bui.widget(
 807            edit=self._tournament_info_button,
 808            right_widget=self._tournament_info_button,
 809        )
 810
 811        # Say 'unavailable' if there are zero tournaments, and if we're
 812        # not signed in add that as well (that's probably why we see no
 813        # tournaments).
 814        if self._tournament_button_count == 0:
 815            unavailable_text = bui.Lstr(resource='unavailableText')
 816            if plus.get_v1_account_state() != 'signed_in':
 817                unavailable_text = bui.Lstr(
 818                    value='${A} (${B})',
 819                    subs=[
 820                        ('${A}', unavailable_text),
 821                        ('${B}', bui.Lstr(resource='notSignedInText')),
 822                    ],
 823                )
 824            bui.textwidget(
 825                parent=w_parent,
 826                position=(h_base + 47, v),
 827                size=(0, 0),
 828                text=unavailable_text,
 829                h_align='left',
 830                v_align='center',
 831                color=bui.app.ui_v1.title_color,
 832                scale=0.9,
 833            )
 834            v -= 40
 835        v -= 198
 836
 837        tournament_h_scroll = None
 838        if self._tournament_button_count > 0:
 839            for i in range(self._tournament_button_count):
 840                tournament_h_scroll = h_scroll = bui.hscrollwidget(
 841                    parent=w_parent,
 842                    size=(self._scroll_width - 10, 205),
 843                    position=(-5, v),
 844                    highlight=False,
 845                    border_opacity=0.0,
 846                    color=(0.45, 0.4, 0.5),
 847                    on_select_call=bui.Call(
 848                        self._on_row_selected, 'tournament' + str(i + 1)
 849                    ),
 850                )
 851                bui.widget(
 852                    edit=h_scroll,
 853                    show_buffer_top=row_v_show_buffer,
 854                    show_buffer_bottom=row_v_show_buffer,
 855                    autoselect=True,
 856                )
 857                if self._selected_row == 'tournament' + str(i + 1):
 858                    bui.containerwidget(
 859                        edit=w_parent,
 860                        selected_child=h_scroll,
 861                        visible_child=h_scroll,
 862                    )
 863                bui.containerwidget(edit=h_scroll, claims_left_right=True)
 864                sc2 = bui.containerwidget(
 865                    parent=h_scroll,
 866                    size=(self._scroll_width - 24, 200),
 867                    background=False,
 868                )
 869                h = 0
 870                v2 = -2
 871                is_last_sel = True
 872                self._tournament_buttons.append(
 873                    TournamentButton(
 874                        sc2,
 875                        h,
 876                        v2,
 877                        is_last_sel,
 878                        on_pressed=bui.WeakCall(self.run_tournament),
 879                    )
 880                )
 881                v -= 200
 882
 883        # Custom Games. (called 'Practice' in UI these days).
 884        v -= 50
 885        bui.textwidget(
 886            parent=w_parent,
 887            position=(h_base + 27, v + 30 + 198),
 888            size=(0, 0),
 889            text=bui.Lstr(
 890                resource='practiceText',
 891                fallback_resource='coopSelectWindow.customText',
 892            ),
 893            h_align='left',
 894            v_align='center',
 895            color=bui.app.ui_v1.title_color,
 896            scale=1.1,
 897        )
 898
 899        items = [
 900            'Challenges:Infinite Onslaught',
 901            'Challenges:Infinite Runaround',
 902            'Challenges:Ninja Fight',
 903            'Challenges:Pro Ninja Fight',
 904            'Challenges:Meteor Shower',
 905            'Challenges:Target Practice B',
 906            'Challenges:Target Practice',
 907        ]
 908
 909        # Show easter-egg-hunt either if its easter or we own it.
 910        if plus.get_v1_account_misc_read_val(
 911            'easter', False
 912        ) or plus.get_v1_account_product_purchased('games.easter_egg_hunt'):
 913            items = [
 914                'Challenges:Easter Egg Hunt',
 915                'Challenges:Pro Easter Egg Hunt',
 916            ] + items
 917
 918        # If we've defined custom games, put them at the beginning.
 919        if bui.app.classic.custom_coop_practice_games:
 920            items = bui.app.classic.custom_coop_practice_games + items
 921
 922        self._custom_h_scroll = custom_h_scroll = h_scroll = bui.hscrollwidget(
 923            parent=w_parent,
 924            size=(self._scroll_width - 10, 205),
 925            position=(-5, v),
 926            highlight=False,
 927            border_opacity=0.0,
 928            color=(0.45, 0.4, 0.5),
 929            on_select_call=bui.Call(self._on_row_selected, 'custom'),
 930        )
 931        bui.widget(
 932            edit=h_scroll,
 933            show_buffer_top=row_v_show_buffer,
 934            show_buffer_bottom=1.5 * row_v_show_buffer,
 935            autoselect=True,
 936        )
 937        if self._selected_row == 'custom':
 938            bui.containerwidget(
 939                edit=w_parent, selected_child=h_scroll, visible_child=h_scroll
 940            )
 941        bui.containerwidget(edit=h_scroll, claims_left_right=True)
 942        sc2 = bui.containerwidget(
 943            parent=h_scroll,
 944            size=(max(self._scroll_width - 24, 30 + 200 * len(items)), 200),
 945            background=False,
 946        )
 947        h_spacing = 200
 948        self._custom_buttons: list[GameButton] = []
 949        h = 0
 950        v2 = -2
 951        for item in items:
 952            is_last_sel = item == self._selected_custom_level
 953            self._custom_buttons.append(
 954                GameButton(self, sc2, item, h, v2, is_last_sel, 'custom')
 955            )
 956            h += h_spacing
 957
 958        # We can't fill in our campaign row until tourney buttons are in place.
 959        # (for wiring up)
 960        self._refresh_campaign_row()
 961
 962        for i, tbutton in enumerate(self._tournament_buttons):
 963            bui.widget(
 964                edit=tbutton.button,
 965                up_widget=(
 966                    self._tournament_info_button
 967                    if i == 0
 968                    else self._tournament_buttons[i - 1].button
 969                ),
 970                down_widget=(
 971                    self._tournament_buttons[(i + 1)].button
 972                    if i + 1 < len(self._tournament_buttons)
 973                    else custom_h_scroll
 974                ),
 975                left_widget=self._back_button,
 976            )
 977            bui.widget(
 978                edit=tbutton.more_scores_button,
 979                down_widget=(
 980                    self._tournament_buttons[(i + 1)].current_leader_name_text
 981                    if i + 1 < len(self._tournament_buttons)
 982                    else custom_h_scroll
 983                ),
 984            )
 985            bui.widget(
 986                edit=tbutton.current_leader_name_text,
 987                up_widget=(
 988                    self._tournament_info_button
 989                    if i == 0
 990                    else self._tournament_buttons[i - 1].more_scores_button
 991                ),
 992            )
 993
 994        for i, btn in enumerate(self._custom_buttons):
 995            try:
 996                bui.widget(
 997                    edit=btn.get_button(),
 998                    up_widget=(
 999                        tournament_h_scroll
1000                        if self._tournament_buttons
1001                        else self._tournament_info_button
1002                    ),
1003                )
1004                if i == 0:
1005                    bui.widget(
1006                        edit=btn.get_button(), left_widget=self._back_button
1007                    )
1008
1009            except Exception:
1010                logging.exception('Error wiring up custom buttons.')
1011
1012        # There's probably several 'onSelected' callbacks pushed onto the
1013        # event queue.. we need to push ours too so we're enabled *after* them.
1014        bui.pushcall(self._enable_selectable_callback)
1015
1016    def _on_row_selected(self, row: str) -> None:
1017        if self._do_selection_callbacks:
1018            if self._selected_row != row:
1019                self._selected_row = row
1020
1021    def _enable_selectable_callback(self) -> None:
1022        self._do_selection_callbacks = True
1023
1024    def is_tourney_data_up_to_date(self) -> bool:
1025        """Return whether our tourney data is up to date."""
1026        return self._tourney_data_up_to_date
1027
1028    def run_game(self, game: str) -> None:
1029        """Run the provided game."""
1030        # pylint: disable=too-many-branches
1031        # pylint: disable=cyclic-import
1032        from bauiv1lib.confirm import ConfirmWindow
1033        from bauiv1lib.purchase import PurchaseWindow
1034        from bauiv1lib.account.signin import show_sign_in_prompt
1035
1036        plus = bui.app.plus
1037        assert plus is not None
1038
1039        assert bui.app.classic is not None
1040
1041        args: dict[str, Any] = {}
1042
1043        if game == 'Easy:The Last Stand':
1044            ConfirmWindow(
1045                bui.Lstr(
1046                    resource='difficultyHardUnlockOnlyText',
1047                    fallback_resource='difficultyHardOnlyText',
1048                ),
1049                cancel_button=False,
1050                width=460,
1051                height=130,
1052            )
1053            return
1054
1055        # Infinite onslaught/runaround require pro; bring up a store link
1056        # if need be.
1057        if (
1058            game
1059            in (
1060                'Challenges:Infinite Runaround',
1061                'Challenges:Infinite Onslaught',
1062            )
1063            and not bui.app.classic.accounts.have_pro()
1064        ):
1065            if plus.get_v1_account_state() != 'signed_in':
1066                show_sign_in_prompt()
1067            else:
1068                PurchaseWindow(items=['pro'])
1069            return
1070
1071        required_purchase: str | None
1072        if game in ['Challenges:Meteor Shower']:
1073            required_purchase = 'games.meteor_shower'
1074        elif game in [
1075            'Challenges:Target Practice',
1076            'Challenges:Target Practice B',
1077        ]:
1078            required_purchase = 'games.target_practice'
1079        elif game in ['Challenges:Ninja Fight']:
1080            required_purchase = 'games.ninja_fight'
1081        elif game in ['Challenges:Pro Ninja Fight']:
1082            required_purchase = 'games.ninja_fight'
1083        elif game in [
1084            'Challenges:Easter Egg Hunt',
1085            'Challenges:Pro Easter Egg Hunt',
1086        ]:
1087            required_purchase = 'games.easter_egg_hunt'
1088        else:
1089            required_purchase = None
1090
1091        if (
1092            required_purchase is not None
1093            and not plus.get_v1_account_product_purchased(required_purchase)
1094        ):
1095            if plus.get_v1_account_state() != 'signed_in':
1096                show_sign_in_prompt()
1097            else:
1098                PurchaseWindow(items=[required_purchase])
1099            return
1100
1101        self._save_state()
1102
1103        if bui.app.classic.launch_coop_game(game, args=args):
1104            bui.containerwidget(edit=self._root_widget, transition='out_left')
1105
1106    def run_tournament(self, tournament_button: TournamentButton) -> None:
1107        """Run the provided tournament game."""
1108        from bauiv1lib.account.signin import show_sign_in_prompt
1109        from bauiv1lib.tournamententry import TournamentEntryWindow
1110
1111        plus = bui.app.plus
1112        assert plus is not None
1113
1114        if plus.get_v1_account_state() != 'signed_in':
1115            show_sign_in_prompt()
1116            return
1117
1118        if bui.workspaces_in_use():
1119            bui.screenmessage(
1120                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1121                color=(1, 0, 0),
1122            )
1123            bui.getsound('error').play()
1124            return
1125
1126        if not self._tourney_data_up_to_date:
1127            bui.screenmessage(
1128                bui.Lstr(resource='tournamentCheckingStateText'),
1129                color=(1, 1, 0),
1130            )
1131            bui.getsound('error').play()
1132            return
1133
1134        if tournament_button.tournament_id is None:
1135            bui.screenmessage(
1136                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1137                color=(1, 0, 0),
1138            )
1139            bui.getsound('error').play()
1140            return
1141
1142        if tournament_button.required_league is not None:
1143            bui.screenmessage(
1144                bui.Lstr(
1145                    resource='league.tournamentLeagueText',
1146                    subs=[
1147                        (
1148                            '${NAME}',
1149                            bui.Lstr(
1150                                translate=(
1151                                    'leagueNames',
1152                                    tournament_button.required_league,
1153                                )
1154                            ),
1155                        )
1156                    ],
1157                ),
1158                color=(1, 0, 0),
1159            )
1160            bui.getsound('error').play()
1161            return
1162
1163        if tournament_button.time_remaining <= 0:
1164            bui.screenmessage(
1165                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1166            )
1167            bui.getsound('error').play()
1168            return
1169
1170        self._save_state()
1171
1172        assert tournament_button.tournament_id is not None
1173        TournamentEntryWindow(
1174            tournament_id=tournament_button.tournament_id,
1175            position=tournament_button.button.get_screen_space_center(),
1176        )
1177
1178    def _save_state(self) -> None:
1179        cfg = bui.app.config
1180        try:
1181            sel = self._root_widget.get_selected_child()
1182            if sel == self._back_button:
1183                sel_name = 'Back'
1184            # elif sel == self._store_button_widget:
1185            #     sel_name = 'Store'
1186            # elif sel == self._league_rank_button_widget:
1187            #     sel_name = 'PowerRanking'
1188            elif sel == self._scrollwidget:
1189                sel_name = 'Scroll'
1190            else:
1191                raise ValueError('unrecognized selection')
1192            assert bui.app.classic is not None
1193            bui.app.ui_v1.window_states[type(self)] = {'sel_name': sel_name}
1194        except Exception:
1195            logging.exception('Error saving state for %s.', self)
1196
1197        cfg['Selected Coop Row'] = self._selected_row
1198        cfg['Selected Coop Custom Level'] = self._selected_custom_level
1199        cfg['Selected Coop Campaign Level'] = self._selected_campaign_level
1200        cfg.commit()
1201
1202    def _restore_state(self) -> None:
1203        try:
1204            assert bui.app.classic is not None
1205            sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get(
1206                'sel_name'
1207            )
1208            if sel_name == 'Back':
1209                sel = self._back_button
1210            elif sel_name == 'Scroll':
1211                sel = self._scrollwidget
1212            # elif sel_name == 'PowerRanking':
1213            #     sel = self._league_rank_button_widget
1214            # elif sel_name == 'Store':
1215            #     sel = self._store_button_widget
1216            else:
1217                sel = self._scrollwidget
1218            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1219        except Exception:
1220            logging.exception('Error restoring state for %s.', self)
1221
1222    def sel_change(self, row: str, game: str) -> None:
1223        """(internal)"""
1224        if self._do_selection_callbacks:
1225            if row == 'custom':
1226                self._selected_custom_level = game
1227            elif row == 'campaign':
1228                self._selected_campaign_level = game

Window for browsing co-op levels/games/etc.

CoopBrowserWindow( transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None)
 26    def __init__(
 27        self,
 28        transition: str | None = 'in_right',
 29        origin_widget: bui.Widget | None = None,
 30    ):
 31        # pylint: disable=too-many-statements
 32        # pylint: disable=cyclic-import
 33
 34        plus = bui.app.plus
 35        assert plus is not None
 36
 37        # Preload some modules we use in a background thread so we won't
 38        # have a visual hitch when the user taps them.
 39        bui.app.threadpool.submit_no_wait(self._preload_modules)
 40
 41        bui.set_analytics_screen('Coop Window')
 42
 43        app = bui.app
 44        classic = app.classic
 45        assert classic is not None
 46        cfg = app.config
 47
 48        # Quick note to players that tourneys won't work in ballistica
 49        # core builds. (need to split the word so it won't get subbed
 50        # out)
 51        if 'ballistica' + 'kit' == bui.appname() and bui.do_once():
 52            bui.apptimer(
 53                1.0,
 54                lambda: bui.screenmessage(
 55                    bui.Lstr(resource='noTournamentsInTestBuildText'),
 56                    color=(1, 1, 0),
 57                ),
 58            )
 59
 60        # Try to recreate the same number of buttons we had last time so our
 61        # re-selection code works.
 62        self._tournament_button_count = app.config.get('Tournament Rows', 0)
 63        assert isinstance(self._tournament_button_count, int)
 64
 65        self.star_tex = bui.gettexture('star')
 66        self.lsbt = bui.getmesh('level_select_button_transparent')
 67        self.lsbo = bui.getmesh('level_select_button_opaque')
 68        self.a_outline_tex = bui.gettexture('achievementOutline')
 69        self.a_outline_mesh = bui.getmesh('achievementOutline')
 70        self._campaign_sub_container: bui.Widget | None = None
 71        self._tournament_info_button: bui.Widget | None = None
 72        self._easy_button: bui.Widget | None = None
 73        self._hard_button: bui.Widget | None = None
 74        self._hard_button_lock_image: bui.Widget | None = None
 75        self._campaign_percent_text: bui.Widget | None = None
 76
 77        uiscale = app.ui_v1.uiscale
 78        self._width = 1520 if uiscale is bui.UIScale.SMALL else 1120
 79        self._x_inset = x_inset = 200 if uiscale is bui.UIScale.SMALL else 0
 80        self._height = (
 81            600
 82            if uiscale is bui.UIScale.SMALL
 83            else 730 if uiscale is bui.UIScale.MEDIUM else 800
 84        )
 85        self._r = 'coopSelectWindow'
 86        top_extra = 0 if uiscale is bui.UIScale.SMALL else 0
 87
 88        self._tourney_data_up_to_date = False
 89
 90        self._campaign_difficulty = plus.get_v1_account_misc_val(
 91            'campaignDifficulty', 'easy'
 92        )
 93
 94        if (
 95            self._campaign_difficulty == 'hard'
 96            and HARD_REQUIRES_PRO
 97            and not classic.accounts.have_pro_options()
 98        ):
 99            plus.add_v1_account_transaction(
100                {
101                    'type': 'SET_MISC_VAL',
102                    'name': 'campaignDifficulty',
103                    'value': 'easy',
104                }
105            )
106            self._campaign_difficulty = 'easy'
107
108        super().__init__(
109            root_widget=bui.containerwidget(
110                size=(self._width, self._height + top_extra),
111                toolbar_visibility='menu_full',
112                stack_offset=(
113                    (0, -8)
114                    if uiscale is bui.UIScale.SMALL
115                    else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0)
116                ),
117                scale=(
118                    1.28
119                    if uiscale is bui.UIScale.SMALL
120                    else 0.8 if uiscale is bui.UIScale.MEDIUM else 0.75
121                ),
122            ),
123            transition=transition,
124            origin_widget=origin_widget,
125        )
126
127        if uiscale is bui.UIScale.SMALL:
128            self._back_button = bui.get_special_widget('back_button')
129            bui.containerwidget(
130                edit=self._root_widget, on_cancel_call=self.main_window_back
131            )
132        else:
133            self._back_button = bui.buttonwidget(
134                parent=self._root_widget,
135                position=(75 + x_inset, self._height - 87),
136                size=(120, 60),
137                scale=1.2,
138                autoselect=True,
139                label=bui.Lstr(resource='backText'),
140                button_type='back',
141                on_activate_call=self.main_window_back,
142            )
143            bui.buttonwidget(
144                edit=self._back_button,
145                button_type='backSmall',
146                size=(60, 50),
147                position=(
148                    75 + x_inset,
149                    self._height - 87 + 6,
150                ),
151                label=bui.charstr(bui.SpecialChar.BACK),
152            )
153            bui.containerwidget(
154                edit=self._root_widget, cancel_button=self._back_button
155            )
156
157        # self._league_rank_button: LeagueRankButton | None
158        # self._store_button: StoreButton | None
159        # self._store_button_widget: bui.Widget | None
160        # self._league_rank_button_widget: bui.Widget | None
161
162        # if not app.ui_v1.use_toolbars:
163        #     prb = self._league_rank_button = LeagueRankButton(
164        #         parent=self._root_widget,
165        #         position=(
166        #             self._width - (282 + x_inset),
167        #             self._height
168        #             - 85
169        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
170        #         ),
171        #         size=(100, 60),
172        #         color=(0.4, 0.4, 0.9),
173        #         textcolor=(0.9, 0.9, 2.0),
174        #         scale=1.05,
175        #         on_activate_call=bui.WeakCall(
176        # self._switch_to_league_rankings),
177        #     )
178        #     self._league_rank_button_widget = prb.get_button()
179
180        #     sbtn = self._store_button = StoreButton(
181        #         parent=self._root_widget,
182        #         position=(
183        #             self._width - (170 + x_inset),
184        #             self._height
185        #             - 85
186        #             - (4 if uiscale is bui.UIScale.SMALL else 0),
187        #         ),
188        #         size=(100, 60),
189        #         color=(0.6, 0.4, 0.7),
190        #         show_tickets=True,
191        #         button_type='square',
192        #         sale_scale=0.85,
193        #         textcolor=(0.9, 0.7, 1.0),
194        #         scale=1.05,
195        #         on_activate_call=bui.WeakCall(self._switch_to_score, None),
196        #     )
197        #     self._store_button_widget = sbtn.get_button()
198        #     assert self._back_button is not None
199        #     bui.widget(
200        #         edit=self._back_button,
201        #         right_widget=self._league_rank_button_widget,
202        #     )
203        #     bui.widget(
204        #         edit=self._league_rank_button_widget,
205        #         left_widget=self._back_button,
206        #     )
207        # else:
208        # self._league_rank_button = None
209        # self._store_button = None
210        # self._store_button_widget = None
211        # self._league_rank_button_widget = None
212
213        # Move our corner buttons dynamically to keep them out of the way of
214        # the party icon :-(
215        # self._update_corner_button_positions()
216        # self._update_corner_button_positions_timer = bui.AppTimer(
217        #     1.0, bui.WeakCall(
218        # self._update_corner_button_positions), repeat=True
219        # )
220
221        self._last_tournament_query_time: float | None = None
222        self._last_tournament_query_response_time: float | None = None
223        self._doing_tournament_query = False
224
225        self._selected_campaign_level = cfg.get(
226            'Selected Coop Campaign Level', None
227        )
228        self._selected_custom_level = cfg.get(
229            'Selected Coop Custom Level', None
230        )
231
232        # Don't want initial construction affecting our last-selected.
233        self._do_selection_callbacks = False
234        v = self._height - 95
235        txt = bui.textwidget(
236            parent=self._root_widget,
237            position=(
238                self._width * 0.5,
239                v + 40 - (0 if uiscale is bui.UIScale.SMALL else 0),
240            ),
241            size=(0, 0),
242            text=bui.Lstr(
243                resource='playModes.singlePlayerCoopText',
244                fallback_resource='playModes.coopText',
245            ),
246            h_align='center',
247            color=app.ui_v1.title_color,
248            scale=1.5,
249            maxwidth=500,
250            v_align='center',
251        )
252
253        if uiscale is bui.UIScale.SMALL:
254            bui.textwidget(edit=txt, text='')
255
256        self._selected_row = cfg.get('Selected Coop Row', None)
257
258        self._scroll_width = self._width - (130 + 2 * x_inset)
259        self._scroll_height = self._height - (
260            219 if uiscale is bui.UIScale.SMALL else 160
261        )
262
263        self._subcontainerwidth = 800.0
264        self._subcontainerheight = 1400.0
265
266        self._scrollwidget = bui.scrollwidget(
267            parent=self._root_widget,
268            highlight=False,
269            position=(
270                (65 + x_inset, 120)
271                if uiscale is bui.UIScale.SMALL
272                else (65 + x_inset, 70)
273            ),
274            size=(self._scroll_width, self._scroll_height),
275            simple_culling_v=10.0,
276            claims_left_right=True,
277            selection_loops_to_parent=True,
278        )
279        self._subcontainer: bui.Widget | None = None
280
281        # Take note of our account state; we'll refresh later if this changes.
282        self._account_state_num = plus.get_v1_account_state_num()
283
284        # Same for fg/bg state.
285        self._fg_state = app.fg_state
286
287        self._refresh()
288        self._restore_state()
289
290        # Even though we might display cached tournament data immediately, we
291        # don't consider it valid until we've pinged.
292        # the server for an update
293        self._tourney_data_up_to_date = False
294
295        # If we've got a cached tournament list for our account and info for
296        # each one of those tournaments, go ahead and display it as a
297        # starting point.
298        if (
299            classic.accounts.account_tournament_list is not None
300            and classic.accounts.account_tournament_list[0]
301            == plus.get_v1_account_state_num()
302            and all(
303                t_id in classic.accounts.tournament_info
304                for t_id in classic.accounts.account_tournament_list[1]
305            )
306        ):
307            tourney_data = [
308                classic.accounts.tournament_info[t_id]
309                for t_id in classic.accounts.account_tournament_list[1]
310            ]
311            self._update_for_data(tourney_data)
312
313        # This will pull new data periodically, update timers, etc.
314        self._update_timer = bui.AppTimer(
315            1.0, bui.WeakCall(self._update), repeat=True
316        )
317        self._update()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

star_tex
lsbt
lsbo
a_outline_tex
a_outline_mesh
@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
319    @override
320    def get_main_window_state(self) -> bui.MainWindowState:
321        # Support recreating our window for back/refresh purposes.
322        cls = type(self)
323        return bui.BasicMainWindowState(
324            create_call=lambda transition, origin_widget: cls(
325                transition=transition, origin_widget=origin_widget
326            )
327        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
329    @override
330    def on_main_window_close(self) -> None:
331        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

def is_tourney_data_up_to_date(self) -> bool:
1024    def is_tourney_data_up_to_date(self) -> bool:
1025        """Return whether our tourney data is up to date."""
1026        return self._tourney_data_up_to_date

Return whether our tourney data is up to date.

def run_game(self, game: str) -> None:
1028    def run_game(self, game: str) -> None:
1029        """Run the provided game."""
1030        # pylint: disable=too-many-branches
1031        # pylint: disable=cyclic-import
1032        from bauiv1lib.confirm import ConfirmWindow
1033        from bauiv1lib.purchase import PurchaseWindow
1034        from bauiv1lib.account.signin import show_sign_in_prompt
1035
1036        plus = bui.app.plus
1037        assert plus is not None
1038
1039        assert bui.app.classic is not None
1040
1041        args: dict[str, Any] = {}
1042
1043        if game == 'Easy:The Last Stand':
1044            ConfirmWindow(
1045                bui.Lstr(
1046                    resource='difficultyHardUnlockOnlyText',
1047                    fallback_resource='difficultyHardOnlyText',
1048                ),
1049                cancel_button=False,
1050                width=460,
1051                height=130,
1052            )
1053            return
1054
1055        # Infinite onslaught/runaround require pro; bring up a store link
1056        # if need be.
1057        if (
1058            game
1059            in (
1060                'Challenges:Infinite Runaround',
1061                'Challenges:Infinite Onslaught',
1062            )
1063            and not bui.app.classic.accounts.have_pro()
1064        ):
1065            if plus.get_v1_account_state() != 'signed_in':
1066                show_sign_in_prompt()
1067            else:
1068                PurchaseWindow(items=['pro'])
1069            return
1070
1071        required_purchase: str | None
1072        if game in ['Challenges:Meteor Shower']:
1073            required_purchase = 'games.meteor_shower'
1074        elif game in [
1075            'Challenges:Target Practice',
1076            'Challenges:Target Practice B',
1077        ]:
1078            required_purchase = 'games.target_practice'
1079        elif game in ['Challenges:Ninja Fight']:
1080            required_purchase = 'games.ninja_fight'
1081        elif game in ['Challenges:Pro Ninja Fight']:
1082            required_purchase = 'games.ninja_fight'
1083        elif game in [
1084            'Challenges:Easter Egg Hunt',
1085            'Challenges:Pro Easter Egg Hunt',
1086        ]:
1087            required_purchase = 'games.easter_egg_hunt'
1088        else:
1089            required_purchase = None
1090
1091        if (
1092            required_purchase is not None
1093            and not plus.get_v1_account_product_purchased(required_purchase)
1094        ):
1095            if plus.get_v1_account_state() != 'signed_in':
1096                show_sign_in_prompt()
1097            else:
1098                PurchaseWindow(items=[required_purchase])
1099            return
1100
1101        self._save_state()
1102
1103        if bui.app.classic.launch_coop_game(game, args=args):
1104            bui.containerwidget(edit=self._root_widget, transition='out_left')

Run the provided game.

def run_tournament( self, tournament_button: bauiv1lib.coop.tournamentbutton.TournamentButton) -> None:
1106    def run_tournament(self, tournament_button: TournamentButton) -> None:
1107        """Run the provided tournament game."""
1108        from bauiv1lib.account.signin import show_sign_in_prompt
1109        from bauiv1lib.tournamententry import TournamentEntryWindow
1110
1111        plus = bui.app.plus
1112        assert plus is not None
1113
1114        if plus.get_v1_account_state() != 'signed_in':
1115            show_sign_in_prompt()
1116            return
1117
1118        if bui.workspaces_in_use():
1119            bui.screenmessage(
1120                bui.Lstr(resource='tournamentsDisabledWorkspaceText'),
1121                color=(1, 0, 0),
1122            )
1123            bui.getsound('error').play()
1124            return
1125
1126        if not self._tourney_data_up_to_date:
1127            bui.screenmessage(
1128                bui.Lstr(resource='tournamentCheckingStateText'),
1129                color=(1, 1, 0),
1130            )
1131            bui.getsound('error').play()
1132            return
1133
1134        if tournament_button.tournament_id is None:
1135            bui.screenmessage(
1136                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1137                color=(1, 0, 0),
1138            )
1139            bui.getsound('error').play()
1140            return
1141
1142        if tournament_button.required_league is not None:
1143            bui.screenmessage(
1144                bui.Lstr(
1145                    resource='league.tournamentLeagueText',
1146                    subs=[
1147                        (
1148                            '${NAME}',
1149                            bui.Lstr(
1150                                translate=(
1151                                    'leagueNames',
1152                                    tournament_button.required_league,
1153                                )
1154                            ),
1155                        )
1156                    ],
1157                ),
1158                color=(1, 0, 0),
1159            )
1160            bui.getsound('error').play()
1161            return
1162
1163        if tournament_button.time_remaining <= 0:
1164            bui.screenmessage(
1165                bui.Lstr(resource='tournamentEndedText'), color=(1, 0, 0)
1166            )
1167            bui.getsound('error').play()
1168            return
1169
1170        self._save_state()
1171
1172        assert tournament_button.tournament_id is not None
1173        TournamentEntryWindow(
1174            tournament_id=tournament_button.tournament_id,
1175            position=tournament_button.button.get_screen_space_center(),
1176        )

Run the provided tournament game.