vh-bombsquad-modded-server-.../dist/ba_data/python/bastd/ui/account/settings.py
2024-06-06 19:50:58 +05:30

1540 lines
57 KiB
Python

# 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')