# Released under the MIT License. See LICENSE for details. # """Provides UI for account functionality.""" # pylint: disable=too-many-lines from __future__ import annotations import time import logging from typing import TYPE_CHECKING import bacommon.cloud from bacommon.login import LoginType import ba import ba.internal if TYPE_CHECKING: from ba.internal import LoginAdapter # These days we're directing people to the web based account settings # for V2 account linking and trying to get them to disconnect remaining # V1 links, but leaving this escape hatch here in case needed. FORCE_ENABLE_V1_LINKING = False class AccountSettingsWindow(ba.Window): """Window for account related functionality.""" def __init__( self, transition: str = 'in_right', modal: bool = False, origin_widget: ba.Widget | None = None, close_once_signed_in: bool = False, ): # pylint: disable=too-many-statements self._sign_in_v2_proxy_button: ba.Widget | None = None self._sign_in_device_button: ba.Widget | None = None self._show_legacy_unlink_button = False self._signing_in_adapter: LoginAdapter | None = None self._close_once_signed_in = close_once_signed_in ba.set_analytics_screen('Account Window') self._explicitly_signed_out_of_gpgs = False # If they provided an origin-widget, scale up from that. scale_origin: tuple[float, float] | None if origin_widget is not None: self._transition_out = 'out_scale' scale_origin = origin_widget.get_screen_space_center() transition = 'in_scale' else: self._transition_out = 'out_right' scale_origin = None self._r = 'accountSettingsWindow' self._modal = modal self._needs_refresh = False self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in' self._v1_account_state_num = ba.internal.get_v1_account_state_num() self._check_sign_in_timer = ba.Timer( 1.0, ba.WeakCall(self._update), timetype=ba.TimeType.REAL, repeat=True, ) # Currently we can only reset achievements on game-center. v1_account_type: str | None if self._v1_signed_in: v1_account_type = ba.internal.get_v1_account_type() else: v1_account_type = None self._can_reset_achievements = v1_account_type == 'Game Center' app = ba.app uiscale = app.ui.uiscale self._width = 760 if uiscale is ba.UIScale.SMALL else 660 x_offs = 50 if uiscale is ba.UIScale.SMALL else 0 self._height = ( 390 if uiscale is ba.UIScale.SMALL else 430 if uiscale is ba.UIScale.MEDIUM else 490 ) self._sign_in_button = None self._sign_in_text = None self._scroll_width = self._width - (100 + x_offs * 2) self._scroll_height = self._height - 120 self._sub_width = self._scroll_width - 20 # Determine which sign-in/sign-out buttons we should show. self._show_sign_in_buttons: list[str] = [] if LoginType.GPGS in ba.app.accounts_v2.login_adapters: self._show_sign_in_buttons.append('Google Play') # Always want to show our web-based v2 login option. self._show_sign_in_buttons.append('V2Proxy') # Legacy v1 device accounts are currently always available # (though we need to start phasing them out at some point). self._show_sign_in_buttons.append('Device') top_extra = 15 if uiscale is ba.UIScale.SMALL else 0 super().__init__( root_widget=ba.containerwidget( size=(self._width, self._height + top_extra), transition=transition, toolbar_visibility='menu_minimal', scale_origin_stack_offset=scale_origin, scale=( 2.09 if uiscale is ba.UIScale.SMALL else 1.4 if uiscale is ba.UIScale.MEDIUM else 1.0 ), stack_offset=(0, -19) if uiscale is ba.UIScale.SMALL else (0, 0), ) ) if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars: self._back_button = None ba.containerwidget( edit=self._root_widget, on_cancel_call=self._back ) else: self._back_button = btn = ba.buttonwidget( parent=self._root_widget, position=(51 + x_offs, self._height - 62), size=(120, 60), scale=0.8, text_scale=1.2, autoselect=True, label=ba.Lstr( resource='doneText' if self._modal else 'backText' ), button_type='regular' if self._modal else 'back', on_activate_call=self._back, ) ba.containerwidget(edit=self._root_widget, cancel_button=btn) if not self._modal: ba.buttonwidget( edit=btn, button_type='backSmall', size=(60, 56), label=ba.charstr(ba.SpecialChar.BACK), ) ba.textwidget( parent=self._root_widget, position=(self._width * 0.5, self._height - 41), size=(0, 0), text=ba.Lstr(resource=self._r + '.titleText'), color=ba.app.ui.title_color, maxwidth=self._width - 340, h_align='center', v_align='center', ) self._scrollwidget = ba.scrollwidget( parent=self._root_widget, highlight=False, position=( (self._width - self._scroll_width) * 0.5, self._height - 65 - self._scroll_height, ), size=(self._scroll_width, self._scroll_height), claims_left_right=True, claims_tab=True, selection_loops_to_parent=True, ) self._subcontainer: ba.Widget | None = None self._refresh() self._restore_state() def _update(self) -> None: # If they want us to close once we're signed in, do so. if self._close_once_signed_in and self._v1_signed_in: self._back() return # Hmm should update this to use get_account_state_num. # Theoretically if we switch from one signed-in account to another # in the background this would break. v1_account_state_num = ba.internal.get_v1_account_state_num() v1_account_state = ba.internal.get_v1_account_state() show_legacy_unlink_button = self._should_show_legacy_unlink_button() if ( v1_account_state_num != self._v1_account_state_num or show_legacy_unlink_button != self._show_legacy_unlink_button or self._needs_refresh ): self._v1_account_state_num = v1_account_state_num self._v1_signed_in = v1_account_state == 'signed_in' self._show_legacy_unlink_button = show_legacy_unlink_button self._refresh() # Go ahead and refresh some individual things # that may change under us. self._update_linked_accounts_text() self._update_unlink_accounts_button() self._refresh_campaign_progress_text() self._refresh_achievements() self._refresh_tickets_text() self._refresh_account_name_text() def _get_sign_in_text(self) -> ba.Lstr: return ba.Lstr(resource=self._r + '.signInText') def _refresh(self) -> None: # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=cyclic-import from bastd.ui import confirm primary_v2_account = ba.app.accounts_v2.primary v1_state = ba.internal.get_v1_account_state() v1_account_type = ( ba.internal.get_v1_account_type() if v1_state == 'signed_in' else 'unknown' ) # We expose GPGS-specific functionality only if it is 'active' # (meaning the current GPGS player matches one of our account's # logins). gpgs_adapter = ba.app.accounts_v2.login_adapters.get(LoginType.GPGS) is_gpgs = ( False if gpgs_adapter is None else gpgs_adapter.is_back_end_active() ) show_signed_in_as = self._v1_signed_in signed_in_as_space = 95.0 show_sign_in_benefits = not self._v1_signed_in sign_in_benefits_space = 80.0 show_signing_in_text = ( v1_state == 'signing_in' or self._signing_in_adapter is not None ) signing_in_text_space = 80.0 show_google_play_sign_in_button = ( v1_state == 'signed_out' and self._signing_in_adapter is None and 'Google Play' in self._show_sign_in_buttons ) show_v2_proxy_sign_in_button = ( v1_state == 'signed_out' and self._signing_in_adapter is None and 'V2Proxy' in self._show_sign_in_buttons ) show_device_sign_in_button = ( v1_state == 'signed_out' and self._signing_in_adapter is None and 'Device' in self._show_sign_in_buttons ) sign_in_button_space = 70.0 deprecated_space = 60 show_game_service_button = self._v1_signed_in and v1_account_type in [ 'Game Center' ] game_service_button_space = 60.0 show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2' show_linked_accounts_text = self._v1_signed_in linked_accounts_text_space = 60.0 show_achievements_button = self._v1_signed_in and v1_account_type in ( 'Google Play', 'Local', 'V2', ) achievements_button_space = 60.0 show_achievements_text = ( self._v1_signed_in and not show_achievements_button ) achievements_text_space = 27.0 show_leaderboards_button = self._v1_signed_in and is_gpgs leaderboards_button_space = 60.0 show_campaign_progress = self._v1_signed_in campaign_progress_space = 27.0 show_tickets = self._v1_signed_in tickets_space = 27.0 show_reset_progress_button = False reset_progress_button_space = 70.0 show_manage_v2_account_button = ( self._v1_signed_in and v1_account_type == 'V2' ) manage_v2_account_button_space = 100.0 show_player_profiles_button = self._v1_signed_in player_profiles_button_space = ( 70.0 if show_manage_v2_account_button else 100.0 ) show_link_accounts_button = self._v1_signed_in and ( primary_v2_account is None or FORCE_ENABLE_V1_LINKING ) link_accounts_button_space = 70.0 show_unlink_accounts_button = show_link_accounts_button unlink_accounts_button_space = 90.0 show_v2_link_info = self._v1_signed_in and not show_link_accounts_button v2_link_info_space = 70.0 legacy_unlink_button_space = 120.0 show_sign_out_button = self._v1_signed_in and v1_account_type in [ 'Local', 'Google Play', 'V2', ] sign_out_button_space = 70.0 # We can show cancel if we're either waiting on an adapter to # provide us with v2 credentials or waiting for those credentials # to be verified. show_cancel_sign_in_button = self._signing_in_adapter is not None or ( ba.app.accounts_v2.have_primary_credentials() and primary_v2_account is None ) cancel_sign_in_button_space = 70.0 if self._subcontainer is not None: self._subcontainer.delete() self._sub_height = 60.0 if show_signed_in_as: self._sub_height += signed_in_as_space if show_signing_in_text: self._sub_height += signing_in_text_space if show_google_play_sign_in_button: self._sub_height += sign_in_button_space if show_v2_proxy_sign_in_button: self._sub_height += sign_in_button_space if show_device_sign_in_button: self._sub_height += sign_in_button_space + deprecated_space if show_game_service_button: self._sub_height += game_service_button_space if show_linked_accounts_text: self._sub_height += linked_accounts_text_space if show_achievements_text: self._sub_height += achievements_text_space if show_achievements_button: self._sub_height += achievements_button_space if show_leaderboards_button: self._sub_height += leaderboards_button_space if show_campaign_progress: self._sub_height += campaign_progress_space if show_tickets: self._sub_height += tickets_space if show_sign_in_benefits: self._sub_height += sign_in_benefits_space if show_reset_progress_button: self._sub_height += reset_progress_button_space if show_manage_v2_account_button: self._sub_height += manage_v2_account_button_space if show_player_profiles_button: self._sub_height += player_profiles_button_space if show_link_accounts_button: self._sub_height += link_accounts_button_space if show_unlink_accounts_button: self._sub_height += unlink_accounts_button_space if show_v2_link_info: self._sub_height += v2_link_info_space if self._show_legacy_unlink_button: self._sub_height += legacy_unlink_button_space if show_sign_out_button: self._sub_height += sign_out_button_space if show_cancel_sign_in_button: self._sub_height += cancel_sign_in_button_space self._subcontainer = ba.containerwidget( parent=self._scrollwidget, size=(self._sub_width, self._sub_height), background=False, claims_left_right=True, claims_tab=True, selection_loops_to_parent=True, ) first_selectable = None v = self._sub_height - 10.0 self._account_name_what_is_text: ba.Widget | None self._account_name_what_is_y = 0.0 self._account_name_text: ba.Widget | None if show_signed_in_as: v -= signed_in_as_space * 0.2 txt = ba.Lstr( resource='accountSettingsWindow.youAreSignedInAsText', fallback_resource='accountSettingsWindow.youAreLoggedInAsText', ) ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), text=txt, scale=0.9, color=ba.app.ui.title_color, maxwidth=self._sub_width * 0.9, h_align='center', v_align='center', ) v -= signed_in_as_space * 0.5 self._account_name_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=1.5, maxwidth=self._sub_width * 0.9, res_scale=1.5, color=(1, 1, 1, 1), h_align='center', v_align='center', ) if show_what_is_v2: self._account_name_what_is_y = v - 23.0 self._account_name_what_is_text = ba.textwidget( parent=self._subcontainer, position=(0.0, self._account_name_what_is_y), size=(200.0, 60), text=ba.Lstr( value='${WHAT} -->', subs=[('${WHAT}', ba.Lstr(resource='whatIsThisText'))], ), scale=0.6, color=(0.3, 0.7, 0.05), maxwidth=200.0, h_align='right', v_align='center', autoselect=True, selectable=True, on_activate_call=show_what_is_v2_page, click_activate=True, ) if first_selectable is None: first_selectable = self._account_name_what_is_text else: self._account_name_what_is_text = None self._refresh_account_name_text() v -= signed_in_as_space * 0.4 else: self._account_name_text = None self._account_name_what_is_text = None if self._back_button is None: bbtn = ba.internal.get_special_widget('back_button') else: bbtn = self._back_button if show_sign_in_benefits: v -= sign_in_benefits_space app = ba.app extra: str | ba.Lstr | None if app.platform in ['mac', 'ios'] and app.subplatform == 'appstore': extra = ba.Lstr( value='\n${S}', subs=[ ('${S}', ba.Lstr(resource='signInWithGameCenterText')) ], ) else: extra = '' ba.textwidget( parent=self._subcontainer, position=( self._sub_width * 0.5, v + sign_in_benefits_space * 0.4, ), size=(0, 0), text=ba.Lstr( value='${A}${B}', subs=[ ('${A}', ba.Lstr(resource=self._r + '.signInInfoText')), ('${B}', extra), ], ), max_height=sign_in_benefits_space * 0.9, scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.8, h_align='center', v_align='center', ) if show_signing_in_text: v -= signing_in_text_space ba.textwidget( parent=self._subcontainer, position=( self._sub_width * 0.5, v + signing_in_text_space * 0.5, ), size=(0, 0), text=ba.Lstr(resource='accountSettingsWindow.signingInText'), scale=0.9, color=(0, 1, 0), maxwidth=self._sub_width * 0.8, h_align='center', v_align='center', ) if show_google_play_sign_in_button: button_width = 350 v -= sign_in_button_space self._sign_in_google_play_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, size=(button_width, 60), label=ba.Lstr( value='${A}${B}', subs=[ ( '${A}', ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO), ), ( '${B}', ba.Lstr( resource=self._r + '.signInWithGooglePlayText' ), ), ], ), on_activate_call=lambda: self._sign_in_press(LoginType.GPGS), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_v2_proxy_sign_in_button: button_width = 350 v -= sign_in_button_space self._sign_in_v2_proxy_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, size=(button_width, 60), label='', on_activate_call=self._v2_proxy_sign_in_press, ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17), text=ba.Lstr( value='${A}${B}', subs=[ ('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)), ( '${B}', ba.Lstr(resource=self._r + '.signInWithV2Text'), ), ], ), maxwidth=button_width * 0.8, color=(0.75, 1.0, 0.7), ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4), text=ba.Lstr(resource=self._r + '.signInWithV2InfoText'), flatness=1.0, scale=0.57, maxwidth=button_width * 0.9, color=(0.55, 0.8, 0.5), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_device_sign_in_button: button_width = 350 v -= sign_in_button_space + deprecated_space self._sign_in_device_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v - 20), autoselect=True, size=(button_width, 60), label='', on_activate_call=lambda: self._sign_in_press('Local'), ) ba.textwidget( parent=self._subcontainer, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 60), text=ba.Lstr(resource='deprecatedText'), scale=0.8, maxwidth=300, color=(0.6, 0.55, 0.45), ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17), text=ba.Lstr( value='${A}${B}', subs=[ ('${A}', ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)), ( '${B}', ba.Lstr(resource=self._r + '.signInWithDeviceText'), ), ], ), maxwidth=button_width * 0.8, color=(0.75, 1.0, 0.7), ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4), text=ba.Lstr(resource=self._r + '.signInWithDeviceInfoText'), flatness=1.0, scale=0.57, maxwidth=button_width * 0.9, color=(0.55, 0.8, 0.5), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100) self._sign_in_text = None if show_manage_v2_account_button: button_width = 300 v -= manage_v2_account_button_space self._manage_v2_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 30), autoselect=True, size=(button_width, 60), label=ba.Lstr(resource=self._r + '.manageAccountText'), color=(0.55, 0.5, 0.6), icon=ba.gettexture('settingsIcon'), textcolor=(0.75, 0.7, 0.8), on_activate_call=ba.WeakCall(self._on_manage_account_press), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) if show_player_profiles_button: button_width = 300 v -= player_profiles_button_space self._player_profiles_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 30), autoselect=True, size=(button_width, 60), label=ba.Lstr(resource='playerProfilesWindow.titleText'), color=(0.55, 0.5, 0.6), icon=ba.gettexture('cuteSpaz'), textcolor=(0.75, 0.7, 0.8), on_activate_call=self._player_profiles_press, ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0) # the button to go to OS-Specific leaderboards/high-score-lists/etc. if show_game_service_button: button_width = 300 v -= game_service_button_space * 0.85 v1_account_type = ba.internal.get_v1_account_type() if v1_account_type == 'Game Center': v1_account_type_name = ba.Lstr(resource='gameCenterText') else: raise ValueError( "unknown account type: '" + str(v1_account_type) + "'" ) self._game_service_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, on_activate_call=ba.internal.show_online_score_ui, size=(button_width, 50), label=v1_account_type_name, ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) v -= game_service_button_space * 0.15 else: self.game_service_button = None self._achievements_text: ba.Widget | None if show_achievements_text: v -= achievements_text_space * 0.5 self._achievements_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.8, h_align='center', v_align='center', ) v -= achievements_text_space * 0.5 else: self._achievements_text = None self._achievements_button: ba.Widget | None if show_achievements_button: button_width = 300 v -= achievements_button_space * 0.85 self._achievements_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, icon=ba.gettexture( 'googlePlayAchievementsIcon' if is_gpgs else 'achievementsIcon' ), icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9), on_activate_call=( self._on_custom_achievements_press if is_gpgs else self._on_achievements_press ), size=(button_width, 50), label='', ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) v -= achievements_button_space * 0.15 else: self._achievements_button = None if show_achievements_text or show_achievements_button: self._refresh_achievements() self._leaderboards_button: ba.Widget | None if show_leaderboards_button: button_width = 300 v -= leaderboards_button_space * 0.85 self._leaderboards_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, icon=ba.gettexture('googlePlayLeaderboardsIcon'), icon_color=(0.8, 0.95, 0.7), on_activate_call=self._on_leaderboards_press, size=(button_width, 50), label=ba.Lstr(resource='leaderboardsText'), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) v -= leaderboards_button_space * 0.15 else: self._leaderboards_button = None self._campaign_progress_text: ba.Widget | None if show_campaign_progress: v -= campaign_progress_space * 0.5 self._campaign_progress_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.8, h_align='center', v_align='center', ) v -= campaign_progress_space * 0.5 self._refresh_campaign_progress_text() else: self._campaign_progress_text = None self._tickets_text: ba.Widget | None if show_tickets: v -= tickets_space * 0.5 self._tickets_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.8, flatness=1.0, h_align='center', v_align='center', ) v -= tickets_space * 0.5 self._refresh_tickets_text() else: self._tickets_text = None # bit of spacing before the reset/sign-out section v -= 5 button_width = 250 if show_reset_progress_button: confirm_text = ( ba.Lstr(resource=self._r + '.resetProgressConfirmText') if self._can_reset_achievements else ba.Lstr( resource=self._r + '.resetProgressConfirmNoAchievementsText' ) ) v -= reset_progress_button_space self._reset_progress_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, size=(button_width, 60), label=ba.Lstr(resource=self._r + '.resetProgressText'), on_activate_call=lambda: confirm.ConfirmWindow( text=confirm_text, width=500, height=200, action=self._reset_progress, ), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn) self._linked_accounts_text: ba.Widget | None if show_linked_accounts_text: v -= linked_accounts_text_space * 0.8 self._linked_accounts_text = ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5, v), size=(0, 0), scale=0.9, color=(0.75, 0.7, 0.8), maxwidth=self._sub_width * 0.95, text=ba.Lstr(resource=self._r + '.linkedAccountsText'), h_align='center', v_align='center', ) v -= linked_accounts_text_space * 0.2 self._update_linked_accounts_text() else: self._linked_accounts_text = None # Show link/unlink buttons only for V1 accounts. if show_link_accounts_button: v -= link_accounts_button_space self._link_accounts_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), autoselect=True, size=(button_width, 60), label='', color=(0.55, 0.5, 0.6), on_activate_call=self._link_accounts_press, ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 17 + 20), text=ba.Lstr(resource=self._r + '.linkAccountsText'), maxwidth=button_width * 0.8, color=(0.75, 0.7, 0.8), ) ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v - 4 + 20), text=ba.Lstr(resource=self._r + '.linkAccountsInfoText'), flatness=1.0, scale=0.5, maxwidth=button_width * 0.8, color=(0.75, 0.7, 0.8), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._unlink_accounts_button: ba.Widget | None if show_unlink_accounts_button: v -= unlink_accounts_button_space self._unlink_accounts_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v + 25), autoselect=True, size=(button_width, 60), label='', color=(0.55, 0.5, 0.6), on_activate_call=self._unlink_accounts_press, ) self._unlink_accounts_button_label = ba.textwidget( parent=self._subcontainer, draw_controller=btn, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + 55), text=ba.Lstr(resource=self._r + '.unlinkAccountsText'), maxwidth=button_width * 0.8, color=(0.75, 0.7, 0.8), ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50) self._update_unlink_accounts_button() else: self._unlink_accounts_button = None if show_v2_link_info: v -= v2_link_info_space ba.textwidget( parent=self._subcontainer, h_align='center', v_align='center', size=(0, 0), position=(self._sub_width * 0.5, v + v2_link_info_space - 20), text=ba.Lstr(resource='v2AccountLinkingInfoText'), flatness=1.0, scale=0.8, maxwidth=450, color=(0.5, 0.45, 0.55), ) if self._show_legacy_unlink_button: v -= legacy_unlink_button_space button_width_w = button_width * 1.5 ba.textwidget( parent=self._subcontainer, position=(self._sub_width * 0.5 - 150.0, v + 75), size=(300.0, 60), text=ba.Lstr(resource='whatIsThisText'), scale=0.8, color=(0.3, 0.7, 0.05), maxwidth=200.0, h_align='center', v_align='center', autoselect=True, selectable=True, on_activate_call=show_what_is_legacy_unlinking_page, click_activate=True, ) btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width_w) * 0.5, v + 25), autoselect=True, size=(button_width_w, 60), label=ba.Lstr(resource=self._r + '.unlinkLegacyV1AccountsText'), textcolor=(0.8, 0.4, 0), color=(0.55, 0.5, 0.6), on_activate_call=self._unlink_accounts_press, ) if show_sign_out_button: v -= sign_out_button_space self._sign_out_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), label=ba.Lstr(resource=self._r + '.signOutText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, on_activate_call=self._sign_out_press, ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) if show_cancel_sign_in_button: v -= cancel_sign_in_button_space self._cancel_sign_in_button = btn = ba.buttonwidget( parent=self._subcontainer, position=((self._sub_width - button_width) * 0.5, v), size=(button_width, 60), label=ba.Lstr(resource='cancelText'), color=(0.55, 0.5, 0.6), textcolor=(0.75, 0.7, 0.8), autoselect=True, on_activate_call=self._cancel_sign_in_press, ) if first_selectable is None: first_selectable = btn if ba.app.ui.use_toolbars: ba.widget( edit=btn, right_widget=ba.internal.get_special_widget('party_button'), ) ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15) # Whatever the topmost selectable thing is, we want it to scroll all # the way up when we select it. if first_selectable is not None: ba.widget( edit=first_selectable, up_widget=bbtn, show_buffer_top=400 ) # (this should re-scroll us to the top..) ba.containerwidget( edit=self._subcontainer, visible_child=first_selectable ) self._needs_refresh = False def _on_custom_achievements_press(self) -> None: ba.timer( 0.15, ba.Call(ba.internal.show_online_score_ui, 'achievements'), timetype=ba.TimeType.REAL, ) def _on_achievements_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui import achievements assert self._achievements_button is not None achievements.AchievementsWindow( position=self._achievements_button.get_screen_space_center() ) def _on_what_is_v2_press(self) -> None: show_what_is_v2_page() def _on_manage_account_press(self) -> None: ba.screenmessage(ba.Lstr(resource='oneMomentText')) # We expect to have a v2 account signed in if we get here. if ba.app.accounts_v2.primary is None: logging.exception( 'got manage-account press without v2 account present' ) return with ba.app.accounts_v2.primary: ba.app.cloud.send_message_cb( bacommon.cloud.ManageAccountMessage(), on_response=ba.WeakCall(self._on_manage_account_response), ) def _on_manage_account_response( self, response: bacommon.cloud.ManageAccountResponse | Exception ) -> None: if isinstance(response, Exception) or response.url is None: ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) return ba.open_url(response.url) def _on_leaderboards_press(self) -> None: ba.timer( 0.15, ba.Call(ba.internal.show_online_score_ui, 'leaderboards'), timetype=ba.TimeType.REAL, ) def _have_unlinkable_v1_accounts(self) -> bool: # if this is not present, we haven't had contact from the server so # let's not proceed.. if ba.internal.get_public_login_id() is None: return False accounts = ba.internal.get_v1_account_misc_read_val_2( 'linkedAccounts', [] ) return len(accounts) > 1 def _update_unlink_accounts_button(self) -> None: if self._unlink_accounts_button is None: return if self._have_unlinkable_v1_accounts(): clr = (0.75, 0.7, 0.8, 1.0) else: clr = (1.0, 1.0, 1.0, 0.25) ba.textwidget(edit=self._unlink_accounts_button_label, color=clr) def _should_show_legacy_unlink_button(self) -> bool: # Only show this when fully signed in to a v2 account. if not self._v1_signed_in or ba.app.accounts_v2.primary is None: return False out = self._have_unlinkable_v1_accounts() return out def _update_linked_accounts_text(self) -> None: if self._linked_accounts_text is None: return # Disable this by default when signed in to a V2 account # (since this shows V1 links which we should no longer care about). if ( ba.app.accounts_v2.primary is not None and not FORCE_ENABLE_V1_LINKING ): return # if this is not present, we haven't had contact from the server so # let's not proceed.. if ba.internal.get_public_login_id() is None: num = int(time.time()) % 4 accounts_str = num * '.' + (4 - num) * ' ' else: accounts = ba.internal.get_v1_account_misc_read_val_2( 'linkedAccounts', [] ) # UPDATE - we now just print the number here; not the actual # accounts (they can see that in the unlink section if they're # curious) accounts_str = str(max(0, len(accounts) - 1)) ba.textwidget( edit=self._linked_accounts_text, text=ba.Lstr( value='${L} ${A}', subs=[ ('${L}', ba.Lstr(resource=self._r + '.linkedAccountsText')), ('${A}', accounts_str), ], ), ) def _refresh_campaign_progress_text(self) -> None: from ba.internal import getcampaign if self._campaign_progress_text is None: return p_str: str | ba.Lstr try: campaign = getcampaign('Default') levels = campaign.levels levels_complete = sum((1 if l.complete else 0) for l in levels) # Last level cant be completed; hence the -1; progress = min(1.0, float(levels_complete) / (len(levels) - 1)) p_str = ba.Lstr( resource=self._r + '.campaignProgressText', subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')], ) except Exception: p_str = '?' ba.print_exception('Error calculating co-op campaign progress.') ba.textwidget(edit=self._campaign_progress_text, text=p_str) def _refresh_tickets_text(self) -> None: if self._tickets_text is None: return try: tc_str = str(ba.internal.get_v1_account_ticket_count()) except Exception: ba.print_exception() tc_str = '-' ba.textwidget( edit=self._tickets_text, text=ba.Lstr( resource=self._r + '.ticketsText', subs=[('${COUNT}', tc_str)] ), ) def _refresh_account_name_text(self) -> None: if self._account_name_text is None: return try: name_str = ba.internal.get_v1_account_display_string() except Exception: ba.print_exception() name_str = '??' ba.textwidget(edit=self._account_name_text, text=name_str) if self._account_name_what_is_text is not None: swidth = ba.internal.get_string_width( name_str, suppress_warning=True ) # Eww; number-fudging. Need to recalibrate this if # account name scaling changes. x = self._sub_width * 0.5 - swidth * 0.75 - 170 ba.textwidget( edit=self._account_name_what_is_text, position=(x, self._account_name_what_is_y), ) def _refresh_achievements(self) -> None: if ( self._achievements_text is None and self._achievements_button is None ): return complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements) total = len(ba.app.ach.achievements) txt_final = ba.Lstr( resource=self._r + '.achievementProgressText', subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))], ) if self._achievements_text is not None: ba.textwidget(edit=self._achievements_text, text=txt_final) if self._achievements_button is not None: ba.buttonwidget(edit=self._achievements_button, label=txt_final) def _link_accounts_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account import link link.AccountLinkWindow(origin_widget=self._link_accounts_button) def _unlink_accounts_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account import unlink if not self._have_unlinkable_v1_accounts(): ba.playsound(ba.getsound('error')) return unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button) def _player_profiles_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.profile import browser as pbrowser self._save_state() ba.containerwidget(edit=self._root_widget, transition='out_left') pbrowser.ProfileBrowserWindow( origin_widget=self._player_profiles_button ) def _cancel_sign_in_press(self) -> None: # If we're waiting on an adapter to give us credentials, abort. self._signing_in_adapter = None # Say we don't wanna be signed in anymore if we are. ba.app.accounts_v2.set_primary_credentials(None) self._needs_refresh = True # Speed UI updates along. ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) def _sign_out_press(self) -> None: if ba.app.accounts_v2.have_primary_credentials(): if ( ba.app.accounts_v2.primary is not None and LoginType.GPGS in ba.app.accounts_v2.primary.logins ): self._explicitly_signed_out_of_gpgs = True ba.app.accounts_v2.set_primary_credentials(None) else: ba.internal.sign_out_v1() cfg = ba.app.config # Also take note that its our *explicit* intention to not be # signed in at this point (affects v1 accounts). cfg['Auto Account State'] = 'signed_out' cfg.commit() ba.buttonwidget( edit=self._sign_out_button, label=ba.Lstr(resource=self._r + '.signingOutText'), ) # Speed UI updates along. ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) def _sign_in_press(self, login_type: str | LoginType) -> None: # V1 login types are strings. if isinstance(login_type, str): ba.internal.sign_in_v1(login_type) # Make note of the type account we're *wanting* # to be signed in with. cfg = ba.app.config cfg['Auto Account State'] = login_type cfg.commit() self._needs_refresh = True ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) return # V2 login sign-in buttons generally go through adapters. adapter = ba.app.accounts_v2.login_adapters.get(login_type) if adapter is not None: self._signing_in_adapter = adapter adapter.sign_in( result_cb=ba.WeakCall(self._on_adapter_sign_in_result), description='account settings button', ) # Will get 'Signing in...' to show. self._needs_refresh = True ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) else: ba.screenmessage(f'Unsupported login_type: {login_type.name}') def _on_adapter_sign_in_result( self, adapter: LoginAdapter, result: LoginAdapter.SignInResult | Exception, ) -> None: is_us = self._signing_in_adapter is adapter # If this isn't our current one we don't care. if not is_us: return # If it is us, note that we're done. self._signing_in_adapter = None if isinstance(result, Exception): # For now just make a bit of noise if anything went wrong; # can get more specific as needed later. ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0)) ba.playsound(ba.getsound('error')) else: # Success! Plug in these credentials which will begin # verifying them and set our primary account-handle # when finished. ba.app.accounts_v2.set_primary_credentials(result.credentials) # Special case - if the user has explicitly logged out and # logged in again with GPGS via this button, warn them that # they need to use the app if they want to switch to a # different GPGS account. if ( self._explicitly_signed_out_of_gpgs and adapter.login_type is LoginType.GPGS ): # Delay this slightly so it hopefully pops up after # credentials go through and the account name shows up. ba.timer( 1.5, ba.Call( ba.screenmessage, ba.Lstr( resource=self._r + '.googlePlayGamesAccountSwitchText' ), ), ) # Speed any UI updates along. self._needs_refresh = True ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL) def _v2_proxy_sign_in_press(self) -> None: # pylint: disable=cyclic-import from bastd.ui.account.v2proxy import V2ProxySignInWindow assert self._sign_in_v2_proxy_button is not None V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button) def _reset_progress(self) -> None: try: from ba.internal import getcampaign # FIXME: This would need to happen server-side these days. if self._can_reset_achievements: ba.app.config['Achievements'] = {} ba.internal.reset_achievements() campaign = getcampaign('Default') campaign.reset() # also writes the config.. campaign = getcampaign('Challenges') campaign.reset() # also writes the config.. except Exception: ba.print_exception('Error resetting co-op campaign progress.') ba.playsound(ba.getsound('shieldDown')) self._refresh() def _back(self) -> None: # pylint: disable=cyclic-import from bastd.ui.mainmenu import MainMenuWindow self._save_state() ba.containerwidget( edit=self._root_widget, transition=self._transition_out ) if not self._modal: ba.app.ui.set_main_menu_window( MainMenuWindow(transition='in_left').get_root_widget() ) def _save_state(self) -> None: try: sel = self._root_widget.get_selected_child() if sel == self._back_button: sel_name = 'Back' elif sel == self._scrollwidget: sel_name = 'Scroll' else: raise ValueError('unrecognized selection') ba.app.ui.window_states[type(self)] = sel_name except Exception: ba.print_exception(f'Error saving state for {self}.') def _restore_state(self) -> None: try: sel_name = ba.app.ui.window_states.get(type(self)) if sel_name == 'Back': sel = self._back_button elif sel_name == 'Scroll': sel = self._scrollwidget else: sel = self._back_button ba.containerwidget(edit=self._root_widget, selected_child=sel) except Exception: ba.print_exception(f'Error restoring state for {self}.') def show_what_is_v2_page() -> None: """Show the webpage describing V2 accounts.""" bamasteraddr = ba.internal.get_master_server_address(version=2) ba.open_url(f'{bamasteraddr}/whatisv2') def show_what_is_legacy_unlinking_page() -> None: """Show the webpage describing legacy unlinking.""" bamasteraddr = ba.internal.get_master_server_address(version=2) ba.open_url(f'{bamasteraddr}/whatarev1links')