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

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
330    @override
331    def on_main_window_close(self) -> None:
332        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:
1026    def is_tourney_data_up_to_date(self) -> bool:
1027        """Return whether our tourney data is up to date."""
1028        return self._tourney_data_up_to_date

Return whether our tourney data is up to date.

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

Run the provided game.

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

Run the provided tournament game.