bastd.ui.account.settings
Provides UI for account functionality.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for account functionality.""" 4# pylint: disable=too-many-lines 5 6from __future__ import annotations 7 8import time 9import logging 10from typing import TYPE_CHECKING 11 12import bacommon.cloud 13from bacommon.login import LoginType 14import ba 15import ba.internal 16 17if TYPE_CHECKING: 18 from ba.internal import LoginAdapter 19 20# These days we're directing people to the web based account settings 21# for V2 account linking and trying to get them to disconnect remaining 22# V1 links, but leaving this escape hatch here in case needed. 23FORCE_ENABLE_V1_LINKING = False 24 25 26class AccountSettingsWindow(ba.Window): 27 """Window for account related functionality.""" 28 29 def __init__( 30 self, 31 transition: str = 'in_right', 32 modal: bool = False, 33 origin_widget: ba.Widget | None = None, 34 close_once_signed_in: bool = False, 35 ): 36 # pylint: disable=too-many-statements 37 38 self._sign_in_v2_proxy_button: ba.Widget | None = None 39 self._sign_in_device_button: ba.Widget | None = None 40 41 self._show_legacy_unlink_button = False 42 43 self._signing_in_adapter: LoginAdapter | None = None 44 self._close_once_signed_in = close_once_signed_in 45 ba.set_analytics_screen('Account Window') 46 47 self._explicitly_signed_out_of_gpgs = False 48 49 # If they provided an origin-widget, scale up from that. 50 scale_origin: tuple[float, float] | None 51 if origin_widget is not None: 52 self._transition_out = 'out_scale' 53 scale_origin = origin_widget.get_screen_space_center() 54 transition = 'in_scale' 55 else: 56 self._transition_out = 'out_right' 57 scale_origin = None 58 59 self._r = 'accountSettingsWindow' 60 self._modal = modal 61 self._needs_refresh = False 62 self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in' 63 self._v1_account_state_num = ba.internal.get_v1_account_state_num() 64 self._check_sign_in_timer = ba.Timer( 65 1.0, 66 ba.WeakCall(self._update), 67 timetype=ba.TimeType.REAL, 68 repeat=True, 69 ) 70 71 # Currently we can only reset achievements on game-center. 72 v1_account_type: str | None 73 if self._v1_signed_in: 74 v1_account_type = ba.internal.get_v1_account_type() 75 else: 76 v1_account_type = None 77 self._can_reset_achievements = v1_account_type == 'Game Center' 78 79 app = ba.app 80 uiscale = app.ui.uiscale 81 82 self._width = 760 if uiscale is ba.UIScale.SMALL else 660 83 x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 84 self._height = ( 85 390 86 if uiscale is ba.UIScale.SMALL 87 else 430 88 if uiscale is ba.UIScale.MEDIUM 89 else 490 90 ) 91 92 self._sign_in_button = None 93 self._sign_in_text = None 94 95 self._scroll_width = self._width - (100 + x_offs * 2) 96 self._scroll_height = self._height - 120 97 self._sub_width = self._scroll_width - 20 98 99 # Determine which sign-in/sign-out buttons we should show. 100 self._show_sign_in_buttons: list[str] = [] 101 102 if LoginType.GPGS in ba.app.accounts_v2.login_adapters: 103 self._show_sign_in_buttons.append('Google Play') 104 105 # Always want to show our web-based v2 login option. 106 self._show_sign_in_buttons.append('V2Proxy') 107 108 # Legacy v1 device accounts are currently always available 109 # (though we need to start phasing them out at some point). 110 self._show_sign_in_buttons.append('Device') 111 112 top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 113 super().__init__( 114 root_widget=ba.containerwidget( 115 size=(self._width, self._height + top_extra), 116 transition=transition, 117 toolbar_visibility='menu_minimal', 118 scale_origin_stack_offset=scale_origin, 119 scale=( 120 2.09 121 if uiscale is ba.UIScale.SMALL 122 else 1.4 123 if uiscale is ba.UIScale.MEDIUM 124 else 1.0 125 ), 126 stack_offset=(0, -19) 127 if uiscale is ba.UIScale.SMALL 128 else (0, 0), 129 ) 130 ) 131 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 132 self._back_button = None 133 ba.containerwidget( 134 edit=self._root_widget, on_cancel_call=self._back 135 ) 136 else: 137 self._back_button = btn = ba.buttonwidget( 138 parent=self._root_widget, 139 position=(51 + x_offs, self._height - 62), 140 size=(120, 60), 141 scale=0.8, 142 text_scale=1.2, 143 autoselect=True, 144 label=ba.Lstr( 145 resource='doneText' if self._modal else 'backText' 146 ), 147 button_type='regular' if self._modal else 'back', 148 on_activate_call=self._back, 149 ) 150 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 151 if not self._modal: 152 ba.buttonwidget( 153 edit=btn, 154 button_type='backSmall', 155 size=(60, 56), 156 label=ba.charstr(ba.SpecialChar.BACK), 157 ) 158 159 ba.textwidget( 160 parent=self._root_widget, 161 position=(self._width * 0.5, self._height - 41), 162 size=(0, 0), 163 text=ba.Lstr(resource=self._r + '.titleText'), 164 color=ba.app.ui.title_color, 165 maxwidth=self._width - 340, 166 h_align='center', 167 v_align='center', 168 ) 169 170 self._scrollwidget = ba.scrollwidget( 171 parent=self._root_widget, 172 highlight=False, 173 position=( 174 (self._width - self._scroll_width) * 0.5, 175 self._height - 65 - self._scroll_height, 176 ), 177 size=(self._scroll_width, self._scroll_height), 178 claims_left_right=True, 179 claims_tab=True, 180 selection_loops_to_parent=True, 181 ) 182 self._subcontainer: ba.Widget | None = None 183 self._refresh() 184 self._restore_state() 185 186 def _update(self) -> None: 187 188 # If they want us to close once we're signed in, do so. 189 if self._close_once_signed_in and self._v1_signed_in: 190 self._back() 191 return 192 193 # Hmm should update this to use get_account_state_num. 194 # Theoretically if we switch from one signed-in account to another 195 # in the background this would break. 196 v1_account_state_num = ba.internal.get_v1_account_state_num() 197 v1_account_state = ba.internal.get_v1_account_state() 198 show_legacy_unlink_button = self._should_show_legacy_unlink_button() 199 200 if ( 201 v1_account_state_num != self._v1_account_state_num 202 or show_legacy_unlink_button != self._show_legacy_unlink_button 203 or self._needs_refresh 204 ): 205 self._v1_account_state_num = v1_account_state_num 206 self._v1_signed_in = v1_account_state == 'signed_in' 207 self._show_legacy_unlink_button = show_legacy_unlink_button 208 self._refresh() 209 210 # Go ahead and refresh some individual things 211 # that may change under us. 212 self._update_linked_accounts_text() 213 self._update_unlink_accounts_button() 214 self._refresh_campaign_progress_text() 215 self._refresh_achievements() 216 self._refresh_tickets_text() 217 self._refresh_account_name_text() 218 219 def _get_sign_in_text(self) -> ba.Lstr: 220 return ba.Lstr(resource=self._r + '.signInText') 221 222 def _refresh(self) -> None: 223 # pylint: disable=too-many-statements 224 # pylint: disable=too-many-branches 225 # pylint: disable=too-many-locals 226 # pylint: disable=cyclic-import 227 from bastd.ui import confirm 228 229 primary_v2_account = ba.app.accounts_v2.primary 230 231 v1_state = ba.internal.get_v1_account_state() 232 v1_account_type = ( 233 ba.internal.get_v1_account_type() 234 if v1_state == 'signed_in' 235 else 'unknown' 236 ) 237 238 # We expose GPGS-specific functionality only if it is 'active' 239 # (meaning the current GPGS player matches one of our account's 240 # logins). 241 gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS) 242 is_gpgs = ( 243 False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() 244 ) 245 246 show_signed_in_as = self._v1_signed_in 247 signed_in_as_space = 95.0 248 249 show_sign_in_benefits = not self._v1_signed_in 250 sign_in_benefits_space = 80.0 251 252 show_signing_in_text = ( 253 v1_state == 'signing_in' or self._signing_in_adapter is not None 254 ) 255 signing_in_text_space = 80.0 256 257 show_google_play_sign_in_button = ( 258 v1_state == 'signed_out' 259 and self._signing_in_adapter is None 260 and 'Google Play' in self._show_sign_in_buttons 261 ) 262 show_v2_proxy_sign_in_button = ( 263 v1_state == 'signed_out' 264 and self._signing_in_adapter is None 265 and 'V2Proxy' in self._show_sign_in_buttons 266 ) 267 show_device_sign_in_button = ( 268 v1_state == 'signed_out' 269 and self._signing_in_adapter is None 270 and 'Device' in self._show_sign_in_buttons 271 ) 272 sign_in_button_space = 70.0 273 deprecated_space = 60 274 275 show_game_service_button = self._v1_signed_in and v1_account_type in [ 276 'Game Center' 277 ] 278 game_service_button_space = 60.0 279 280 show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2' 281 282 show_linked_accounts_text = self._v1_signed_in 283 linked_accounts_text_space = 60.0 284 285 show_achievements_button = self._v1_signed_in and v1_account_type in ( 286 'Google Play', 287 'Local', 288 'V2', 289 ) 290 achievements_button_space = 60.0 291 292 show_achievements_text = ( 293 self._v1_signed_in and not show_achievements_button 294 ) 295 achievements_text_space = 27.0 296 297 show_leaderboards_button = self._v1_signed_in and is_gpgs 298 leaderboards_button_space = 60.0 299 300 show_campaign_progress = self._v1_signed_in 301 campaign_progress_space = 27.0 302 303 show_tickets = self._v1_signed_in 304 tickets_space = 27.0 305 306 show_reset_progress_button = False 307 reset_progress_button_space = 70.0 308 309 show_manage_v2_account_button = ( 310 self._v1_signed_in and v1_account_type == 'V2' 311 ) 312 manage_v2_account_button_space = 100.0 313 314 show_player_profiles_button = self._v1_signed_in 315 player_profiles_button_space = ( 316 70.0 if show_manage_v2_account_button else 100.0 317 ) 318 319 show_link_accounts_button = self._v1_signed_in and ( 320 primary_v2_account is None or FORCE_ENABLE_V1_LINKING 321 ) 322 link_accounts_button_space = 70.0 323 324 show_unlink_accounts_button = show_link_accounts_button 325 unlink_accounts_button_space = 90.0 326 327 show_v2_link_info = self._v1_signed_in and not show_link_accounts_button 328 v2_link_info_space = 70.0 329 330 legacy_unlink_button_space = 120.0 331 332 show_sign_out_button = self._v1_signed_in and v1_account_type in [ 333 'Local', 334 'Google Play', 335 'V2', 336 ] 337 sign_out_button_space = 70.0 338 339 # We can show cancel if we're either waiting on an adapter to 340 # provide us with v2 credentials or waiting for those credentials 341 # to be verified. 342 show_cancel_sign_in_button = self._signing_in_adapter is not None or ( 343 ba.app.accounts_v2.have_primary_credentials() 344 and primary_v2_account is None 345 ) 346 cancel_sign_in_button_space = 70.0 347 348 if self._subcontainer is not None: 349 self._subcontainer.delete() 350 self._sub_height = 60.0 351 if show_signed_in_as: 352 self._sub_height += signed_in_as_space 353 if show_signing_in_text: 354 self._sub_height += signing_in_text_space 355 if show_google_play_sign_in_button: 356 self._sub_height += sign_in_button_space 357 if show_v2_proxy_sign_in_button: 358 self._sub_height += sign_in_button_space 359 if show_device_sign_in_button: 360 self._sub_height += sign_in_button_space + deprecated_space 361 if show_game_service_button: 362 self._sub_height += game_service_button_space 363 if show_linked_accounts_text: 364 self._sub_height += linked_accounts_text_space 365 if show_achievements_text: 366 self._sub_height += achievements_text_space 367 if show_achievements_button: 368 self._sub_height += achievements_button_space 369 if show_leaderboards_button: 370 self._sub_height += leaderboards_button_space 371 if show_campaign_progress: 372 self._sub_height += campaign_progress_space 373 if show_tickets: 374 self._sub_height += tickets_space 375 if show_sign_in_benefits: 376 self._sub_height += sign_in_benefits_space 377 if show_reset_progress_button: 378 self._sub_height += reset_progress_button_space 379 if show_manage_v2_account_button: 380 self._sub_height += manage_v2_account_button_space 381 if show_player_profiles_button: 382 self._sub_height += player_profiles_button_space 383 if show_link_accounts_button: 384 self._sub_height += link_accounts_button_space 385 if show_unlink_accounts_button: 386 self._sub_height += unlink_accounts_button_space 387 if show_v2_link_info: 388 self._sub_height += v2_link_info_space 389 if self._show_legacy_unlink_button: 390 self._sub_height += legacy_unlink_button_space 391 if show_sign_out_button: 392 self._sub_height += sign_out_button_space 393 if show_cancel_sign_in_button: 394 self._sub_height += cancel_sign_in_button_space 395 self._subcontainer = ba.containerwidget( 396 parent=self._scrollwidget, 397 size=(self._sub_width, self._sub_height), 398 background=False, 399 claims_left_right=True, 400 claims_tab=True, 401 selection_loops_to_parent=True, 402 ) 403 404 first_selectable = None 405 v = self._sub_height - 10.0 406 407 self._account_name_what_is_text: ba.Widget | None 408 self._account_name_what_is_y = 0.0 409 self._account_name_text: ba.Widget | None 410 if show_signed_in_as: 411 v -= signed_in_as_space * 0.2 412 txt = ba.Lstr( 413 resource='accountSettingsWindow.youAreSignedInAsText', 414 fallback_resource='accountSettingsWindow.youAreLoggedInAsText', 415 ) 416 ba.textwidget( 417 parent=self._subcontainer, 418 position=(self._sub_width * 0.5, v), 419 size=(0, 0), 420 text=txt, 421 scale=0.9, 422 color=ba.app.ui.title_color, 423 maxwidth=self._sub_width * 0.9, 424 h_align='center', 425 v_align='center', 426 ) 427 v -= signed_in_as_space * 0.5 428 self._account_name_text = ba.textwidget( 429 parent=self._subcontainer, 430 position=(self._sub_width * 0.5, v), 431 size=(0, 0), 432 scale=1.5, 433 maxwidth=self._sub_width * 0.9, 434 res_scale=1.5, 435 color=(1, 1, 1, 1), 436 h_align='center', 437 v_align='center', 438 ) 439 440 if show_what_is_v2: 441 self._account_name_what_is_y = v - 23.0 442 self._account_name_what_is_text = ba.textwidget( 443 parent=self._subcontainer, 444 position=(0.0, self._account_name_what_is_y), 445 size=(200.0, 60), 446 text=ba.Lstr( 447 value='${WHAT} -->', 448 subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))], 449 ), 450 scale=0.6, 451 color=(0.3, 0.7, 0.05), 452 maxwidth=200.0, 453 h_align='right', 454 v_align='center', 455 autoselect=True, 456 selectable=True, 457 on_activate_call=show_what_is_v2_page, 458 click_activate=True, 459 ) 460 if first_selectable is None: 461 first_selectable = self._account_name_what_is_text 462 else: 463 self._account_name_what_is_text = None 464 465 self._refresh_account_name_text() 466 467 v -= signed_in_as_space * 0.4 468 469 else: 470 self._account_name_text = None 471 self._account_name_what_is_text = None 472 473 if self._back_button is None: 474 bbtn = ba.internal.get_special_widget('back_button') 475 else: 476 bbtn = self._back_button 477 478 if show_sign_in_benefits: 479 v -= sign_in_benefits_space 480 app = ba.app 481 extra: str | ba.Lstr | None 482 if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': 483 extra = ba.Lstr( 484 value='\n${S}', 485 subs=[ 486 ('${S}', ba.Lstr(resource='signInWithGameCenterText')) 487 ], 488 ) 489 else: 490 extra = '' 491 492 ba.textwidget( 493 parent=self._subcontainer, 494 position=( 495 self._sub_width * 0.5, 496 v + sign_in_benefits_space * 0.4, 497 ), 498 size=(0, 0), 499 text=ba.Lstr( 500 value='${A}${B}', 501 subs=[ 502 ('${A}', ba.Lstr(resource=self._r + '.signInInfoText')), 503 ('${B}', extra), 504 ], 505 ), 506 max_height=sign_in_benefits_space * 0.9, 507 scale=0.9, 508 color=(0.75, 0.7, 0.8), 509 maxwidth=self._sub_width * 0.8, 510 h_align='center', 511 v_align='center', 512 ) 513 514 if show_signing_in_text: 515 v -= signing_in_text_space 516 517 ba.textwidget( 518 parent=self._subcontainer, 519 position=( 520 self._sub_width * 0.5, 521 v + signing_in_text_space * 0.5, 522 ), 523 size=(0, 0), 524 text=ba.Lstr(resource='accountSettingsWindow.signingInText'), 525 scale=0.9, 526 color=(0, 1, 0), 527 maxwidth=self._sub_width * 0.8, 528 h_align='center', 529 v_align='center', 530 ) 531 532 if show_google_play_sign_in_button: 533 button_width = 350 534 v -= sign_in_button_space 535 self._sign_in_google_play_button = btn = ba.buttonwidget( 536 parent=self._subcontainer, 537 position=((self._sub_width - button_width) * 0.5, v - 20), 538 autoselect=True, 539 size=(button_width, 60), 540 label=ba.Lstr( 541 value='${A}${B}', 542 subs=[ 543 ( 544 '${A}', 545 ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO), 546 ), 547 ( 548 '${B}', 549 ba.Lstr( 550 resource=self._r + '.signInWithGooglePlayText' 551 ), 552 ), 553 ], 554 ), 555 on_activate_call=lambda: self._sign_in_press(LoginType.GPGS), 556 ) 557 if first_selectable is None: 558 first_selectable = btn 559 if ba.app.ui.use_toolbars: 560 ba.widget( 561 edit=btn, 562 right_widget=ba.internal.get_special_widget('party_button'), 563 ) 564 ba.widget(edit=btn, left_widget=bbtn) 565 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 566 self._sign_in_text = None 567 568 if show_v2_proxy_sign_in_button: 569 button_width = 350 570 v -= sign_in_button_space 571 self._sign_in_v2_proxy_button = btn = ba.buttonwidget( 572 parent=self._subcontainer, 573 position=((self._sub_width - button_width) * 0.5, v - 20), 574 autoselect=True, 575 size=(button_width, 60), 576 label='', 577 on_activate_call=self._v2_proxy_sign_in_press, 578 ) 579 ba.textwidget( 580 parent=self._subcontainer, 581 draw_controller=btn, 582 h_align='center', 583 v_align='center', 584 size=(0, 0), 585 position=(self._sub_width * 0.5, v + 17), 586 text=ba.Lstr( 587 value='${A}${B}', 588 subs=[ 589 ('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)), 590 ( 591 '${B}', 592 ba.Lstr(resource=self._r + '.signInWithV2Text'), 593 ), 594 ], 595 ), 596 maxwidth=button_width * 0.8, 597 color=(0.75, 1.0, 0.7), 598 ) 599 ba.textwidget( 600 parent=self._subcontainer, 601 draw_controller=btn, 602 h_align='center', 603 v_align='center', 604 size=(0, 0), 605 position=(self._sub_width * 0.5, v - 4), 606 text=ba.Lstr(resource=self._r + '.signInWithV2InfoText'), 607 flatness=1.0, 608 scale=0.57, 609 maxwidth=button_width * 0.9, 610 color=(0.55, 0.8, 0.5), 611 ) 612 if first_selectable is None: 613 first_selectable = btn 614 if ba.app.ui.use_toolbars: 615 ba.widget( 616 edit=btn, 617 right_widget=ba.internal.get_special_widget('party_button'), 618 ) 619 ba.widget(edit=btn, left_widget=bbtn) 620 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 621 self._sign_in_text = None 622 623 if show_device_sign_in_button: 624 button_width = 350 625 v -= sign_in_button_space + deprecated_space 626 self._sign_in_device_button = btn = ba.buttonwidget( 627 parent=self._subcontainer, 628 position=((self._sub_width - button_width) * 0.5, v - 20), 629 autoselect=True, 630 size=(button_width, 60), 631 label='', 632 on_activate_call=lambda: self._sign_in_press('Local'), 633 ) 634 ba.textwidget( 635 parent=self._subcontainer, 636 h_align='center', 637 v_align='center', 638 size=(0, 0), 639 position=(self._sub_width * 0.5, v + 60), 640 text=ba.Lstr(resource='deprecatedText'), 641 scale=0.8, 642 maxwidth=300, 643 color=(0.6, 0.55, 0.45), 644 ) 645 646 ba.textwidget( 647 parent=self._subcontainer, 648 draw_controller=btn, 649 h_align='center', 650 v_align='center', 651 size=(0, 0), 652 position=(self._sub_width * 0.5, v + 17), 653 text=ba.Lstr( 654 value='${A}${B}', 655 subs=[ 656 ('${A}', ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)), 657 ( 658 '${B}', 659 ba.Lstr(resource=self._r + '.signInWithDeviceText'), 660 ), 661 ], 662 ), 663 maxwidth=button_width * 0.8, 664 color=(0.75, 1.0, 0.7), 665 ) 666 ba.textwidget( 667 parent=self._subcontainer, 668 draw_controller=btn, 669 h_align='center', 670 v_align='center', 671 size=(0, 0), 672 position=(self._sub_width * 0.5, v - 4), 673 text=ba.Lstr(resource=self._r + '.signInWithDeviceInfoText'), 674 flatness=1.0, 675 scale=0.57, 676 maxwidth=button_width * 0.9, 677 color=(0.55, 0.8, 0.5), 678 ) 679 if first_selectable is None: 680 first_selectable = btn 681 if ba.app.ui.use_toolbars: 682 ba.widget( 683 edit=btn, 684 right_widget=ba.internal.get_special_widget('party_button'), 685 ) 686 ba.widget(edit=btn, left_widget=bbtn) 687 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 688 self._sign_in_text = None 689 690 if show_manage_v2_account_button: 691 button_width = 300 692 v -= manage_v2_account_button_space 693 self._manage_v2_button = btn = ba.buttonwidget( 694 parent=self._subcontainer, 695 position=((self._sub_width - button_width) * 0.5, v + 30), 696 autoselect=True, 697 size=(button_width, 60), 698 label=ba.Lstr(resource=self._r + '.manageAccountText'), 699 color=(0.55, 0.5, 0.6), 700 icon=ba.gettexture('settingsIcon'), 701 textcolor=(0.75, 0.7, 0.8), 702 on_activate_call=ba.WeakCall(self._on_manage_account_press), 703 ) 704 if first_selectable is None: 705 first_selectable = btn 706 if ba.app.ui.use_toolbars: 707 ba.widget( 708 edit=btn, 709 right_widget=ba.internal.get_special_widget('party_button'), 710 ) 711 ba.widget(edit=btn, left_widget=bbtn) 712 713 if show_player_profiles_button: 714 button_width = 300 715 v -= player_profiles_button_space 716 self._player_profiles_button = btn = ba.buttonwidget( 717 parent=self._subcontainer, 718 position=((self._sub_width - button_width) * 0.5, v + 30), 719 autoselect=True, 720 size=(button_width, 60), 721 label=ba.Lstr(resource='playerProfilesWindow.titleText'), 722 color=(0.55, 0.5, 0.6), 723 icon=ba.gettexture('cuteSpaz'), 724 textcolor=(0.75, 0.7, 0.8), 725 on_activate_call=self._player_profiles_press, 726 ) 727 if first_selectable is None: 728 first_selectable = btn 729 if ba.app.ui.use_toolbars: 730 ba.widget( 731 edit=btn, 732 right_widget=ba.internal.get_special_widget('party_button'), 733 ) 734 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) 735 736 # the button to go to OS-Specific leaderboards/high-score-lists/etc. 737 if show_game_service_button: 738 button_width = 300 739 v -= game_service_button_space * 0.85 740 v1_account_type = ba.internal.get_v1_account_type() 741 if v1_account_type == 'Game Center': 742 v1_account_type_name = ba.Lstr(resource='gameCenterText') 743 else: 744 raise ValueError( 745 "unknown account type: '" + str(v1_account_type) + "'" 746 ) 747 self._game_service_button = btn = ba.buttonwidget( 748 parent=self._subcontainer, 749 position=((self._sub_width - button_width) * 0.5, v), 750 color=(0.55, 0.5, 0.6), 751 textcolor=(0.75, 0.7, 0.8), 752 autoselect=True, 753 on_activate_call=ba.internal.show_online_score_ui, 754 size=(button_width, 50), 755 label=v1_account_type_name, 756 ) 757 if first_selectable is None: 758 first_selectable = btn 759 if ba.app.ui.use_toolbars: 760 ba.widget( 761 edit=btn, 762 right_widget=ba.internal.get_special_widget('party_button'), 763 ) 764 ba.widget(edit=btn, left_widget=bbtn) 765 v -= game_service_button_space * 0.15 766 else: 767 self.game_service_button = None 768 769 self._achievements_text: ba.Widget | None 770 if show_achievements_text: 771 v -= achievements_text_space * 0.5 772 self._achievements_text = ba.textwidget( 773 parent=self._subcontainer, 774 position=(self._sub_width * 0.5, v), 775 size=(0, 0), 776 scale=0.9, 777 color=(0.75, 0.7, 0.8), 778 maxwidth=self._sub_width * 0.8, 779 h_align='center', 780 v_align='center', 781 ) 782 v -= achievements_text_space * 0.5 783 else: 784 self._achievements_text = None 785 786 self._achievements_button: ba.Widget | None 787 if show_achievements_button: 788 button_width = 300 789 v -= achievements_button_space * 0.85 790 self._achievements_button = btn = ba.buttonwidget( 791 parent=self._subcontainer, 792 position=((self._sub_width - button_width) * 0.5, v), 793 color=(0.55, 0.5, 0.6), 794 textcolor=(0.75, 0.7, 0.8), 795 autoselect=True, 796 icon=ba.gettexture( 797 'googlePlayAchievementsIcon' 798 if is_gpgs 799 else 'achievementsIcon' 800 ), 801 icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9), 802 on_activate_call=( 803 self._on_custom_achievements_press 804 if is_gpgs 805 else self._on_achievements_press 806 ), 807 size=(button_width, 50), 808 label='', 809 ) 810 if first_selectable is None: 811 first_selectable = btn 812 if ba.app.ui.use_toolbars: 813 ba.widget( 814 edit=btn, 815 right_widget=ba.internal.get_special_widget('party_button'), 816 ) 817 ba.widget(edit=btn, left_widget=bbtn) 818 v -= achievements_button_space * 0.15 819 else: 820 self._achievements_button = None 821 822 if show_achievements_text or show_achievements_button: 823 self._refresh_achievements() 824 825 self._leaderboards_button: ba.Widget | None 826 if show_leaderboards_button: 827 button_width = 300 828 v -= leaderboards_button_space * 0.85 829 self._leaderboards_button = btn = ba.buttonwidget( 830 parent=self._subcontainer, 831 position=((self._sub_width - button_width) * 0.5, v), 832 color=(0.55, 0.5, 0.6), 833 textcolor=(0.75, 0.7, 0.8), 834 autoselect=True, 835 icon=ba.gettexture('googlePlayLeaderboardsIcon'), 836 icon_color=(0.8, 0.95, 0.7), 837 on_activate_call=self._on_leaderboards_press, 838 size=(button_width, 50), 839 label=ba.Lstr(resource='leaderboardsText'), 840 ) 841 if first_selectable is None: 842 first_selectable = btn 843 if ba.app.ui.use_toolbars: 844 ba.widget( 845 edit=btn, 846 right_widget=ba.internal.get_special_widget('party_button'), 847 ) 848 ba.widget(edit=btn, left_widget=bbtn) 849 v -= leaderboards_button_space * 0.15 850 else: 851 self._leaderboards_button = None 852 853 self._campaign_progress_text: ba.Widget | None 854 if show_campaign_progress: 855 v -= campaign_progress_space * 0.5 856 self._campaign_progress_text = ba.textwidget( 857 parent=self._subcontainer, 858 position=(self._sub_width * 0.5, v), 859 size=(0, 0), 860 scale=0.9, 861 color=(0.75, 0.7, 0.8), 862 maxwidth=self._sub_width * 0.8, 863 h_align='center', 864 v_align='center', 865 ) 866 v -= campaign_progress_space * 0.5 867 self._refresh_campaign_progress_text() 868 else: 869 self._campaign_progress_text = None 870 871 self._tickets_text: ba.Widget | None 872 if show_tickets: 873 v -= tickets_space * 0.5 874 self._tickets_text = ba.textwidget( 875 parent=self._subcontainer, 876 position=(self._sub_width * 0.5, v), 877 size=(0, 0), 878 scale=0.9, 879 color=(0.75, 0.7, 0.8), 880 maxwidth=self._sub_width * 0.8, 881 flatness=1.0, 882 h_align='center', 883 v_align='center', 884 ) 885 v -= tickets_space * 0.5 886 self._refresh_tickets_text() 887 888 else: 889 self._tickets_text = None 890 891 # bit of spacing before the reset/sign-out section 892 v -= 5 893 894 button_width = 250 895 if show_reset_progress_button: 896 confirm_text = ( 897 ba.Lstr(resource=self._r + '.resetProgressConfirmText') 898 if self._can_reset_achievements 899 else ba.Lstr( 900 resource=self._r + '.resetProgressConfirmNoAchievementsText' 901 ) 902 ) 903 v -= reset_progress_button_space 904 self._reset_progress_button = btn = ba.buttonwidget( 905 parent=self._subcontainer, 906 position=((self._sub_width - button_width) * 0.5, v), 907 color=(0.55, 0.5, 0.6), 908 textcolor=(0.75, 0.7, 0.8), 909 autoselect=True, 910 size=(button_width, 60), 911 label=ba.Lstr(resource=self._r + '.resetProgressText'), 912 on_activate_call=lambda: confirm.ConfirmWindow( 913 text=confirm_text, 914 width=500, 915 height=200, 916 action=self._reset_progress, 917 ), 918 ) 919 if first_selectable is None: 920 first_selectable = btn 921 if ba.app.ui.use_toolbars: 922 ba.widget( 923 edit=btn, 924 right_widget=ba.internal.get_special_widget('party_button'), 925 ) 926 ba.widget(edit=btn, left_widget=bbtn) 927 928 self._linked_accounts_text: ba.Widget | None 929 if show_linked_accounts_text: 930 v -= linked_accounts_text_space * 0.8 931 self._linked_accounts_text = ba.textwidget( 932 parent=self._subcontainer, 933 position=(self._sub_width * 0.5, v), 934 size=(0, 0), 935 scale=0.9, 936 color=(0.75, 0.7, 0.8), 937 maxwidth=self._sub_width * 0.95, 938 text=ba.Lstr(resource=self._r + '.linkedAccountsText'), 939 h_align='center', 940 v_align='center', 941 ) 942 v -= linked_accounts_text_space * 0.2 943 self._update_linked_accounts_text() 944 else: 945 self._linked_accounts_text = None 946 947 # Show link/unlink buttons only for V1 accounts. 948 949 if show_link_accounts_button: 950 v -= link_accounts_button_space 951 self._link_accounts_button = btn = ba.buttonwidget( 952 parent=self._subcontainer, 953 position=((self._sub_width - button_width) * 0.5, v), 954 autoselect=True, 955 size=(button_width, 60), 956 label='', 957 color=(0.55, 0.5, 0.6), 958 on_activate_call=self._link_accounts_press, 959 ) 960 ba.textwidget( 961 parent=self._subcontainer, 962 draw_controller=btn, 963 h_align='center', 964 v_align='center', 965 size=(0, 0), 966 position=(self._sub_width * 0.5, v + 17 + 20), 967 text=ba.Lstr(resource=self._r + '.linkAccountsText'), 968 maxwidth=button_width * 0.8, 969 color=(0.75, 0.7, 0.8), 970 ) 971 ba.textwidget( 972 parent=self._subcontainer, 973 draw_controller=btn, 974 h_align='center', 975 v_align='center', 976 size=(0, 0), 977 position=(self._sub_width * 0.5, v - 4 + 20), 978 text=ba.Lstr(resource=self._r + '.linkAccountsInfoText'), 979 flatness=1.0, 980 scale=0.5, 981 maxwidth=button_width * 0.8, 982 color=(0.75, 0.7, 0.8), 983 ) 984 if first_selectable is None: 985 first_selectable = btn 986 if ba.app.ui.use_toolbars: 987 ba.widget( 988 edit=btn, 989 right_widget=ba.internal.get_special_widget('party_button'), 990 ) 991 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) 992 993 self._unlink_accounts_button: ba.Widget | None 994 if show_unlink_accounts_button: 995 v -= unlink_accounts_button_space 996 self._unlink_accounts_button = btn = ba.buttonwidget( 997 parent=self._subcontainer, 998 position=((self._sub_width - button_width) * 0.5, v + 25), 999 autoselect=True, 1000 size=(button_width, 60), 1001 label='', 1002 color=(0.55, 0.5, 0.6), 1003 on_activate_call=self._unlink_accounts_press, 1004 ) 1005 self._unlink_accounts_button_label = ba.textwidget( 1006 parent=self._subcontainer, 1007 draw_controller=btn, 1008 h_align='center', 1009 v_align='center', 1010 size=(0, 0), 1011 position=(self._sub_width * 0.5, v + 55), 1012 text=ba.Lstr(resource=self._r + '.unlinkAccountsText'), 1013 maxwidth=button_width * 0.8, 1014 color=(0.75, 0.7, 0.8), 1015 ) 1016 if first_selectable is None: 1017 first_selectable = btn 1018 if ba.app.ui.use_toolbars: 1019 ba.widget( 1020 edit=btn, 1021 right_widget=ba.internal.get_special_widget('party_button'), 1022 ) 1023 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) 1024 self._update_unlink_accounts_button() 1025 else: 1026 self._unlink_accounts_button = None 1027 1028 if show_v2_link_info: 1029 v -= v2_link_info_space 1030 ba.textwidget( 1031 parent=self._subcontainer, 1032 h_align='center', 1033 v_align='center', 1034 size=(0, 0), 1035 position=(self._sub_width * 0.5, v + v2_link_info_space - 20), 1036 text=ba.Lstr(resource='v2AccountLinkingInfoText'), 1037 flatness=1.0, 1038 scale=0.8, 1039 maxwidth=450, 1040 color=(0.5, 0.45, 0.55), 1041 ) 1042 1043 if self._show_legacy_unlink_button: 1044 v -= legacy_unlink_button_space 1045 button_width_w = button_width * 1.5 1046 ba.textwidget( 1047 parent=self._subcontainer, 1048 position=(self._sub_width * 0.5 - 150.0, v + 75), 1049 size=(300.0, 60), 1050 text=ba.Lstr(resource='whatIsThisText'), 1051 scale=0.8, 1052 color=(0.3, 0.7, 0.05), 1053 maxwidth=200.0, 1054 h_align='center', 1055 v_align='center', 1056 autoselect=True, 1057 selectable=True, 1058 on_activate_call=show_what_is_legacy_unlinking_page, 1059 click_activate=True, 1060 ) 1061 btn = ba.buttonwidget( 1062 parent=self._subcontainer, 1063 position=((self._sub_width - button_width_w) * 0.5, v + 25), 1064 autoselect=True, 1065 size=(button_width_w, 60), 1066 label=ba.Lstr(resource=self._r + '.unlinkLegacyV1AccountsText'), 1067 textcolor=(0.8, 0.4, 0), 1068 color=(0.55, 0.5, 0.6), 1069 on_activate_call=self._unlink_accounts_press, 1070 ) 1071 1072 if show_sign_out_button: 1073 v -= sign_out_button_space 1074 self._sign_out_button = btn = ba.buttonwidget( 1075 parent=self._subcontainer, 1076 position=((self._sub_width - button_width) * 0.5, v), 1077 size=(button_width, 60), 1078 label=ba.Lstr(resource=self._r + '.signOutText'), 1079 color=(0.55, 0.5, 0.6), 1080 textcolor=(0.75, 0.7, 0.8), 1081 autoselect=True, 1082 on_activate_call=self._sign_out_press, 1083 ) 1084 if first_selectable is None: 1085 first_selectable = btn 1086 if ba.app.ui.use_toolbars: 1087 ba.widget( 1088 edit=btn, 1089 right_widget=ba.internal.get_special_widget('party_button'), 1090 ) 1091 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) 1092 1093 if show_cancel_sign_in_button: 1094 v -= cancel_sign_in_button_space 1095 self._cancel_sign_in_button = btn = ba.buttonwidget( 1096 parent=self._subcontainer, 1097 position=((self._sub_width - button_width) * 0.5, v), 1098 size=(button_width, 60), 1099 label=ba.Lstr(resource='cancelText'), 1100 color=(0.55, 0.5, 0.6), 1101 textcolor=(0.75, 0.7, 0.8), 1102 autoselect=True, 1103 on_activate_call=self._cancel_sign_in_press, 1104 ) 1105 if first_selectable is None: 1106 first_selectable = btn 1107 if ba.app.ui.use_toolbars: 1108 ba.widget( 1109 edit=btn, 1110 right_widget=ba.internal.get_special_widget('party_button'), 1111 ) 1112 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) 1113 1114 # Whatever the topmost selectable thing is, we want it to scroll all 1115 # the way up when we select it. 1116 if first_selectable is not None: 1117 ba.widget( 1118 edit=first_selectable, up_widget=bbtn, show_buffer_top=400 1119 ) 1120 # (this should re-scroll us to the top..) 1121 ba.containerwidget( 1122 edit=self._subcontainer, visible_child=first_selectable 1123 ) 1124 self._needs_refresh = False 1125 1126 def _on_custom_achievements_press(self) -> None: 1127 ba.timer( 1128 0.15, 1129 ba.Call(ba.internal.show_online_score_ui, 'achievements'), 1130 timetype=ba.TimeType.REAL, 1131 ) 1132 1133 def _on_achievements_press(self) -> None: 1134 # pylint: disable=cyclic-import 1135 from bastd.ui import achievements 1136 1137 assert self._achievements_button is not None 1138 achievements.AchievementsWindow( 1139 position=self._achievements_button.get_screen_space_center() 1140 ) 1141 1142 def _on_what_is_v2_press(self) -> None: 1143 show_what_is_v2_page() 1144 1145 def _on_manage_account_press(self) -> None: 1146 ba.screenmessage(ba.Lstr(resource='oneMomentText')) 1147 1148 # We expect to have a v2 account signed in if we get here. 1149 if ba.app.accounts_v2.primary is None: 1150 logging.exception( 1151 'got manage-account press without v2 account present' 1152 ) 1153 return 1154 1155 with ba.app.accounts_v2.primary: 1156 ba.app.cloud.send_message_cb( 1157 bacommon.cloud.ManageAccountMessage(), 1158 on_response=ba.WeakCall(self._on_manage_account_response), 1159 ) 1160 1161 def _on_manage_account_response( 1162 self, response: bacommon.cloud.ManageAccountResponse | Exception 1163 ) -> None: 1164 1165 if isinstance(response, Exception) or response.url is None: 1166 ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) 1167 ba.playsound(ba.getsound('error')) 1168 return 1169 1170 ba.open_url(response.url) 1171 1172 def _on_leaderboards_press(self) -> None: 1173 ba.timer( 1174 0.15, 1175 ba.Call(ba.internal.show_online_score_ui, 'leaderboards'), 1176 timetype=ba.TimeType.REAL, 1177 ) 1178 1179 def _have_unlinkable_v1_accounts(self) -> bool: 1180 # if this is not present, we haven't had contact from the server so 1181 # let's not proceed.. 1182 if ba.internal.get_public_login_id() is None: 1183 return False 1184 accounts = ba.internal.get_v1_account_misc_read_val_2( 1185 'linkedAccounts', [] 1186 ) 1187 return len(accounts) > 1 1188 1189 def _update_unlink_accounts_button(self) -> None: 1190 if self._unlink_accounts_button is None: 1191 return 1192 if self._have_unlinkable_v1_accounts(): 1193 clr = (0.75, 0.7, 0.8, 1.0) 1194 else: 1195 clr = (1.0, 1.0, 1.0, 0.25) 1196 ba.textwidget(edit=self._unlink_accounts_button_label, color=clr) 1197 1198 def _should_show_legacy_unlink_button(self) -> bool: 1199 1200 # Only show this when fully signed in to a v2 account. 1201 if not self._v1_signed_in or ba.app.accounts_v2.primary is None: 1202 return False 1203 1204 out = self._have_unlinkable_v1_accounts() 1205 return out 1206 1207 def _update_linked_accounts_text(self) -> None: 1208 if self._linked_accounts_text is None: 1209 return 1210 1211 # Disable this by default when signed in to a V2 account 1212 # (since this shows V1 links which we should no longer care about). 1213 if ( 1214 ba.app.accounts_v2.primary is not None 1215 and not FORCE_ENABLE_V1_LINKING 1216 ): 1217 return 1218 1219 # if this is not present, we haven't had contact from the server so 1220 # let's not proceed.. 1221 if ba.internal.get_public_login_id() is None: 1222 num = int(time.time()) % 4 1223 accounts_str = num * '.' + (4 - num) * ' ' 1224 else: 1225 accounts = ba.internal.get_v1_account_misc_read_val_2( 1226 'linkedAccounts', [] 1227 ) 1228 # UPDATE - we now just print the number here; not the actual 1229 # accounts (they can see that in the unlink section if they're 1230 # curious) 1231 accounts_str = str(max(0, len(accounts) - 1)) 1232 ba.textwidget( 1233 edit=self._linked_accounts_text, 1234 text=ba.Lstr( 1235 value='${L} ${A}', 1236 subs=[ 1237 ('${L}', ba.Lstr(resource=self._r + '.linkedAccountsText')), 1238 ('${A}', accounts_str), 1239 ], 1240 ), 1241 ) 1242 1243 def _refresh_campaign_progress_text(self) -> None: 1244 from ba.internal import getcampaign 1245 1246 if self._campaign_progress_text is None: 1247 return 1248 p_str: str | ba.Lstr 1249 try: 1250 campaign = getcampaign('Default') 1251 levels = campaign.levels 1252 levels_complete = sum((1 if l.complete else 0) for l in levels) 1253 1254 # Last level cant be completed; hence the -1; 1255 progress = min(1.0, float(levels_complete) / (len(levels) - 1)) 1256 p_str = ba.Lstr( 1257 resource=self._r + '.campaignProgressText', 1258 subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')], 1259 ) 1260 except Exception: 1261 p_str = '?' 1262 ba.print_exception('Error calculating co-op campaign progress.') 1263 ba.textwidget(edit=self._campaign_progress_text, text=p_str) 1264 1265 def _refresh_tickets_text(self) -> None: 1266 if self._tickets_text is None: 1267 return 1268 try: 1269 tc_str = str(ba.internal.get_v1_account_ticket_count()) 1270 except Exception: 1271 ba.print_exception() 1272 tc_str = '-' 1273 ba.textwidget( 1274 edit=self._tickets_text, 1275 text=ba.Lstr( 1276 resource=self._r + '.ticketsText', subs=[('${COUNT}', tc_str)] 1277 ), 1278 ) 1279 1280 def _refresh_account_name_text(self) -> None: 1281 1282 if self._account_name_text is None: 1283 return 1284 try: 1285 name_str = ba.internal.get_v1_account_display_string() 1286 except Exception: 1287 ba.print_exception() 1288 name_str = '??' 1289 1290 ba.textwidget(edit=self._account_name_text, text=name_str) 1291 if self._account_name_what_is_text is not None: 1292 swidth = ba.internal.get_string_width( 1293 name_str, suppress_warning=True 1294 ) 1295 # Eww; number-fudging. Need to recalibrate this if 1296 # account name scaling changes. 1297 x = self._sub_width * 0.5 - swidth * 0.75 - 170 1298 1299 ba.textwidget( 1300 edit=self._account_name_what_is_text, 1301 position=(x, self._account_name_what_is_y), 1302 ) 1303 1304 def _refresh_achievements(self) -> None: 1305 if ( 1306 self._achievements_text is None 1307 and self._achievements_button is None 1308 ): 1309 return 1310 complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) 1311 total = len(ba.app.ach.achievements) 1312 txt_final = ba.Lstr( 1313 resource=self._r + '.achievementProgressText', 1314 subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))], 1315 ) 1316 1317 if self._achievements_text is not None: 1318 ba.textwidget(edit=self._achievements_text, text=txt_final) 1319 if self._achievements_button is not None: 1320 ba.buttonwidget(edit=self._achievements_button, label=txt_final) 1321 1322 def _link_accounts_press(self) -> None: 1323 # pylint: disable=cyclic-import 1324 from bastd.ui.account import link 1325 1326 link.AccountLinkWindow(origin_widget=self._link_accounts_button) 1327 1328 def _unlink_accounts_press(self) -> None: 1329 # pylint: disable=cyclic-import 1330 from bastd.ui.account import unlink 1331 1332 if not self._have_unlinkable_v1_accounts(): 1333 ba.playsound(ba.getsound('error')) 1334 return 1335 unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) 1336 1337 def _player_profiles_press(self) -> None: 1338 # pylint: disable=cyclic-import 1339 from bastd.ui.profile import browser as pbrowser 1340 1341 self._save_state() 1342 ba.containerwidget(edit=self._root_widget, transition='out_left') 1343 pbrowser.ProfileBrowserWindow( 1344 origin_widget=self._player_profiles_button 1345 ) 1346 1347 def _cancel_sign_in_press(self) -> None: 1348 1349 # If we're waiting on an adapter to give us credentials, abort. 1350 self._signing_in_adapter = None 1351 1352 # Say we don't wanna be signed in anymore if we are. 1353 ba.app.accounts_v2.set_primary_credentials(None) 1354 1355 self._needs_refresh = True 1356 1357 # Speed UI updates along. 1358 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1359 1360 def _sign_out_press(self) -> None: 1361 1362 if ba.app.accounts_v2.have_primary_credentials(): 1363 if ( 1364 ba.app.accounts_v2.primary is not None 1365 and LoginType.GPGS in ba.app.accounts_v2.primary.logins 1366 ): 1367 self._explicitly_signed_out_of_gpgs = True 1368 ba.app.accounts_v2.set_primary_credentials(None) 1369 else: 1370 ba.internal.sign_out_v1() 1371 1372 cfg = ba.app.config 1373 1374 # Also take note that its our *explicit* intention to not be 1375 # signed in at this point (affects v1 accounts). 1376 cfg['Auto Account State'] = 'signed_out' 1377 cfg.commit() 1378 ba.buttonwidget( 1379 edit=self._sign_out_button, 1380 label=ba.Lstr(resource=self._r + '.signingOutText'), 1381 ) 1382 1383 # Speed UI updates along. 1384 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1385 1386 def _sign_in_press(self, login_type: str | LoginType) -> None: 1387 1388 # V1 login types are strings. 1389 if isinstance(login_type, str): 1390 ba.internal.sign_in_v1(login_type) 1391 1392 # Make note of the type account we're *wanting* 1393 # to be signed in with. 1394 cfg = ba.app.config 1395 cfg['Auto Account State'] = login_type 1396 cfg.commit() 1397 self._needs_refresh = True 1398 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1399 return 1400 1401 # V2 login sign-in buttons generally go through adapters. 1402 adapter = ba.app.accounts_v2.login_adapters.get(login_type) 1403 if adapter is not None: 1404 self._signing_in_adapter = adapter 1405 adapter.sign_in( 1406 result_cb=ba.WeakCall(self._on_adapter_sign_in_result), 1407 description='account settings button', 1408 ) 1409 # Will get 'Signing in...' to show. 1410 self._needs_refresh = True 1411 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1412 else: 1413 ba.screenmessage(f'Unsupported login_type: {login_type.name}') 1414 1415 def _on_adapter_sign_in_result( 1416 self, 1417 adapter: LoginAdapter, 1418 result: LoginAdapter.SignInResult | Exception, 1419 ) -> None: 1420 is_us = self._signing_in_adapter is adapter 1421 1422 # If this isn't our current one we don't care. 1423 if not is_us: 1424 return 1425 1426 # If it is us, note that we're done. 1427 self._signing_in_adapter = None 1428 1429 if isinstance(result, Exception): 1430 # For now just make a bit of noise if anything went wrong; 1431 # can get more specific as needed later. 1432 ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) 1433 ba.playsound(ba.getsound('error')) 1434 else: 1435 # Success! Plug in these credentials which will begin 1436 # verifying them and set our primary account-handle 1437 # when finished. 1438 ba.app.accounts_v2.set_primary_credentials(result.credentials) 1439 1440 # Special case - if the user has explicitly logged out and 1441 # logged in again with GPGS via this button, warn them that 1442 # they need to use the app if they want to switch to a 1443 # different GPGS account. 1444 if ( 1445 self._explicitly_signed_out_of_gpgs 1446 and adapter.login_type is LoginType.GPGS 1447 ): 1448 # Delay this slightly so it hopefully pops up after 1449 # credentials go through and the account name shows up. 1450 ba.timer( 1451 1.5, 1452 ba.Call( 1453 ba.screenmessage, 1454 ba.Lstr( 1455 resource=self._r 1456 + '.googlePlayGamesAccountSwitchText' 1457 ), 1458 ), 1459 ) 1460 1461 # Speed any UI updates along. 1462 self._needs_refresh = True 1463 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1464 1465 def _v2_proxy_sign_in_press(self) -> None: 1466 # pylint: disable=cyclic-import 1467 from bastd.ui.account.v2proxy import V2ProxySignInWindow 1468 1469 assert self._sign_in_v2_proxy_button is not None 1470 V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button) 1471 1472 def _reset_progress(self) -> None: 1473 try: 1474 from ba.internal import getcampaign 1475 1476 # FIXME: This would need to happen server-side these days. 1477 if self._can_reset_achievements: 1478 ba.app.config['Achievements'] = {} 1479 ba.internal.reset_achievements() 1480 campaign = getcampaign('Default') 1481 campaign.reset() # also writes the config.. 1482 campaign = getcampaign('Challenges') 1483 campaign.reset() # also writes the config.. 1484 except Exception: 1485 ba.print_exception('Error resetting co-op campaign progress.') 1486 1487 ba.playsound(ba.getsound('shieldDown')) 1488 self._refresh() 1489 1490 def _back(self) -> None: 1491 # pylint: disable=cyclic-import 1492 from bastd.ui.mainmenu import MainMenuWindow 1493 1494 self._save_state() 1495 ba.containerwidget( 1496 edit=self._root_widget, transition=self._transition_out 1497 ) 1498 1499 if not self._modal: 1500 ba.app.ui.set_main_menu_window( 1501 MainMenuWindow(transition='in_left').get_root_widget() 1502 ) 1503 1504 def _save_state(self) -> None: 1505 try: 1506 sel = self._root_widget.get_selected_child() 1507 if sel == self._back_button: 1508 sel_name = 'Back' 1509 elif sel == self._scrollwidget: 1510 sel_name = 'Scroll' 1511 else: 1512 raise ValueError('unrecognized selection') 1513 ba.app.ui.window_states[type(self)] = sel_name 1514 except Exception: 1515 ba.print_exception(f'Error saving state for {self}.') 1516 1517 def _restore_state(self) -> None: 1518 try: 1519 sel_name = ba.app.ui.window_states.get(type(self)) 1520 if sel_name == 'Back': 1521 sel = self._back_button 1522 elif sel_name == 'Scroll': 1523 sel = self._scrollwidget 1524 else: 1525 sel = self._back_button 1526 ba.containerwidget(edit=self._root_widget, selected_child=sel) 1527 except Exception: 1528 ba.print_exception(f'Error restoring state for {self}.') 1529 1530 1531def show_what_is_v2_page() -> None: 1532 """Show the webpage describing V2 accounts.""" 1533 bamasteraddr = ba.internal.get_master_server_address(version=2) 1534 ba.open_url(f'{bamasteraddr}/whatisv2') 1535 1536 1537def show_what_is_legacy_unlinking_page() -> None: 1538 """Show the webpage describing legacy unlinking.""" 1539 bamasteraddr = ba.internal.get_master_server_address(version=2) 1540 ba.open_url(f'{bamasteraddr}/whatarev1links')
class
AccountSettingsWindow(ba.ui.Window):
27class AccountSettingsWindow(ba.Window): 28 """Window for account related functionality.""" 29 30 def __init__( 31 self, 32 transition: str = 'in_right', 33 modal: bool = False, 34 origin_widget: ba.Widget | None = None, 35 close_once_signed_in: bool = False, 36 ): 37 # pylint: disable=too-many-statements 38 39 self._sign_in_v2_proxy_button: ba.Widget | None = None 40 self._sign_in_device_button: ba.Widget | None = None 41 42 self._show_legacy_unlink_button = False 43 44 self._signing_in_adapter: LoginAdapter | None = None 45 self._close_once_signed_in = close_once_signed_in 46 ba.set_analytics_screen('Account Window') 47 48 self._explicitly_signed_out_of_gpgs = False 49 50 # If they provided an origin-widget, scale up from that. 51 scale_origin: tuple[float, float] | None 52 if origin_widget is not None: 53 self._transition_out = 'out_scale' 54 scale_origin = origin_widget.get_screen_space_center() 55 transition = 'in_scale' 56 else: 57 self._transition_out = 'out_right' 58 scale_origin = None 59 60 self._r = 'accountSettingsWindow' 61 self._modal = modal 62 self._needs_refresh = False 63 self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in' 64 self._v1_account_state_num = ba.internal.get_v1_account_state_num() 65 self._check_sign_in_timer = ba.Timer( 66 1.0, 67 ba.WeakCall(self._update), 68 timetype=ba.TimeType.REAL, 69 repeat=True, 70 ) 71 72 # Currently we can only reset achievements on game-center. 73 v1_account_type: str | None 74 if self._v1_signed_in: 75 v1_account_type = ba.internal.get_v1_account_type() 76 else: 77 v1_account_type = None 78 self._can_reset_achievements = v1_account_type == 'Game Center' 79 80 app = ba.app 81 uiscale = app.ui.uiscale 82 83 self._width = 760 if uiscale is ba.UIScale.SMALL else 660 84 x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 85 self._height = ( 86 390 87 if uiscale is ba.UIScale.SMALL 88 else 430 89 if uiscale is ba.UIScale.MEDIUM 90 else 490 91 ) 92 93 self._sign_in_button = None 94 self._sign_in_text = None 95 96 self._scroll_width = self._width - (100 + x_offs * 2) 97 self._scroll_height = self._height - 120 98 self._sub_width = self._scroll_width - 20 99 100 # Determine which sign-in/sign-out buttons we should show. 101 self._show_sign_in_buttons: list[str] = [] 102 103 if LoginType.GPGS in ba.app.accounts_v2.login_adapters: 104 self._show_sign_in_buttons.append('Google Play') 105 106 # Always want to show our web-based v2 login option. 107 self._show_sign_in_buttons.append('V2Proxy') 108 109 # Legacy v1 device accounts are currently always available 110 # (though we need to start phasing them out at some point). 111 self._show_sign_in_buttons.append('Device') 112 113 top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 114 super().__init__( 115 root_widget=ba.containerwidget( 116 size=(self._width, self._height + top_extra), 117 transition=transition, 118 toolbar_visibility='menu_minimal', 119 scale_origin_stack_offset=scale_origin, 120 scale=( 121 2.09 122 if uiscale is ba.UIScale.SMALL 123 else 1.4 124 if uiscale is ba.UIScale.MEDIUM 125 else 1.0 126 ), 127 stack_offset=(0, -19) 128 if uiscale is ba.UIScale.SMALL 129 else (0, 0), 130 ) 131 ) 132 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 133 self._back_button = None 134 ba.containerwidget( 135 edit=self._root_widget, on_cancel_call=self._back 136 ) 137 else: 138 self._back_button = btn = ba.buttonwidget( 139 parent=self._root_widget, 140 position=(51 + x_offs, self._height - 62), 141 size=(120, 60), 142 scale=0.8, 143 text_scale=1.2, 144 autoselect=True, 145 label=ba.Lstr( 146 resource='doneText' if self._modal else 'backText' 147 ), 148 button_type='regular' if self._modal else 'back', 149 on_activate_call=self._back, 150 ) 151 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 152 if not self._modal: 153 ba.buttonwidget( 154 edit=btn, 155 button_type='backSmall', 156 size=(60, 56), 157 label=ba.charstr(ba.SpecialChar.BACK), 158 ) 159 160 ba.textwidget( 161 parent=self._root_widget, 162 position=(self._width * 0.5, self._height - 41), 163 size=(0, 0), 164 text=ba.Lstr(resource=self._r + '.titleText'), 165 color=ba.app.ui.title_color, 166 maxwidth=self._width - 340, 167 h_align='center', 168 v_align='center', 169 ) 170 171 self._scrollwidget = ba.scrollwidget( 172 parent=self._root_widget, 173 highlight=False, 174 position=( 175 (self._width - self._scroll_width) * 0.5, 176 self._height - 65 - self._scroll_height, 177 ), 178 size=(self._scroll_width, self._scroll_height), 179 claims_left_right=True, 180 claims_tab=True, 181 selection_loops_to_parent=True, 182 ) 183 self._subcontainer: ba.Widget | None = None 184 self._refresh() 185 self._restore_state() 186 187 def _update(self) -> None: 188 189 # If they want us to close once we're signed in, do so. 190 if self._close_once_signed_in and self._v1_signed_in: 191 self._back() 192 return 193 194 # Hmm should update this to use get_account_state_num. 195 # Theoretically if we switch from one signed-in account to another 196 # in the background this would break. 197 v1_account_state_num = ba.internal.get_v1_account_state_num() 198 v1_account_state = ba.internal.get_v1_account_state() 199 show_legacy_unlink_button = self._should_show_legacy_unlink_button() 200 201 if ( 202 v1_account_state_num != self._v1_account_state_num 203 or show_legacy_unlink_button != self._show_legacy_unlink_button 204 or self._needs_refresh 205 ): 206 self._v1_account_state_num = v1_account_state_num 207 self._v1_signed_in = v1_account_state == 'signed_in' 208 self._show_legacy_unlink_button = show_legacy_unlink_button 209 self._refresh() 210 211 # Go ahead and refresh some individual things 212 # that may change under us. 213 self._update_linked_accounts_text() 214 self._update_unlink_accounts_button() 215 self._refresh_campaign_progress_text() 216 self._refresh_achievements() 217 self._refresh_tickets_text() 218 self._refresh_account_name_text() 219 220 def _get_sign_in_text(self) -> ba.Lstr: 221 return ba.Lstr(resource=self._r + '.signInText') 222 223 def _refresh(self) -> None: 224 # pylint: disable=too-many-statements 225 # pylint: disable=too-many-branches 226 # pylint: disable=too-many-locals 227 # pylint: disable=cyclic-import 228 from bastd.ui import confirm 229 230 primary_v2_account = ba.app.accounts_v2.primary 231 232 v1_state = ba.internal.get_v1_account_state() 233 v1_account_type = ( 234 ba.internal.get_v1_account_type() 235 if v1_state == 'signed_in' 236 else 'unknown' 237 ) 238 239 # We expose GPGS-specific functionality only if it is 'active' 240 # (meaning the current GPGS player matches one of our account's 241 # logins). 242 gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS) 243 is_gpgs = ( 244 False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() 245 ) 246 247 show_signed_in_as = self._v1_signed_in 248 signed_in_as_space = 95.0 249 250 show_sign_in_benefits = not self._v1_signed_in 251 sign_in_benefits_space = 80.0 252 253 show_signing_in_text = ( 254 v1_state == 'signing_in' or self._signing_in_adapter is not None 255 ) 256 signing_in_text_space = 80.0 257 258 show_google_play_sign_in_button = ( 259 v1_state == 'signed_out' 260 and self._signing_in_adapter is None 261 and 'Google Play' in self._show_sign_in_buttons 262 ) 263 show_v2_proxy_sign_in_button = ( 264 v1_state == 'signed_out' 265 and self._signing_in_adapter is None 266 and 'V2Proxy' in self._show_sign_in_buttons 267 ) 268 show_device_sign_in_button = ( 269 v1_state == 'signed_out' 270 and self._signing_in_adapter is None 271 and 'Device' in self._show_sign_in_buttons 272 ) 273 sign_in_button_space = 70.0 274 deprecated_space = 60 275 276 show_game_service_button = self._v1_signed_in and v1_account_type in [ 277 'Game Center' 278 ] 279 game_service_button_space = 60.0 280 281 show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2' 282 283 show_linked_accounts_text = self._v1_signed_in 284 linked_accounts_text_space = 60.0 285 286 show_achievements_button = self._v1_signed_in and v1_account_type in ( 287 'Google Play', 288 'Local', 289 'V2', 290 ) 291 achievements_button_space = 60.0 292 293 show_achievements_text = ( 294 self._v1_signed_in and not show_achievements_button 295 ) 296 achievements_text_space = 27.0 297 298 show_leaderboards_button = self._v1_signed_in and is_gpgs 299 leaderboards_button_space = 60.0 300 301 show_campaign_progress = self._v1_signed_in 302 campaign_progress_space = 27.0 303 304 show_tickets = self._v1_signed_in 305 tickets_space = 27.0 306 307 show_reset_progress_button = False 308 reset_progress_button_space = 70.0 309 310 show_manage_v2_account_button = ( 311 self._v1_signed_in and v1_account_type == 'V2' 312 ) 313 manage_v2_account_button_space = 100.0 314 315 show_player_profiles_button = self._v1_signed_in 316 player_profiles_button_space = ( 317 70.0 if show_manage_v2_account_button else 100.0 318 ) 319 320 show_link_accounts_button = self._v1_signed_in and ( 321 primary_v2_account is None or FORCE_ENABLE_V1_LINKING 322 ) 323 link_accounts_button_space = 70.0 324 325 show_unlink_accounts_button = show_link_accounts_button 326 unlink_accounts_button_space = 90.0 327 328 show_v2_link_info = self._v1_signed_in and not show_link_accounts_button 329 v2_link_info_space = 70.0 330 331 legacy_unlink_button_space = 120.0 332 333 show_sign_out_button = self._v1_signed_in and v1_account_type in [ 334 'Local', 335 'Google Play', 336 'V2', 337 ] 338 sign_out_button_space = 70.0 339 340 # We can show cancel if we're either waiting on an adapter to 341 # provide us with v2 credentials or waiting for those credentials 342 # to be verified. 343 show_cancel_sign_in_button = self._signing_in_adapter is not None or ( 344 ba.app.accounts_v2.have_primary_credentials() 345 and primary_v2_account is None 346 ) 347 cancel_sign_in_button_space = 70.0 348 349 if self._subcontainer is not None: 350 self._subcontainer.delete() 351 self._sub_height = 60.0 352 if show_signed_in_as: 353 self._sub_height += signed_in_as_space 354 if show_signing_in_text: 355 self._sub_height += signing_in_text_space 356 if show_google_play_sign_in_button: 357 self._sub_height += sign_in_button_space 358 if show_v2_proxy_sign_in_button: 359 self._sub_height += sign_in_button_space 360 if show_device_sign_in_button: 361 self._sub_height += sign_in_button_space + deprecated_space 362 if show_game_service_button: 363 self._sub_height += game_service_button_space 364 if show_linked_accounts_text: 365 self._sub_height += linked_accounts_text_space 366 if show_achievements_text: 367 self._sub_height += achievements_text_space 368 if show_achievements_button: 369 self._sub_height += achievements_button_space 370 if show_leaderboards_button: 371 self._sub_height += leaderboards_button_space 372 if show_campaign_progress: 373 self._sub_height += campaign_progress_space 374 if show_tickets: 375 self._sub_height += tickets_space 376 if show_sign_in_benefits: 377 self._sub_height += sign_in_benefits_space 378 if show_reset_progress_button: 379 self._sub_height += reset_progress_button_space 380 if show_manage_v2_account_button: 381 self._sub_height += manage_v2_account_button_space 382 if show_player_profiles_button: 383 self._sub_height += player_profiles_button_space 384 if show_link_accounts_button: 385 self._sub_height += link_accounts_button_space 386 if show_unlink_accounts_button: 387 self._sub_height += unlink_accounts_button_space 388 if show_v2_link_info: 389 self._sub_height += v2_link_info_space 390 if self._show_legacy_unlink_button: 391 self._sub_height += legacy_unlink_button_space 392 if show_sign_out_button: 393 self._sub_height += sign_out_button_space 394 if show_cancel_sign_in_button: 395 self._sub_height += cancel_sign_in_button_space 396 self._subcontainer = ba.containerwidget( 397 parent=self._scrollwidget, 398 size=(self._sub_width, self._sub_height), 399 background=False, 400 claims_left_right=True, 401 claims_tab=True, 402 selection_loops_to_parent=True, 403 ) 404 405 first_selectable = None 406 v = self._sub_height - 10.0 407 408 self._account_name_what_is_text: ba.Widget | None 409 self._account_name_what_is_y = 0.0 410 self._account_name_text: ba.Widget | None 411 if show_signed_in_as: 412 v -= signed_in_as_space * 0.2 413 txt = ba.Lstr( 414 resource='accountSettingsWindow.youAreSignedInAsText', 415 fallback_resource='accountSettingsWindow.youAreLoggedInAsText', 416 ) 417 ba.textwidget( 418 parent=self._subcontainer, 419 position=(self._sub_width * 0.5, v), 420 size=(0, 0), 421 text=txt, 422 scale=0.9, 423 color=ba.app.ui.title_color, 424 maxwidth=self._sub_width * 0.9, 425 h_align='center', 426 v_align='center', 427 ) 428 v -= signed_in_as_space * 0.5 429 self._account_name_text = ba.textwidget( 430 parent=self._subcontainer, 431 position=(self._sub_width * 0.5, v), 432 size=(0, 0), 433 scale=1.5, 434 maxwidth=self._sub_width * 0.9, 435 res_scale=1.5, 436 color=(1, 1, 1, 1), 437 h_align='center', 438 v_align='center', 439 ) 440 441 if show_what_is_v2: 442 self._account_name_what_is_y = v - 23.0 443 self._account_name_what_is_text = ba.textwidget( 444 parent=self._subcontainer, 445 position=(0.0, self._account_name_what_is_y), 446 size=(200.0, 60), 447 text=ba.Lstr( 448 value='${WHAT} -->', 449 subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))], 450 ), 451 scale=0.6, 452 color=(0.3, 0.7, 0.05), 453 maxwidth=200.0, 454 h_align='right', 455 v_align='center', 456 autoselect=True, 457 selectable=True, 458 on_activate_call=show_what_is_v2_page, 459 click_activate=True, 460 ) 461 if first_selectable is None: 462 first_selectable = self._account_name_what_is_text 463 else: 464 self._account_name_what_is_text = None 465 466 self._refresh_account_name_text() 467 468 v -= signed_in_as_space * 0.4 469 470 else: 471 self._account_name_text = None 472 self._account_name_what_is_text = None 473 474 if self._back_button is None: 475 bbtn = ba.internal.get_special_widget('back_button') 476 else: 477 bbtn = self._back_button 478 479 if show_sign_in_benefits: 480 v -= sign_in_benefits_space 481 app = ba.app 482 extra: str | ba.Lstr | None 483 if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': 484 extra = ba.Lstr( 485 value='\n${S}', 486 subs=[ 487 ('${S}', ba.Lstr(resource='signInWithGameCenterText')) 488 ], 489 ) 490 else: 491 extra = '' 492 493 ba.textwidget( 494 parent=self._subcontainer, 495 position=( 496 self._sub_width * 0.5, 497 v + sign_in_benefits_space * 0.4, 498 ), 499 size=(0, 0), 500 text=ba.Lstr( 501 value='${A}${B}', 502 subs=[ 503 ('${A}', ba.Lstr(resource=self._r + '.signInInfoText')), 504 ('${B}', extra), 505 ], 506 ), 507 max_height=sign_in_benefits_space * 0.9, 508 scale=0.9, 509 color=(0.75, 0.7, 0.8), 510 maxwidth=self._sub_width * 0.8, 511 h_align='center', 512 v_align='center', 513 ) 514 515 if show_signing_in_text: 516 v -= signing_in_text_space 517 518 ba.textwidget( 519 parent=self._subcontainer, 520 position=( 521 self._sub_width * 0.5, 522 v + signing_in_text_space * 0.5, 523 ), 524 size=(0, 0), 525 text=ba.Lstr(resource='accountSettingsWindow.signingInText'), 526 scale=0.9, 527 color=(0, 1, 0), 528 maxwidth=self._sub_width * 0.8, 529 h_align='center', 530 v_align='center', 531 ) 532 533 if show_google_play_sign_in_button: 534 button_width = 350 535 v -= sign_in_button_space 536 self._sign_in_google_play_button = btn = ba.buttonwidget( 537 parent=self._subcontainer, 538 position=((self._sub_width - button_width) * 0.5, v - 20), 539 autoselect=True, 540 size=(button_width, 60), 541 label=ba.Lstr( 542 value='${A}${B}', 543 subs=[ 544 ( 545 '${A}', 546 ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO), 547 ), 548 ( 549 '${B}', 550 ba.Lstr( 551 resource=self._r + '.signInWithGooglePlayText' 552 ), 553 ), 554 ], 555 ), 556 on_activate_call=lambda: self._sign_in_press(LoginType.GPGS), 557 ) 558 if first_selectable is None: 559 first_selectable = btn 560 if ba.app.ui.use_toolbars: 561 ba.widget( 562 edit=btn, 563 right_widget=ba.internal.get_special_widget('party_button'), 564 ) 565 ba.widget(edit=btn, left_widget=bbtn) 566 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 567 self._sign_in_text = None 568 569 if show_v2_proxy_sign_in_button: 570 button_width = 350 571 v -= sign_in_button_space 572 self._sign_in_v2_proxy_button = btn = ba.buttonwidget( 573 parent=self._subcontainer, 574 position=((self._sub_width - button_width) * 0.5, v - 20), 575 autoselect=True, 576 size=(button_width, 60), 577 label='', 578 on_activate_call=self._v2_proxy_sign_in_press, 579 ) 580 ba.textwidget( 581 parent=self._subcontainer, 582 draw_controller=btn, 583 h_align='center', 584 v_align='center', 585 size=(0, 0), 586 position=(self._sub_width * 0.5, v + 17), 587 text=ba.Lstr( 588 value='${A}${B}', 589 subs=[ 590 ('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)), 591 ( 592 '${B}', 593 ba.Lstr(resource=self._r + '.signInWithV2Text'), 594 ), 595 ], 596 ), 597 maxwidth=button_width * 0.8, 598 color=(0.75, 1.0, 0.7), 599 ) 600 ba.textwidget( 601 parent=self._subcontainer, 602 draw_controller=btn, 603 h_align='center', 604 v_align='center', 605 size=(0, 0), 606 position=(self._sub_width * 0.5, v - 4), 607 text=ba.Lstr(resource=self._r + '.signInWithV2InfoText'), 608 flatness=1.0, 609 scale=0.57, 610 maxwidth=button_width * 0.9, 611 color=(0.55, 0.8, 0.5), 612 ) 613 if first_selectable is None: 614 first_selectable = btn 615 if ba.app.ui.use_toolbars: 616 ba.widget( 617 edit=btn, 618 right_widget=ba.internal.get_special_widget('party_button'), 619 ) 620 ba.widget(edit=btn, left_widget=bbtn) 621 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 622 self._sign_in_text = None 623 624 if show_device_sign_in_button: 625 button_width = 350 626 v -= sign_in_button_space + deprecated_space 627 self._sign_in_device_button = btn = ba.buttonwidget( 628 parent=self._subcontainer, 629 position=((self._sub_width - button_width) * 0.5, v - 20), 630 autoselect=True, 631 size=(button_width, 60), 632 label='', 633 on_activate_call=lambda: self._sign_in_press('Local'), 634 ) 635 ba.textwidget( 636 parent=self._subcontainer, 637 h_align='center', 638 v_align='center', 639 size=(0, 0), 640 position=(self._sub_width * 0.5, v + 60), 641 text=ba.Lstr(resource='deprecatedText'), 642 scale=0.8, 643 maxwidth=300, 644 color=(0.6, 0.55, 0.45), 645 ) 646 647 ba.textwidget( 648 parent=self._subcontainer, 649 draw_controller=btn, 650 h_align='center', 651 v_align='center', 652 size=(0, 0), 653 position=(self._sub_width * 0.5, v + 17), 654 text=ba.Lstr( 655 value='${A}${B}', 656 subs=[ 657 ('${A}', ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)), 658 ( 659 '${B}', 660 ba.Lstr(resource=self._r + '.signInWithDeviceText'), 661 ), 662 ], 663 ), 664 maxwidth=button_width * 0.8, 665 color=(0.75, 1.0, 0.7), 666 ) 667 ba.textwidget( 668 parent=self._subcontainer, 669 draw_controller=btn, 670 h_align='center', 671 v_align='center', 672 size=(0, 0), 673 position=(self._sub_width * 0.5, v - 4), 674 text=ba.Lstr(resource=self._r + '.signInWithDeviceInfoText'), 675 flatness=1.0, 676 scale=0.57, 677 maxwidth=button_width * 0.9, 678 color=(0.55, 0.8, 0.5), 679 ) 680 if first_selectable is None: 681 first_selectable = btn 682 if ba.app.ui.use_toolbars: 683 ba.widget( 684 edit=btn, 685 right_widget=ba.internal.get_special_widget('party_button'), 686 ) 687 ba.widget(edit=btn, left_widget=bbtn) 688 ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) 689 self._sign_in_text = None 690 691 if show_manage_v2_account_button: 692 button_width = 300 693 v -= manage_v2_account_button_space 694 self._manage_v2_button = btn = ba.buttonwidget( 695 parent=self._subcontainer, 696 position=((self._sub_width - button_width) * 0.5, v + 30), 697 autoselect=True, 698 size=(button_width, 60), 699 label=ba.Lstr(resource=self._r + '.manageAccountText'), 700 color=(0.55, 0.5, 0.6), 701 icon=ba.gettexture('settingsIcon'), 702 textcolor=(0.75, 0.7, 0.8), 703 on_activate_call=ba.WeakCall(self._on_manage_account_press), 704 ) 705 if first_selectable is None: 706 first_selectable = btn 707 if ba.app.ui.use_toolbars: 708 ba.widget( 709 edit=btn, 710 right_widget=ba.internal.get_special_widget('party_button'), 711 ) 712 ba.widget(edit=btn, left_widget=bbtn) 713 714 if show_player_profiles_button: 715 button_width = 300 716 v -= player_profiles_button_space 717 self._player_profiles_button = btn = ba.buttonwidget( 718 parent=self._subcontainer, 719 position=((self._sub_width - button_width) * 0.5, v + 30), 720 autoselect=True, 721 size=(button_width, 60), 722 label=ba.Lstr(resource='playerProfilesWindow.titleText'), 723 color=(0.55, 0.5, 0.6), 724 icon=ba.gettexture('cuteSpaz'), 725 textcolor=(0.75, 0.7, 0.8), 726 on_activate_call=self._player_profiles_press, 727 ) 728 if first_selectable is None: 729 first_selectable = btn 730 if ba.app.ui.use_toolbars: 731 ba.widget( 732 edit=btn, 733 right_widget=ba.internal.get_special_widget('party_button'), 734 ) 735 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) 736 737 # the button to go to OS-Specific leaderboards/high-score-lists/etc. 738 if show_game_service_button: 739 button_width = 300 740 v -= game_service_button_space * 0.85 741 v1_account_type = ba.internal.get_v1_account_type() 742 if v1_account_type == 'Game Center': 743 v1_account_type_name = ba.Lstr(resource='gameCenterText') 744 else: 745 raise ValueError( 746 "unknown account type: '" + str(v1_account_type) + "'" 747 ) 748 self._game_service_button = btn = ba.buttonwidget( 749 parent=self._subcontainer, 750 position=((self._sub_width - button_width) * 0.5, v), 751 color=(0.55, 0.5, 0.6), 752 textcolor=(0.75, 0.7, 0.8), 753 autoselect=True, 754 on_activate_call=ba.internal.show_online_score_ui, 755 size=(button_width, 50), 756 label=v1_account_type_name, 757 ) 758 if first_selectable is None: 759 first_selectable = btn 760 if ba.app.ui.use_toolbars: 761 ba.widget( 762 edit=btn, 763 right_widget=ba.internal.get_special_widget('party_button'), 764 ) 765 ba.widget(edit=btn, left_widget=bbtn) 766 v -= game_service_button_space * 0.15 767 else: 768 self.game_service_button = None 769 770 self._achievements_text: ba.Widget | None 771 if show_achievements_text: 772 v -= achievements_text_space * 0.5 773 self._achievements_text = ba.textwidget( 774 parent=self._subcontainer, 775 position=(self._sub_width * 0.5, v), 776 size=(0, 0), 777 scale=0.9, 778 color=(0.75, 0.7, 0.8), 779 maxwidth=self._sub_width * 0.8, 780 h_align='center', 781 v_align='center', 782 ) 783 v -= achievements_text_space * 0.5 784 else: 785 self._achievements_text = None 786 787 self._achievements_button: ba.Widget | None 788 if show_achievements_button: 789 button_width = 300 790 v -= achievements_button_space * 0.85 791 self._achievements_button = btn = ba.buttonwidget( 792 parent=self._subcontainer, 793 position=((self._sub_width - button_width) * 0.5, v), 794 color=(0.55, 0.5, 0.6), 795 textcolor=(0.75, 0.7, 0.8), 796 autoselect=True, 797 icon=ba.gettexture( 798 'googlePlayAchievementsIcon' 799 if is_gpgs 800 else 'achievementsIcon' 801 ), 802 icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9), 803 on_activate_call=( 804 self._on_custom_achievements_press 805 if is_gpgs 806 else self._on_achievements_press 807 ), 808 size=(button_width, 50), 809 label='', 810 ) 811 if first_selectable is None: 812 first_selectable = btn 813 if ba.app.ui.use_toolbars: 814 ba.widget( 815 edit=btn, 816 right_widget=ba.internal.get_special_widget('party_button'), 817 ) 818 ba.widget(edit=btn, left_widget=bbtn) 819 v -= achievements_button_space * 0.15 820 else: 821 self._achievements_button = None 822 823 if show_achievements_text or show_achievements_button: 824 self._refresh_achievements() 825 826 self._leaderboards_button: ba.Widget | None 827 if show_leaderboards_button: 828 button_width = 300 829 v -= leaderboards_button_space * 0.85 830 self._leaderboards_button = btn = ba.buttonwidget( 831 parent=self._subcontainer, 832 position=((self._sub_width - button_width) * 0.5, v), 833 color=(0.55, 0.5, 0.6), 834 textcolor=(0.75, 0.7, 0.8), 835 autoselect=True, 836 icon=ba.gettexture('googlePlayLeaderboardsIcon'), 837 icon_color=(0.8, 0.95, 0.7), 838 on_activate_call=self._on_leaderboards_press, 839 size=(button_width, 50), 840 label=ba.Lstr(resource='leaderboardsText'), 841 ) 842 if first_selectable is None: 843 first_selectable = btn 844 if ba.app.ui.use_toolbars: 845 ba.widget( 846 edit=btn, 847 right_widget=ba.internal.get_special_widget('party_button'), 848 ) 849 ba.widget(edit=btn, left_widget=bbtn) 850 v -= leaderboards_button_space * 0.15 851 else: 852 self._leaderboards_button = None 853 854 self._campaign_progress_text: ba.Widget | None 855 if show_campaign_progress: 856 v -= campaign_progress_space * 0.5 857 self._campaign_progress_text = ba.textwidget( 858 parent=self._subcontainer, 859 position=(self._sub_width * 0.5, v), 860 size=(0, 0), 861 scale=0.9, 862 color=(0.75, 0.7, 0.8), 863 maxwidth=self._sub_width * 0.8, 864 h_align='center', 865 v_align='center', 866 ) 867 v -= campaign_progress_space * 0.5 868 self._refresh_campaign_progress_text() 869 else: 870 self._campaign_progress_text = None 871 872 self._tickets_text: ba.Widget | None 873 if show_tickets: 874 v -= tickets_space * 0.5 875 self._tickets_text = ba.textwidget( 876 parent=self._subcontainer, 877 position=(self._sub_width * 0.5, v), 878 size=(0, 0), 879 scale=0.9, 880 color=(0.75, 0.7, 0.8), 881 maxwidth=self._sub_width * 0.8, 882 flatness=1.0, 883 h_align='center', 884 v_align='center', 885 ) 886 v -= tickets_space * 0.5 887 self._refresh_tickets_text() 888 889 else: 890 self._tickets_text = None 891 892 # bit of spacing before the reset/sign-out section 893 v -= 5 894 895 button_width = 250 896 if show_reset_progress_button: 897 confirm_text = ( 898 ba.Lstr(resource=self._r + '.resetProgressConfirmText') 899 if self._can_reset_achievements 900 else ba.Lstr( 901 resource=self._r + '.resetProgressConfirmNoAchievementsText' 902 ) 903 ) 904 v -= reset_progress_button_space 905 self._reset_progress_button = btn = ba.buttonwidget( 906 parent=self._subcontainer, 907 position=((self._sub_width - button_width) * 0.5, v), 908 color=(0.55, 0.5, 0.6), 909 textcolor=(0.75, 0.7, 0.8), 910 autoselect=True, 911 size=(button_width, 60), 912 label=ba.Lstr(resource=self._r + '.resetProgressText'), 913 on_activate_call=lambda: confirm.ConfirmWindow( 914 text=confirm_text, 915 width=500, 916 height=200, 917 action=self._reset_progress, 918 ), 919 ) 920 if first_selectable is None: 921 first_selectable = btn 922 if ba.app.ui.use_toolbars: 923 ba.widget( 924 edit=btn, 925 right_widget=ba.internal.get_special_widget('party_button'), 926 ) 927 ba.widget(edit=btn, left_widget=bbtn) 928 929 self._linked_accounts_text: ba.Widget | None 930 if show_linked_accounts_text: 931 v -= linked_accounts_text_space * 0.8 932 self._linked_accounts_text = ba.textwidget( 933 parent=self._subcontainer, 934 position=(self._sub_width * 0.5, v), 935 size=(0, 0), 936 scale=0.9, 937 color=(0.75, 0.7, 0.8), 938 maxwidth=self._sub_width * 0.95, 939 text=ba.Lstr(resource=self._r + '.linkedAccountsText'), 940 h_align='center', 941 v_align='center', 942 ) 943 v -= linked_accounts_text_space * 0.2 944 self._update_linked_accounts_text() 945 else: 946 self._linked_accounts_text = None 947 948 # Show link/unlink buttons only for V1 accounts. 949 950 if show_link_accounts_button: 951 v -= link_accounts_button_space 952 self._link_accounts_button = btn = ba.buttonwidget( 953 parent=self._subcontainer, 954 position=((self._sub_width - button_width) * 0.5, v), 955 autoselect=True, 956 size=(button_width, 60), 957 label='', 958 color=(0.55, 0.5, 0.6), 959 on_activate_call=self._link_accounts_press, 960 ) 961 ba.textwidget( 962 parent=self._subcontainer, 963 draw_controller=btn, 964 h_align='center', 965 v_align='center', 966 size=(0, 0), 967 position=(self._sub_width * 0.5, v + 17 + 20), 968 text=ba.Lstr(resource=self._r + '.linkAccountsText'), 969 maxwidth=button_width * 0.8, 970 color=(0.75, 0.7, 0.8), 971 ) 972 ba.textwidget( 973 parent=self._subcontainer, 974 draw_controller=btn, 975 h_align='center', 976 v_align='center', 977 size=(0, 0), 978 position=(self._sub_width * 0.5, v - 4 + 20), 979 text=ba.Lstr(resource=self._r + '.linkAccountsInfoText'), 980 flatness=1.0, 981 scale=0.5, 982 maxwidth=button_width * 0.8, 983 color=(0.75, 0.7, 0.8), 984 ) 985 if first_selectable is None: 986 first_selectable = btn 987 if ba.app.ui.use_toolbars: 988 ba.widget( 989 edit=btn, 990 right_widget=ba.internal.get_special_widget('party_button'), 991 ) 992 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) 993 994 self._unlink_accounts_button: ba.Widget | None 995 if show_unlink_accounts_button: 996 v -= unlink_accounts_button_space 997 self._unlink_accounts_button = btn = ba.buttonwidget( 998 parent=self._subcontainer, 999 position=((self._sub_width - button_width) * 0.5, v + 25), 1000 autoselect=True, 1001 size=(button_width, 60), 1002 label='', 1003 color=(0.55, 0.5, 0.6), 1004 on_activate_call=self._unlink_accounts_press, 1005 ) 1006 self._unlink_accounts_button_label = ba.textwidget( 1007 parent=self._subcontainer, 1008 draw_controller=btn, 1009 h_align='center', 1010 v_align='center', 1011 size=(0, 0), 1012 position=(self._sub_width * 0.5, v + 55), 1013 text=ba.Lstr(resource=self._r + '.unlinkAccountsText'), 1014 maxwidth=button_width * 0.8, 1015 color=(0.75, 0.7, 0.8), 1016 ) 1017 if first_selectable is None: 1018 first_selectable = btn 1019 if ba.app.ui.use_toolbars: 1020 ba.widget( 1021 edit=btn, 1022 right_widget=ba.internal.get_special_widget('party_button'), 1023 ) 1024 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) 1025 self._update_unlink_accounts_button() 1026 else: 1027 self._unlink_accounts_button = None 1028 1029 if show_v2_link_info: 1030 v -= v2_link_info_space 1031 ba.textwidget( 1032 parent=self._subcontainer, 1033 h_align='center', 1034 v_align='center', 1035 size=(0, 0), 1036 position=(self._sub_width * 0.5, v + v2_link_info_space - 20), 1037 text=ba.Lstr(resource='v2AccountLinkingInfoText'), 1038 flatness=1.0, 1039 scale=0.8, 1040 maxwidth=450, 1041 color=(0.5, 0.45, 0.55), 1042 ) 1043 1044 if self._show_legacy_unlink_button: 1045 v -= legacy_unlink_button_space 1046 button_width_w = button_width * 1.5 1047 ba.textwidget( 1048 parent=self._subcontainer, 1049 position=(self._sub_width * 0.5 - 150.0, v + 75), 1050 size=(300.0, 60), 1051 text=ba.Lstr(resource='whatIsThisText'), 1052 scale=0.8, 1053 color=(0.3, 0.7, 0.05), 1054 maxwidth=200.0, 1055 h_align='center', 1056 v_align='center', 1057 autoselect=True, 1058 selectable=True, 1059 on_activate_call=show_what_is_legacy_unlinking_page, 1060 click_activate=True, 1061 ) 1062 btn = ba.buttonwidget( 1063 parent=self._subcontainer, 1064 position=((self._sub_width - button_width_w) * 0.5, v + 25), 1065 autoselect=True, 1066 size=(button_width_w, 60), 1067 label=ba.Lstr(resource=self._r + '.unlinkLegacyV1AccountsText'), 1068 textcolor=(0.8, 0.4, 0), 1069 color=(0.55, 0.5, 0.6), 1070 on_activate_call=self._unlink_accounts_press, 1071 ) 1072 1073 if show_sign_out_button: 1074 v -= sign_out_button_space 1075 self._sign_out_button = btn = ba.buttonwidget( 1076 parent=self._subcontainer, 1077 position=((self._sub_width - button_width) * 0.5, v), 1078 size=(button_width, 60), 1079 label=ba.Lstr(resource=self._r + '.signOutText'), 1080 color=(0.55, 0.5, 0.6), 1081 textcolor=(0.75, 0.7, 0.8), 1082 autoselect=True, 1083 on_activate_call=self._sign_out_press, 1084 ) 1085 if first_selectable is None: 1086 first_selectable = btn 1087 if ba.app.ui.use_toolbars: 1088 ba.widget( 1089 edit=btn, 1090 right_widget=ba.internal.get_special_widget('party_button'), 1091 ) 1092 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) 1093 1094 if show_cancel_sign_in_button: 1095 v -= cancel_sign_in_button_space 1096 self._cancel_sign_in_button = btn = ba.buttonwidget( 1097 parent=self._subcontainer, 1098 position=((self._sub_width - button_width) * 0.5, v), 1099 size=(button_width, 60), 1100 label=ba.Lstr(resource='cancelText'), 1101 color=(0.55, 0.5, 0.6), 1102 textcolor=(0.75, 0.7, 0.8), 1103 autoselect=True, 1104 on_activate_call=self._cancel_sign_in_press, 1105 ) 1106 if first_selectable is None: 1107 first_selectable = btn 1108 if ba.app.ui.use_toolbars: 1109 ba.widget( 1110 edit=btn, 1111 right_widget=ba.internal.get_special_widget('party_button'), 1112 ) 1113 ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) 1114 1115 # Whatever the topmost selectable thing is, we want it to scroll all 1116 # the way up when we select it. 1117 if first_selectable is not None: 1118 ba.widget( 1119 edit=first_selectable, up_widget=bbtn, show_buffer_top=400 1120 ) 1121 # (this should re-scroll us to the top..) 1122 ba.containerwidget( 1123 edit=self._subcontainer, visible_child=first_selectable 1124 ) 1125 self._needs_refresh = False 1126 1127 def _on_custom_achievements_press(self) -> None: 1128 ba.timer( 1129 0.15, 1130 ba.Call(ba.internal.show_online_score_ui, 'achievements'), 1131 timetype=ba.TimeType.REAL, 1132 ) 1133 1134 def _on_achievements_press(self) -> None: 1135 # pylint: disable=cyclic-import 1136 from bastd.ui import achievements 1137 1138 assert self._achievements_button is not None 1139 achievements.AchievementsWindow( 1140 position=self._achievements_button.get_screen_space_center() 1141 ) 1142 1143 def _on_what_is_v2_press(self) -> None: 1144 show_what_is_v2_page() 1145 1146 def _on_manage_account_press(self) -> None: 1147 ba.screenmessage(ba.Lstr(resource='oneMomentText')) 1148 1149 # We expect to have a v2 account signed in if we get here. 1150 if ba.app.accounts_v2.primary is None: 1151 logging.exception( 1152 'got manage-account press without v2 account present' 1153 ) 1154 return 1155 1156 with ba.app.accounts_v2.primary: 1157 ba.app.cloud.send_message_cb( 1158 bacommon.cloud.ManageAccountMessage(), 1159 on_response=ba.WeakCall(self._on_manage_account_response), 1160 ) 1161 1162 def _on_manage_account_response( 1163 self, response: bacommon.cloud.ManageAccountResponse | Exception 1164 ) -> None: 1165 1166 if isinstance(response, Exception) or response.url is None: 1167 ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) 1168 ba.playsound(ba.getsound('error')) 1169 return 1170 1171 ba.open_url(response.url) 1172 1173 def _on_leaderboards_press(self) -> None: 1174 ba.timer( 1175 0.15, 1176 ba.Call(ba.internal.show_online_score_ui, 'leaderboards'), 1177 timetype=ba.TimeType.REAL, 1178 ) 1179 1180 def _have_unlinkable_v1_accounts(self) -> bool: 1181 # if this is not present, we haven't had contact from the server so 1182 # let's not proceed.. 1183 if ba.internal.get_public_login_id() is None: 1184 return False 1185 accounts = ba.internal.get_v1_account_misc_read_val_2( 1186 'linkedAccounts', [] 1187 ) 1188 return len(accounts) > 1 1189 1190 def _update_unlink_accounts_button(self) -> None: 1191 if self._unlink_accounts_button is None: 1192 return 1193 if self._have_unlinkable_v1_accounts(): 1194 clr = (0.75, 0.7, 0.8, 1.0) 1195 else: 1196 clr = (1.0, 1.0, 1.0, 0.25) 1197 ba.textwidget(edit=self._unlink_accounts_button_label, color=clr) 1198 1199 def _should_show_legacy_unlink_button(self) -> bool: 1200 1201 # Only show this when fully signed in to a v2 account. 1202 if not self._v1_signed_in or ba.app.accounts_v2.primary is None: 1203 return False 1204 1205 out = self._have_unlinkable_v1_accounts() 1206 return out 1207 1208 def _update_linked_accounts_text(self) -> None: 1209 if self._linked_accounts_text is None: 1210 return 1211 1212 # Disable this by default when signed in to a V2 account 1213 # (since this shows V1 links which we should no longer care about). 1214 if ( 1215 ba.app.accounts_v2.primary is not None 1216 and not FORCE_ENABLE_V1_LINKING 1217 ): 1218 return 1219 1220 # if this is not present, we haven't had contact from the server so 1221 # let's not proceed.. 1222 if ba.internal.get_public_login_id() is None: 1223 num = int(time.time()) % 4 1224 accounts_str = num * '.' + (4 - num) * ' ' 1225 else: 1226 accounts = ba.internal.get_v1_account_misc_read_val_2( 1227 'linkedAccounts', [] 1228 ) 1229 # UPDATE - we now just print the number here; not the actual 1230 # accounts (they can see that in the unlink section if they're 1231 # curious) 1232 accounts_str = str(max(0, len(accounts) - 1)) 1233 ba.textwidget( 1234 edit=self._linked_accounts_text, 1235 text=ba.Lstr( 1236 value='${L} ${A}', 1237 subs=[ 1238 ('${L}', ba.Lstr(resource=self._r + '.linkedAccountsText')), 1239 ('${A}', accounts_str), 1240 ], 1241 ), 1242 ) 1243 1244 def _refresh_campaign_progress_text(self) -> None: 1245 from ba.internal import getcampaign 1246 1247 if self._campaign_progress_text is None: 1248 return 1249 p_str: str | ba.Lstr 1250 try: 1251 campaign = getcampaign('Default') 1252 levels = campaign.levels 1253 levels_complete = sum((1 if l.complete else 0) for l in levels) 1254 1255 # Last level cant be completed; hence the -1; 1256 progress = min(1.0, float(levels_complete) / (len(levels) - 1)) 1257 p_str = ba.Lstr( 1258 resource=self._r + '.campaignProgressText', 1259 subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')], 1260 ) 1261 except Exception: 1262 p_str = '?' 1263 ba.print_exception('Error calculating co-op campaign progress.') 1264 ba.textwidget(edit=self._campaign_progress_text, text=p_str) 1265 1266 def _refresh_tickets_text(self) -> None: 1267 if self._tickets_text is None: 1268 return 1269 try: 1270 tc_str = str(ba.internal.get_v1_account_ticket_count()) 1271 except Exception: 1272 ba.print_exception() 1273 tc_str = '-' 1274 ba.textwidget( 1275 edit=self._tickets_text, 1276 text=ba.Lstr( 1277 resource=self._r + '.ticketsText', subs=[('${COUNT}', tc_str)] 1278 ), 1279 ) 1280 1281 def _refresh_account_name_text(self) -> None: 1282 1283 if self._account_name_text is None: 1284 return 1285 try: 1286 name_str = ba.internal.get_v1_account_display_string() 1287 except Exception: 1288 ba.print_exception() 1289 name_str = '??' 1290 1291 ba.textwidget(edit=self._account_name_text, text=name_str) 1292 if self._account_name_what_is_text is not None: 1293 swidth = ba.internal.get_string_width( 1294 name_str, suppress_warning=True 1295 ) 1296 # Eww; number-fudging. Need to recalibrate this if 1297 # account name scaling changes. 1298 x = self._sub_width * 0.5 - swidth * 0.75 - 170 1299 1300 ba.textwidget( 1301 edit=self._account_name_what_is_text, 1302 position=(x, self._account_name_what_is_y), 1303 ) 1304 1305 def _refresh_achievements(self) -> None: 1306 if ( 1307 self._achievements_text is None 1308 and self._achievements_button is None 1309 ): 1310 return 1311 complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) 1312 total = len(ba.app.ach.achievements) 1313 txt_final = ba.Lstr( 1314 resource=self._r + '.achievementProgressText', 1315 subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))], 1316 ) 1317 1318 if self._achievements_text is not None: 1319 ba.textwidget(edit=self._achievements_text, text=txt_final) 1320 if self._achievements_button is not None: 1321 ba.buttonwidget(edit=self._achievements_button, label=txt_final) 1322 1323 def _link_accounts_press(self) -> None: 1324 # pylint: disable=cyclic-import 1325 from bastd.ui.account import link 1326 1327 link.AccountLinkWindow(origin_widget=self._link_accounts_button) 1328 1329 def _unlink_accounts_press(self) -> None: 1330 # pylint: disable=cyclic-import 1331 from bastd.ui.account import unlink 1332 1333 if not self._have_unlinkable_v1_accounts(): 1334 ba.playsound(ba.getsound('error')) 1335 return 1336 unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) 1337 1338 def _player_profiles_press(self) -> None: 1339 # pylint: disable=cyclic-import 1340 from bastd.ui.profile import browser as pbrowser 1341 1342 self._save_state() 1343 ba.containerwidget(edit=self._root_widget, transition='out_left') 1344 pbrowser.ProfileBrowserWindow( 1345 origin_widget=self._player_profiles_button 1346 ) 1347 1348 def _cancel_sign_in_press(self) -> None: 1349 1350 # If we're waiting on an adapter to give us credentials, abort. 1351 self._signing_in_adapter = None 1352 1353 # Say we don't wanna be signed in anymore if we are. 1354 ba.app.accounts_v2.set_primary_credentials(None) 1355 1356 self._needs_refresh = True 1357 1358 # Speed UI updates along. 1359 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1360 1361 def _sign_out_press(self) -> None: 1362 1363 if ba.app.accounts_v2.have_primary_credentials(): 1364 if ( 1365 ba.app.accounts_v2.primary is not None 1366 and LoginType.GPGS in ba.app.accounts_v2.primary.logins 1367 ): 1368 self._explicitly_signed_out_of_gpgs = True 1369 ba.app.accounts_v2.set_primary_credentials(None) 1370 else: 1371 ba.internal.sign_out_v1() 1372 1373 cfg = ba.app.config 1374 1375 # Also take note that its our *explicit* intention to not be 1376 # signed in at this point (affects v1 accounts). 1377 cfg['Auto Account State'] = 'signed_out' 1378 cfg.commit() 1379 ba.buttonwidget( 1380 edit=self._sign_out_button, 1381 label=ba.Lstr(resource=self._r + '.signingOutText'), 1382 ) 1383 1384 # Speed UI updates along. 1385 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1386 1387 def _sign_in_press(self, login_type: str | LoginType) -> None: 1388 1389 # V1 login types are strings. 1390 if isinstance(login_type, str): 1391 ba.internal.sign_in_v1(login_type) 1392 1393 # Make note of the type account we're *wanting* 1394 # to be signed in with. 1395 cfg = ba.app.config 1396 cfg['Auto Account State'] = login_type 1397 cfg.commit() 1398 self._needs_refresh = True 1399 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1400 return 1401 1402 # V2 login sign-in buttons generally go through adapters. 1403 adapter = ba.app.accounts_v2.login_adapters.get(login_type) 1404 if adapter is not None: 1405 self._signing_in_adapter = adapter 1406 adapter.sign_in( 1407 result_cb=ba.WeakCall(self._on_adapter_sign_in_result), 1408 description='account settings button', 1409 ) 1410 # Will get 'Signing in...' to show. 1411 self._needs_refresh = True 1412 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1413 else: 1414 ba.screenmessage(f'Unsupported login_type: {login_type.name}') 1415 1416 def _on_adapter_sign_in_result( 1417 self, 1418 adapter: LoginAdapter, 1419 result: LoginAdapter.SignInResult | Exception, 1420 ) -> None: 1421 is_us = self._signing_in_adapter is adapter 1422 1423 # If this isn't our current one we don't care. 1424 if not is_us: 1425 return 1426 1427 # If it is us, note that we're done. 1428 self._signing_in_adapter = None 1429 1430 if isinstance(result, Exception): 1431 # For now just make a bit of noise if anything went wrong; 1432 # can get more specific as needed later. 1433 ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) 1434 ba.playsound(ba.getsound('error')) 1435 else: 1436 # Success! Plug in these credentials which will begin 1437 # verifying them and set our primary account-handle 1438 # when finished. 1439 ba.app.accounts_v2.set_primary_credentials(result.credentials) 1440 1441 # Special case - if the user has explicitly logged out and 1442 # logged in again with GPGS via this button, warn them that 1443 # they need to use the app if they want to switch to a 1444 # different GPGS account. 1445 if ( 1446 self._explicitly_signed_out_of_gpgs 1447 and adapter.login_type is LoginType.GPGS 1448 ): 1449 # Delay this slightly so it hopefully pops up after 1450 # credentials go through and the account name shows up. 1451 ba.timer( 1452 1.5, 1453 ba.Call( 1454 ba.screenmessage, 1455 ba.Lstr( 1456 resource=self._r 1457 + '.googlePlayGamesAccountSwitchText' 1458 ), 1459 ), 1460 ) 1461 1462 # Speed any UI updates along. 1463 self._needs_refresh = True 1464 ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) 1465 1466 def _v2_proxy_sign_in_press(self) -> None: 1467 # pylint: disable=cyclic-import 1468 from bastd.ui.account.v2proxy import V2ProxySignInWindow 1469 1470 assert self._sign_in_v2_proxy_button is not None 1471 V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button) 1472 1473 def _reset_progress(self) -> None: 1474 try: 1475 from ba.internal import getcampaign 1476 1477 # FIXME: This would need to happen server-side these days. 1478 if self._can_reset_achievements: 1479 ba.app.config['Achievements'] = {} 1480 ba.internal.reset_achievements() 1481 campaign = getcampaign('Default') 1482 campaign.reset() # also writes the config.. 1483 campaign = getcampaign('Challenges') 1484 campaign.reset() # also writes the config.. 1485 except Exception: 1486 ba.print_exception('Error resetting co-op campaign progress.') 1487 1488 ba.playsound(ba.getsound('shieldDown')) 1489 self._refresh() 1490 1491 def _back(self) -> None: 1492 # pylint: disable=cyclic-import 1493 from bastd.ui.mainmenu import MainMenuWindow 1494 1495 self._save_state() 1496 ba.containerwidget( 1497 edit=self._root_widget, transition=self._transition_out 1498 ) 1499 1500 if not self._modal: 1501 ba.app.ui.set_main_menu_window( 1502 MainMenuWindow(transition='in_left').get_root_widget() 1503 ) 1504 1505 def _save_state(self) -> None: 1506 try: 1507 sel = self._root_widget.get_selected_child() 1508 if sel == self._back_button: 1509 sel_name = 'Back' 1510 elif sel == self._scrollwidget: 1511 sel_name = 'Scroll' 1512 else: 1513 raise ValueError('unrecognized selection') 1514 ba.app.ui.window_states[type(self)] = sel_name 1515 except Exception: 1516 ba.print_exception(f'Error saving state for {self}.') 1517 1518 def _restore_state(self) -> None: 1519 try: 1520 sel_name = ba.app.ui.window_states.get(type(self)) 1521 if sel_name == 'Back': 1522 sel = self._back_button 1523 elif sel_name == 'Scroll': 1524 sel = self._scrollwidget 1525 else: 1526 sel = self._back_button 1527 ba.containerwidget(edit=self._root_widget, selected_child=sel) 1528 except Exception: 1529 ba.print_exception(f'Error restoring state for {self}.')
Window for account related functionality.
AccountSettingsWindow( transition: str = 'in_right', modal: bool = False, origin_widget: _ba.Widget | None = None, close_once_signed_in: bool = False)
30 def __init__( 31 self, 32 transition: str = 'in_right', 33 modal: bool = False, 34 origin_widget: ba.Widget | None = None, 35 close_once_signed_in: bool = False, 36 ): 37 # pylint: disable=too-many-statements 38 39 self._sign_in_v2_proxy_button: ba.Widget | None = None 40 self._sign_in_device_button: ba.Widget | None = None 41 42 self._show_legacy_unlink_button = False 43 44 self._signing_in_adapter: LoginAdapter | None = None 45 self._close_once_signed_in = close_once_signed_in 46 ba.set_analytics_screen('Account Window') 47 48 self._explicitly_signed_out_of_gpgs = False 49 50 # If they provided an origin-widget, scale up from that. 51 scale_origin: tuple[float, float] | None 52 if origin_widget is not None: 53 self._transition_out = 'out_scale' 54 scale_origin = origin_widget.get_screen_space_center() 55 transition = 'in_scale' 56 else: 57 self._transition_out = 'out_right' 58 scale_origin = None 59 60 self._r = 'accountSettingsWindow' 61 self._modal = modal 62 self._needs_refresh = False 63 self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in' 64 self._v1_account_state_num = ba.internal.get_v1_account_state_num() 65 self._check_sign_in_timer = ba.Timer( 66 1.0, 67 ba.WeakCall(self._update), 68 timetype=ba.TimeType.REAL, 69 repeat=True, 70 ) 71 72 # Currently we can only reset achievements on game-center. 73 v1_account_type: str | None 74 if self._v1_signed_in: 75 v1_account_type = ba.internal.get_v1_account_type() 76 else: 77 v1_account_type = None 78 self._can_reset_achievements = v1_account_type == 'Game Center' 79 80 app = ba.app 81 uiscale = app.ui.uiscale 82 83 self._width = 760 if uiscale is ba.UIScale.SMALL else 660 84 x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 85 self._height = ( 86 390 87 if uiscale is ba.UIScale.SMALL 88 else 430 89 if uiscale is ba.UIScale.MEDIUM 90 else 490 91 ) 92 93 self._sign_in_button = None 94 self._sign_in_text = None 95 96 self._scroll_width = self._width - (100 + x_offs * 2) 97 self._scroll_height = self._height - 120 98 self._sub_width = self._scroll_width - 20 99 100 # Determine which sign-in/sign-out buttons we should show. 101 self._show_sign_in_buttons: list[str] = [] 102 103 if LoginType.GPGS in ba.app.accounts_v2.login_adapters: 104 self._show_sign_in_buttons.append('Google Play') 105 106 # Always want to show our web-based v2 login option. 107 self._show_sign_in_buttons.append('V2Proxy') 108 109 # Legacy v1 device accounts are currently always available 110 # (though we need to start phasing them out at some point). 111 self._show_sign_in_buttons.append('Device') 112 113 top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 114 super().__init__( 115 root_widget=ba.containerwidget( 116 size=(self._width, self._height + top_extra), 117 transition=transition, 118 toolbar_visibility='menu_minimal', 119 scale_origin_stack_offset=scale_origin, 120 scale=( 121 2.09 122 if uiscale is ba.UIScale.SMALL 123 else 1.4 124 if uiscale is ba.UIScale.MEDIUM 125 else 1.0 126 ), 127 stack_offset=(0, -19) 128 if uiscale is ba.UIScale.SMALL 129 else (0, 0), 130 ) 131 ) 132 if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: 133 self._back_button = None 134 ba.containerwidget( 135 edit=self._root_widget, on_cancel_call=self._back 136 ) 137 else: 138 self._back_button = btn = ba.buttonwidget( 139 parent=self._root_widget, 140 position=(51 + x_offs, self._height - 62), 141 size=(120, 60), 142 scale=0.8, 143 text_scale=1.2, 144 autoselect=True, 145 label=ba.Lstr( 146 resource='doneText' if self._modal else 'backText' 147 ), 148 button_type='regular' if self._modal else 'back', 149 on_activate_call=self._back, 150 ) 151 ba.containerwidget(edit=self._root_widget, cancel_button=btn) 152 if not self._modal: 153 ba.buttonwidget( 154 edit=btn, 155 button_type='backSmall', 156 size=(60, 56), 157 label=ba.charstr(ba.SpecialChar.BACK), 158 ) 159 160 ba.textwidget( 161 parent=self._root_widget, 162 position=(self._width * 0.5, self._height - 41), 163 size=(0, 0), 164 text=ba.Lstr(resource=self._r + '.titleText'), 165 color=ba.app.ui.title_color, 166 maxwidth=self._width - 340, 167 h_align='center', 168 v_align='center', 169 ) 170 171 self._scrollwidget = ba.scrollwidget( 172 parent=self._root_widget, 173 highlight=False, 174 position=( 175 (self._width - self._scroll_width) * 0.5, 176 self._height - 65 - self._scroll_height, 177 ), 178 size=(self._scroll_width, self._scroll_height), 179 claims_left_right=True, 180 claims_tab=True, 181 selection_loops_to_parent=True, 182 ) 183 self._subcontainer: ba.Widget | None = None 184 self._refresh() 185 self._restore_state()
Inherited Members
- ba.ui.Window
- get_root_widget
def
show_what_is_v2_page() -> None:
1532def show_what_is_v2_page() -> None: 1533 """Show the webpage describing V2 accounts.""" 1534 bamasteraddr = ba.internal.get_master_server_address(version=2) 1535 ba.open_url(f'{bamasteraddr}/whatisv2')
Show the webpage describing V2 accounts.
def
show_what_is_legacy_unlinking_page() -> None:
1538def show_what_is_legacy_unlinking_page() -> None: 1539 """Show the webpage describing legacy unlinking.""" 1540 bamasteraddr = ba.internal.get_master_server_address(version=2) 1541 ba.open_url(f'{bamasteraddr}/whatarev1links')
Show the webpage describing legacy unlinking.