mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-14 17:46:03 +00:00
1.7.17 base update
This commit is contained in:
parent
e5034cbb6f
commit
0610d132f2
48 changed files with 2309 additions and 511 deletions
1
dist/ba_data/python/_bainternal.py
vendored
1
dist/ba_data/python/_bainternal.py
vendored
|
|
@ -252,7 +252,6 @@ def submit_score(
|
||||||
name: Any,
|
name: Any,
|
||||||
score: int | None,
|
score: int | None,
|
||||||
callback: Callable,
|
callback: Callable,
|
||||||
friend_callback: Callable | None,
|
|
||||||
order: str = 'increasing',
|
order: str = 'increasing',
|
||||||
tournament_id: str | None = None,
|
tournament_id: str | None = None,
|
||||||
score_type: str = 'points',
|
score_type: str = 'points',
|
||||||
|
|
|
||||||
6
dist/ba_data/python/ba/__init__.py
vendored
6
dist/ba_data/python/ba/__init__.py
vendored
|
|
@ -43,7 +43,8 @@ from _ba import (
|
||||||
newnode,
|
newnode,
|
||||||
playsound,
|
playsound,
|
||||||
printnodes,
|
printnodes,
|
||||||
printobjects,
|
ls_objects,
|
||||||
|
ls_input_devices,
|
||||||
pushcall,
|
pushcall,
|
||||||
quit,
|
quit,
|
||||||
rowwidget,
|
rowwidget,
|
||||||
|
|
@ -316,7 +317,8 @@ __all__ = [
|
||||||
'print_error',
|
'print_error',
|
||||||
'print_exception',
|
'print_exception',
|
||||||
'printnodes',
|
'printnodes',
|
||||||
'printobjects',
|
'ls_objects',
|
||||||
|
'ls_input_devices',
|
||||||
'pushcall',
|
'pushcall',
|
||||||
'quit',
|
'quit',
|
||||||
'rowwidget',
|
'rowwidget',
|
||||||
|
|
|
||||||
3
dist/ba_data/python/ba/_accountv1.py
vendored
3
dist/ba_data/python/ba/_accountv1.py
vendored
|
|
@ -48,6 +48,9 @@ class AccountV1Subsystem:
|
||||||
|
|
||||||
_ba.pushcall(do_auto_sign_in)
|
_ba.pushcall(do_auto_sign_in)
|
||||||
|
|
||||||
|
def on_app_pause(self) -> None:
|
||||||
|
"""Should be called when app is pausing."""
|
||||||
|
|
||||||
def on_app_resume(self) -> None:
|
def on_app_resume(self) -> None:
|
||||||
"""Should be called when the app is resumed."""
|
"""Should be called when the app is resumed."""
|
||||||
|
|
||||||
|
|
|
||||||
301
dist/ba_data/python/ba/_accountv2.py
vendored
301
dist/ba_data/python/ba/_accountv2.py
vendored
|
|
@ -4,13 +4,23 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from efro.call import tpartial
|
||||||
|
from efro.error import CommunicationError
|
||||||
|
from bacommon.login import LoginType
|
||||||
import _ba
|
import _ba
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from ba._login import LoginAdapter
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_LOG = False
|
||||||
|
|
||||||
|
|
||||||
class AccountV2Subsystem:
|
class AccountV2Subsystem:
|
||||||
"""Subsystem for modern account handling in the app.
|
"""Subsystem for modern account handling in the app.
|
||||||
|
|
@ -30,9 +40,23 @@ class AccountV2Subsystem:
|
||||||
|
|
||||||
self._kicked_off_workspace_load = False
|
self._kicked_off_workspace_load = False
|
||||||
|
|
||||||
|
self.login_adapters: dict[LoginType, LoginAdapter] = {}
|
||||||
|
|
||||||
|
self._implicit_signed_in_adapter: LoginAdapter | None = None
|
||||||
|
self._implicit_state_changed = False
|
||||||
|
self._can_do_auto_sign_in = True
|
||||||
|
|
||||||
|
if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
|
||||||
|
from ba._login import LoginAdapterGPGS
|
||||||
|
|
||||||
|
self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
|
||||||
|
|
||||||
def on_app_launch(self) -> None:
|
def on_app_launch(self) -> None:
|
||||||
"""Should be called at standard on_app_launch time."""
|
"""Should be called at standard on_app_launch time."""
|
||||||
|
|
||||||
|
for adapter in self.login_adapters.values():
|
||||||
|
adapter.on_app_launch()
|
||||||
|
|
||||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||||
"""Set credentials for the primary app account."""
|
"""Set credentials for the primary app account."""
|
||||||
raise RuntimeError('This should be overridden.')
|
raise RuntimeError('This should be overridden.')
|
||||||
|
|
@ -49,7 +73,7 @@ class AccountV2Subsystem:
|
||||||
@property
|
@property
|
||||||
def primary(self) -> AccountV2Handle | None:
|
def primary(self) -> AccountV2Handle | None:
|
||||||
"""The primary account for the app, or None if not logged in."""
|
"""The primary account for the app, or None if not logged in."""
|
||||||
return None
|
return self.do_get_primary()
|
||||||
|
|
||||||
def do_get_primary(self) -> AccountV2Handle | None:
|
def do_get_primary(self) -> AccountV2Handle | None:
|
||||||
"""Internal - should be overridden by subclass."""
|
"""Internal - should be overridden by subclass."""
|
||||||
|
|
@ -60,9 +84,11 @@ class AccountV2Subsystem:
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Callback run after the primary account changes.
|
"""Callback run after the primary account changes.
|
||||||
|
|
||||||
Will be called with None on log-outs or when new credentials
|
Will be called with None on log-outs and when new credentials
|
||||||
are set but have not yet been verified.
|
are set but have not yet been verified.
|
||||||
"""
|
"""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
# Currently don't do anything special on sign-outs.
|
# Currently don't do anything special on sign-outs.
|
||||||
if account is None:
|
if account is None:
|
||||||
return
|
return
|
||||||
|
|
@ -99,6 +125,30 @@ class AccountV2Subsystem:
|
||||||
self._initial_login_completed = True
|
self._initial_login_completed = True
|
||||||
_ba.app.on_initial_login_completed()
|
_ba.app.on_initial_login_completed()
|
||||||
|
|
||||||
|
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
|
||||||
|
"""Should be called when logins for the active account change."""
|
||||||
|
|
||||||
|
for adapter in self.login_adapters.values():
|
||||||
|
adapter.set_active_logins(logins)
|
||||||
|
|
||||||
|
def on_implicit_sign_in(
|
||||||
|
self, login_type: LoginType, login_id: str, display_name: str
|
||||||
|
) -> None:
|
||||||
|
"""An implicit sign-in happened (called by native layer)."""
|
||||||
|
from ba._login import LoginAdapter
|
||||||
|
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
self.login_adapters[login_type].set_implicit_login_state(
|
||||||
|
LoginAdapter.ImplicitLoginState(
|
||||||
|
login_id=login_id, display_name=display_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_implicit_sign_out(self, login_type: LoginType) -> None:
|
||||||
|
"""An implicit sign-out happened (called by native layer)."""
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
self.login_adapters[login_type].set_implicit_login_state(None)
|
||||||
|
|
||||||
def on_no_initial_primary_account(self) -> None:
|
def on_no_initial_primary_account(self) -> None:
|
||||||
"""Callback run if the app has no primary account after launch.
|
"""Callback run if the app has no primary account after launch.
|
||||||
|
|
||||||
|
|
@ -110,6 +160,240 @@ class AccountV2Subsystem:
|
||||||
self._initial_login_completed = True
|
self._initial_login_completed = True
|
||||||
_ba.app.on_initial_login_completed()
|
_ba.app.on_initial_login_completed()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hashstr(val: str) -> str:
|
||||||
|
md5 = hashlib.md5()
|
||||||
|
md5.update(val.encode())
|
||||||
|
return md5.hexdigest()
|
||||||
|
|
||||||
|
def on_implicit_login_state_changed(
|
||||||
|
self,
|
||||||
|
login_type: LoginType,
|
||||||
|
state: LoginAdapter.ImplicitLoginState | None,
|
||||||
|
) -> None:
|
||||||
|
"""Called when implicit login state changes.
|
||||||
|
|
||||||
|
Login systems that tend to sign themselves in/out in the
|
||||||
|
background are considered implicit. We may choose to honor or
|
||||||
|
ignore their states, allowing the user to opt for other login
|
||||||
|
types even if the default implicit one can't be explicitly
|
||||||
|
logged out or otherwise controlled.
|
||||||
|
"""
|
||||||
|
from ba._language import Lstr
|
||||||
|
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
|
cfg = _ba.app.config
|
||||||
|
cfgkey = 'ImplicitLoginStates'
|
||||||
|
cfgdict = _ba.app.config.setdefault(cfgkey, {})
|
||||||
|
|
||||||
|
# Store which (if any) adapter is currently implicitly signed in.
|
||||||
|
# Making the assumption there will only ever be one implicit
|
||||||
|
# adapter at a time; may need to update this if that changes.
|
||||||
|
prev_state = cfgdict.get(login_type.value)
|
||||||
|
if state is None:
|
||||||
|
self._implicit_signed_in_adapter = None
|
||||||
|
new_state = cfgdict[login_type.value] = None
|
||||||
|
else:
|
||||||
|
self._implicit_signed_in_adapter = self.login_adapters[login_type]
|
||||||
|
new_state = cfgdict[login_type.value] = self._hashstr(
|
||||||
|
state.login_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Special case: if the user is already signed in but not with
|
||||||
|
# this implicit login, we may want to let them know that the
|
||||||
|
# 'Welcome back FOO' they likely just saw is not actually
|
||||||
|
# accurate.
|
||||||
|
if (
|
||||||
|
self.primary is not None
|
||||||
|
and not self.login_adapters[login_type].is_back_end_active()
|
||||||
|
):
|
||||||
|
if login_type is LoginType.GPGS:
|
||||||
|
service_str = Lstr(resource='googlePlayText')
|
||||||
|
else:
|
||||||
|
service_str = None
|
||||||
|
if service_str is not None:
|
||||||
|
_ba.timer(
|
||||||
|
2.0,
|
||||||
|
tpartial(
|
||||||
|
_ba.screenmessage,
|
||||||
|
Lstr(
|
||||||
|
resource='notUsingAccountText',
|
||||||
|
subs=[
|
||||||
|
('${ACCOUNT}', state.display_name),
|
||||||
|
('${SERVICE}', service_str),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(1, 0.5, 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg.commit()
|
||||||
|
|
||||||
|
# We want to respond any time the implicit state changes;
|
||||||
|
# generally this means the user has explicitly signed in/out or
|
||||||
|
# switched accounts within that back-end.
|
||||||
|
if prev_state != new_state:
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'AccountV2: Implicit state changed (%s -> %s);'
|
||||||
|
' will update app sign-in state accordingly.',
|
||||||
|
prev_state,
|
||||||
|
new_state,
|
||||||
|
)
|
||||||
|
self._implicit_state_changed = True
|
||||||
|
|
||||||
|
# We may want to auto-sign-in based on this new state.
|
||||||
|
self._update_auto_sign_in()
|
||||||
|
|
||||||
|
def on_cloud_connectivity_changed(self, connected: bool) -> None:
|
||||||
|
"""Should be called with cloud connectivity changes."""
|
||||||
|
del connected # Unused.
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
|
# We may want to auto-sign-in based on this new state.
|
||||||
|
self._update_auto_sign_in()
|
||||||
|
|
||||||
|
def _update_auto_sign_in(self) -> None:
|
||||||
|
from ba._internal import get_v1_account_state
|
||||||
|
|
||||||
|
# If implicit state has changed, try to respond.
|
||||||
|
if self._implicit_state_changed:
|
||||||
|
if self._implicit_signed_in_adapter is None:
|
||||||
|
# If implicit back-end is signed out, follow suit
|
||||||
|
# immediately; no need to wait for network connectivity.
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'AccountV2: Signing out as result'
|
||||||
|
' of implicit state change...',
|
||||||
|
)
|
||||||
|
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||||
|
self._implicit_state_changed = False
|
||||||
|
|
||||||
|
# Once we've made a move here we don't want to
|
||||||
|
# do any more automatic ones.
|
||||||
|
self._can_do_auto_sign_in = False
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Ok; we've got a new implicit state. If we've got
|
||||||
|
# connectivity, let's attempt to sign in with it.
|
||||||
|
# Consider this an 'explicit' sign in because the
|
||||||
|
# implicit-login state change presumably was triggered
|
||||||
|
# by some user action (signing in, signing out, or
|
||||||
|
# switching accounts via the back-end).
|
||||||
|
# NOTE: should test case where we don't have
|
||||||
|
# connectivity here.
|
||||||
|
if _ba.app.cloud.is_connected():
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'AccountV2: Signing in as result'
|
||||||
|
' of implicit state change...',
|
||||||
|
)
|
||||||
|
self._implicit_signed_in_adapter.sign_in(
|
||||||
|
self._on_explicit_sign_in_completed
|
||||||
|
)
|
||||||
|
self._implicit_state_changed = False
|
||||||
|
|
||||||
|
# Once we've made a move here we don't want to
|
||||||
|
# do any more automatic ones.
|
||||||
|
self._can_do_auto_sign_in = False
|
||||||
|
|
||||||
|
if not self._can_do_auto_sign_in:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we're not currently signed in, we have connectivity, and
|
||||||
|
# we have an available implicit login, auto-sign-in with it.
|
||||||
|
# The implicit-state-change logic above should keep things
|
||||||
|
# mostly in-sync, but due to connectivity or other issues that
|
||||||
|
# might not always be the case. We prefer to keep people signed
|
||||||
|
# in as a rule, even if there are corner cases where this might
|
||||||
|
# not be what they want (A user signing out and then restarting
|
||||||
|
# may be auto-signed back in).
|
||||||
|
connected = _ba.app.cloud.is_connected()
|
||||||
|
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||||
|
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||||
|
if (
|
||||||
|
connected
|
||||||
|
and not signed_in_v1
|
||||||
|
and not signed_in_v2
|
||||||
|
and self._implicit_signed_in_adapter is not None
|
||||||
|
):
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'AccountV2: Signing in due to on-launch-auto-sign-in...',
|
||||||
|
)
|
||||||
|
self._can_do_auto_sign_in = False # Only ATTEMPT once
|
||||||
|
self._implicit_signed_in_adapter.sign_in(
|
||||||
|
self._on_implicit_sign_in_completed
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_explicit_sign_in_completed(
|
||||||
|
self,
|
||||||
|
adapter: LoginAdapter,
|
||||||
|
result: LoginAdapter.SignInResult | Exception,
|
||||||
|
) -> None:
|
||||||
|
"""A sign-in has completed that the user asked for explicitly."""
|
||||||
|
from ba._language import Lstr
|
||||||
|
|
||||||
|
del adapter # Unused.
|
||||||
|
|
||||||
|
# Make some noise on errors since the user knows
|
||||||
|
# a sign-in attempt is happening in this case.
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
# We expect the occasional communication errors;
|
||||||
|
# Log a full exception for anything else though.
|
||||||
|
if not isinstance(result, CommunicationError):
|
||||||
|
logging.warning(
|
||||||
|
'Error on explicit accountv2 sign in attempt.',
|
||||||
|
exc_info=result,
|
||||||
|
)
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
_ba.screenmessage(
|
||||||
|
Lstr(resource='internal.signInErrorText'),
|
||||||
|
color=(1, 0, 0),
|
||||||
|
)
|
||||||
|
_ba.playsound(_ba.getsound('error'))
|
||||||
|
|
||||||
|
# Also I suppose we should sign them out in this case since
|
||||||
|
# it could be misleading to be still signed in with the old
|
||||||
|
# account.
|
||||||
|
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||||
|
|
||||||
|
def _on_implicit_sign_in_completed(
|
||||||
|
self,
|
||||||
|
adapter: LoginAdapter,
|
||||||
|
result: LoginAdapter.SignInResult | Exception,
|
||||||
|
) -> None:
|
||||||
|
"""A sign-in has completed that the user didn't ask for explicitly."""
|
||||||
|
from ba._internal import get_v1_account_state
|
||||||
|
|
||||||
|
del adapter # Unused.
|
||||||
|
|
||||||
|
# Log errors but don't inform the user; they're not aware of this
|
||||||
|
# attempt and ignorance is bliss.
|
||||||
|
if isinstance(result, Exception):
|
||||||
|
# We expect the occasional communication errors;
|
||||||
|
# Log a full exception for anything else though.
|
||||||
|
if not isinstance(result, CommunicationError):
|
||||||
|
logging.warning(
|
||||||
|
'Error on implicit accountv2 sign in attempt.',
|
||||||
|
exc_info=result,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we're still connected and still not signed in,
|
||||||
|
# plug in the credentials we got. We want to be extra cautious
|
||||||
|
# in case the user has since explicitly signed in since we
|
||||||
|
# kicked off.
|
||||||
|
connected = _ba.app.cloud.is_connected()
|
||||||
|
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||||
|
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||||
|
if connected and not signed_in_v1 and not signed_in_v2:
|
||||||
|
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||||
|
|
||||||
def _on_set_active_workspace_completed(self) -> None:
|
def _on_set_active_workspace_completed(self) -> None:
|
||||||
if not self._initial_login_completed:
|
if not self._initial_login_completed:
|
||||||
self._initial_login_completed = True
|
self._initial_login_completed = True
|
||||||
|
|
@ -129,8 +413,17 @@ class AccountV2Handle:
|
||||||
self.workspacename: str | None = None
|
self.workspacename: str | None = None
|
||||||
self.workspaceid: str | None = None
|
self.workspaceid: str | None = None
|
||||||
|
|
||||||
|
# Login types and their display-names associated with this account.
|
||||||
|
self.logins: dict[LoginType, str] = {}
|
||||||
|
|
||||||
def __enter__(self) -> None:
|
def __enter__(self) -> None:
|
||||||
"""Support for "with" statement."""
|
"""Support for "with" statement.
|
||||||
|
|
||||||
|
This allows cloud messages to be sent on our behalf.
|
||||||
|
"""
|
||||||
|
|
||||||
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
|
||||||
"""Support for "with" statement."""
|
"""Support for "with" statement.
|
||||||
|
|
||||||
|
This allows cloud messages to be sent on our behalf.
|
||||||
|
"""
|
||||||
|
|
|
||||||
2
dist/ba_data/python/ba/_achievement.py
vendored
2
dist/ba_data/python/ba/_achievement.py
vendored
|
|
@ -697,7 +697,7 @@ class Achievement:
|
||||||
# Even though there are technically achievements when we're not
|
# Even though there are technically achievements when we're not
|
||||||
# signed in, lets not show them (otherwise we tend to get
|
# signed in, lets not show them (otherwise we tend to get
|
||||||
# confusing 'controller connected' achievements popping up while
|
# confusing 'controller connected' achievements popping up while
|
||||||
# waiting to log in which can be confusing).
|
# waiting to sign in which can be confusing).
|
||||||
if _internal.get_v1_account_state() != 'signed_in':
|
if _internal.get_v1_account_state() != 'signed_in':
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
41
dist/ba_data/python/ba/_app.py
vendored
41
dist/ba_data/python/ba/_app.py
vendored
|
|
@ -32,6 +32,7 @@ if TYPE_CHECKING:
|
||||||
from bastd.actor import spazappearance
|
from bastd.actor import spazappearance
|
||||||
from ba._accountv2 import AccountV2Subsystem
|
from ba._accountv2 import AccountV2Subsystem
|
||||||
from ba._level import Level
|
from ba._level import Level
|
||||||
|
from ba._apputils import AppHealthMonitor
|
||||||
|
|
||||||
|
|
||||||
class App:
|
class App:
|
||||||
|
|
@ -50,7 +51,9 @@ class App:
|
||||||
# Implementations for these will be filled in by internal libs.
|
# Implementations for these will be filled in by internal libs.
|
||||||
accounts_v2: AccountV2Subsystem
|
accounts_v2: AccountV2Subsystem
|
||||||
cloud: CloudSubsystem
|
cloud: CloudSubsystem
|
||||||
|
|
||||||
log_handler: efro.log.LogHandler
|
log_handler: efro.log.LogHandler
|
||||||
|
health_monitor: AppHealthMonitor
|
||||||
|
|
||||||
class State(Enum):
|
class State(Enum):
|
||||||
"""High level state the app can be in."""
|
"""High level state the app can be in."""
|
||||||
|
|
@ -346,7 +349,6 @@ class App:
|
||||||
# pylint: disable=cyclic-import
|
# pylint: disable=cyclic-import
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
from ba import _asyncio
|
from ba import _asyncio
|
||||||
from ba import _apputils
|
|
||||||
from ba import _appconfig
|
from ba import _appconfig
|
||||||
from ba import _map
|
from ba import _map
|
||||||
from ba import _campaign
|
from ba import _campaign
|
||||||
|
|
@ -354,10 +356,16 @@ class App:
|
||||||
from bastd import maps as stdmaps
|
from bastd import maps as stdmaps
|
||||||
from bastd.actor import spazappearance
|
from bastd.actor import spazappearance
|
||||||
from ba._generated.enums import TimeType
|
from ba._generated.enums import TimeType
|
||||||
|
from ba._apputils import (
|
||||||
|
log_dumped_app_state,
|
||||||
|
handle_leftover_v1_cloud_log_file,
|
||||||
|
AppHealthMonitor,
|
||||||
|
)
|
||||||
|
|
||||||
assert _ba.in_logic_thread()
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
self._aioloop = _asyncio.setup_asyncio()
|
self._aioloop = _asyncio.setup_asyncio()
|
||||||
|
self.health_monitor = AppHealthMonitor()
|
||||||
|
|
||||||
cfg = self.config
|
cfg = self.config
|
||||||
|
|
||||||
|
|
@ -401,15 +409,15 @@ class App:
|
||||||
|
|
||||||
# If there's a leftover log file, attempt to upload it to the
|
# If there's a leftover log file, attempt to upload it to the
|
||||||
# master-server and/or get rid of it.
|
# master-server and/or get rid of it.
|
||||||
_apputils.handle_leftover_v1_cloud_log_file()
|
handle_leftover_v1_cloud_log_file()
|
||||||
|
|
||||||
# Only do this stuff if our config file is healthy so we don't
|
# Only do this stuff if our config file is healthy so we don't
|
||||||
# overwrite a broken one or whatnot and wipe out data.
|
# overwrite a broken one or whatnot and wipe out data.
|
||||||
if not self.config_file_healthy:
|
if not self.config_file_healthy:
|
||||||
if self.platform in ('mac', 'linux', 'windows'):
|
if self.platform in ('mac', 'linux', 'windows'):
|
||||||
from bastd.ui import configerror
|
from bastd.ui.configerror import ConfigErrorWindow
|
||||||
|
|
||||||
configerror.ConfigErrorWindow()
|
_ba.pushcall(ConfigErrorWindow)
|
||||||
return
|
return
|
||||||
|
|
||||||
# For now on other systems we just overwrite the bum config.
|
# For now on other systems we just overwrite the bum config.
|
||||||
|
|
@ -459,6 +467,9 @@ class App:
|
||||||
'on_app_launch found state %s; expected LAUNCHING.', self.state
|
'on_app_launch found state %s; expected LAUNCHING.', self.state
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If any traceback dumps happened last run, log and clear them.
|
||||||
|
log_dumped_app_state()
|
||||||
|
|
||||||
self._launch_completed = True
|
self._launch_completed = True
|
||||||
self._update_state()
|
self._update_state()
|
||||||
|
|
||||||
|
|
@ -483,8 +494,23 @@ class App:
|
||||||
assert _ba.in_logic_thread()
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
if self._app_paused:
|
if self._app_paused:
|
||||||
|
# Entering paused state:
|
||||||
|
if self.state is not self.State.PAUSED:
|
||||||
self.state = self.State.PAUSED
|
self.state = self.State.PAUSED
|
||||||
|
self.cloud.on_app_pause()
|
||||||
|
self.accounts_v1.on_app_pause()
|
||||||
|
self.plugins.on_app_pause()
|
||||||
|
self.health_monitor.on_app_pause()
|
||||||
else:
|
else:
|
||||||
|
# Leaving paused state:
|
||||||
|
if self.state is self.State.PAUSED:
|
||||||
|
self.fg_state += 1
|
||||||
|
self.cloud.on_app_resume()
|
||||||
|
self.accounts_v1.on_app_resume()
|
||||||
|
self.music.on_app_resume()
|
||||||
|
self.plugins.on_app_resume()
|
||||||
|
self.health_monitor.on_app_resume()
|
||||||
|
|
||||||
if self._initial_login_completed and self._meta_scan_completed:
|
if self._initial_login_completed and self._meta_scan_completed:
|
||||||
self.state = self.State.RUNNING
|
self.state = self.State.RUNNING
|
||||||
if not self._called_on_app_running:
|
if not self._called_on_app_running:
|
||||||
|
|
@ -498,19 +524,16 @@ class App:
|
||||||
def on_app_pause(self) -> None:
|
def on_app_pause(self) -> None:
|
||||||
"""Called when the app goes to a suspended state."""
|
"""Called when the app goes to a suspended state."""
|
||||||
|
|
||||||
|
assert not self._app_paused # Should avoid redundant calls.
|
||||||
self._app_paused = True
|
self._app_paused = True
|
||||||
self._update_state()
|
self._update_state()
|
||||||
self.plugins.on_app_pause()
|
|
||||||
|
|
||||||
def on_app_resume(self) -> None:
|
def on_app_resume(self) -> None:
|
||||||
"""Run when the app resumes from a suspended state."""
|
"""Run when the app resumes from a suspended state."""
|
||||||
|
|
||||||
|
assert self._app_paused # Should avoid redundant calls.
|
||||||
self._app_paused = False
|
self._app_paused = False
|
||||||
self._update_state()
|
self._update_state()
|
||||||
self.fg_state += 1
|
|
||||||
self.accounts_v1.on_app_resume()
|
|
||||||
self.music.on_app_resume()
|
|
||||||
self.plugins.on_app_resume()
|
|
||||||
|
|
||||||
def on_app_shutdown(self) -> None:
|
def on_app_shutdown(self) -> None:
|
||||||
"""(internal)"""
|
"""(internal)"""
|
||||||
|
|
|
||||||
266
dist/ba_data/python/ba/_apputils.py
vendored
266
dist/ba_data/python/ba/_apputils.py
vendored
|
|
@ -5,12 +5,17 @@ from __future__ import annotations
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
from threading import Thread
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from efro.log import LogLevel
|
||||||
|
from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
|
||||||
import _ba
|
import _ba
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any
|
from typing import Any, TextIO
|
||||||
import ba
|
import ba
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -260,3 +265,262 @@ def print_corrupt_file_error() -> None:
|
||||||
_ba.timer(
|
_ba.timer(
|
||||||
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
|
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_tbfiles: list[TextIO] = []
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class DumpedAppStateMetadata:
|
||||||
|
"""High level info about a dumped app state."""
|
||||||
|
|
||||||
|
reason: str
|
||||||
|
app_time: float
|
||||||
|
log_level: LogLevel
|
||||||
|
|
||||||
|
|
||||||
|
def dump_app_state(
|
||||||
|
delay: float = 0.0,
|
||||||
|
reason: str = 'Unspecified',
|
||||||
|
log_level: LogLevel = LogLevel.WARNING,
|
||||||
|
) -> None:
|
||||||
|
"""Dump various app state for debugging purposes.
|
||||||
|
|
||||||
|
This includes stack traces for all Python threads (and potentially
|
||||||
|
other info in the future).
|
||||||
|
|
||||||
|
This is intended for use debugging deadlock situations. It will dump
|
||||||
|
to preset file location(s) in the app config dir, and will attempt to
|
||||||
|
log and clear the results after dumping. If that should fail (due to
|
||||||
|
a hung app, etc.), then the results will be logged and cleared on the
|
||||||
|
next app run.
|
||||||
|
|
||||||
|
Do not use this call during regular smooth operation of the app; it
|
||||||
|
is should only be used for debugging or in response to confirmed
|
||||||
|
problems as it can leak file descriptors, cause hitches, etc.
|
||||||
|
"""
|
||||||
|
# pylint: disable=consider-using-with
|
||||||
|
import faulthandler
|
||||||
|
from ba._generated.enums import TimeType
|
||||||
|
|
||||||
|
# Dump our metadata immediately. If a delay is passed, it generally
|
||||||
|
# means we expect things to hang momentarily, so we should not delay
|
||||||
|
# writing our metadata or it will likely not happen. Though we
|
||||||
|
# should remember that metadata doesn't line up perfectly in time with
|
||||||
|
# the dump in that case.
|
||||||
|
try:
|
||||||
|
mdpath = os.path.join(
|
||||||
|
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||||
|
)
|
||||||
|
with open(mdpath, 'w', encoding='utf-8') as outfile:
|
||||||
|
outfile.write(
|
||||||
|
dataclass_to_json(
|
||||||
|
DumpedAppStateMetadata(
|
||||||
|
reason=reason,
|
||||||
|
app_time=_ba.time(TimeType.REAL),
|
||||||
|
log_level=log_level,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
# Abandon whole dump if we can't write metadata.
|
||||||
|
logging.exception('Error writing app state dump metadata.')
|
||||||
|
return
|
||||||
|
|
||||||
|
tbpath = os.path.join(
|
||||||
|
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||||
|
)
|
||||||
|
|
||||||
|
# faulthandler needs the raw file descriptor to still be valid when
|
||||||
|
# it fires, so stuff this into a global var to make sure it doesn't get
|
||||||
|
# cleaned up.
|
||||||
|
tbfile = open(tbpath, 'w', encoding='utf-8')
|
||||||
|
_tbfiles.append(tbfile)
|
||||||
|
|
||||||
|
if delay > 0.0:
|
||||||
|
faulthandler.dump_traceback_later(delay, file=tbfile)
|
||||||
|
else:
|
||||||
|
faulthandler.dump_traceback(file=tbfile)
|
||||||
|
|
||||||
|
# Attempt to log shortly after dumping.
|
||||||
|
# Allow sufficient time since we don't know how long the dump takes.
|
||||||
|
# We want this to work from any thread, so need to kick this part
|
||||||
|
# over to the logic thread so timer works.
|
||||||
|
_ba.pushcall(
|
||||||
|
lambda: _ba.timer(
|
||||||
|
delay + 1.0, log_dumped_app_state, timetype=TimeType.REAL
|
||||||
|
),
|
||||||
|
from_other_thread=True,
|
||||||
|
suppress_other_thread_warning=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def log_dumped_app_state() -> None:
|
||||||
|
"""If an app-state dump exists, log it and clear it. No-op otherwise."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
out = ''
|
||||||
|
mdpath = os.path.join(
|
||||||
|
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||||
|
)
|
||||||
|
if os.path.exists(mdpath):
|
||||||
|
with open(mdpath, 'r', encoding='utf-8') as infile:
|
||||||
|
metadata = dataclass_from_json(
|
||||||
|
DumpedAppStateMetadata, infile.read()
|
||||||
|
)
|
||||||
|
os.unlink(mdpath)
|
||||||
|
out += (
|
||||||
|
f'App state dump:\nReason: {metadata.reason}\n'
|
||||||
|
f'Time: {metadata.app_time:.2f}'
|
||||||
|
)
|
||||||
|
tbpath = os.path.join(
|
||||||
|
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||||
|
)
|
||||||
|
if os.path.exists(tbpath):
|
||||||
|
with open(tbpath, 'r', encoding='utf-8') as infile:
|
||||||
|
out += '\nPython tracebacks:\n' + infile.read()
|
||||||
|
os.unlink(tbpath)
|
||||||
|
logging.log(metadata.log_level.python_logging_level, out)
|
||||||
|
except Exception:
|
||||||
|
logging.exception('Error logging dumped app state.')
|
||||||
|
|
||||||
|
|
||||||
|
class AppHealthMonitor:
|
||||||
|
"""Logs things like app-not-responding issues."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
self._running = True
|
||||||
|
self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
|
||||||
|
self._thread.start()
|
||||||
|
self._response = False
|
||||||
|
self._first_check = True
|
||||||
|
|
||||||
|
def _app_monitor_thread_main(self) -> None:
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._monitor_app()
|
||||||
|
except Exception:
|
||||||
|
logging.exception('Error in AppHealthMonitor thread.')
|
||||||
|
|
||||||
|
def _set_response(self) -> None:
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
self._response = True
|
||||||
|
|
||||||
|
def _check_running(self) -> bool:
|
||||||
|
# Workaround for the fact that mypy assumes _running
|
||||||
|
# doesn't change during the course of a function.
|
||||||
|
return self._running
|
||||||
|
|
||||||
|
def _monitor_app(self) -> None:
|
||||||
|
import time
|
||||||
|
|
||||||
|
while bool(True):
|
||||||
|
|
||||||
|
# Always sleep a bit between checks.
|
||||||
|
time.sleep(1.234)
|
||||||
|
|
||||||
|
# Do nothing while backgrounded.
|
||||||
|
while not self._running:
|
||||||
|
time.sleep(2.3456)
|
||||||
|
|
||||||
|
# Wait for the logic thread to run something we send it.
|
||||||
|
starttime = time.monotonic()
|
||||||
|
self._response = False
|
||||||
|
_ba.pushcall(self._set_response, raw=True)
|
||||||
|
while not self._response:
|
||||||
|
|
||||||
|
# Abort this check if we went into the background.
|
||||||
|
if not self._check_running():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Wait a bit longer the first time through since the app
|
||||||
|
# could still be starting up; we generally don't want to
|
||||||
|
# report that.
|
||||||
|
threshold = 10 if self._first_check else 5
|
||||||
|
|
||||||
|
# If we've been waiting too long (and the app is running)
|
||||||
|
# dump the app state and bail. Make an exception for the
|
||||||
|
# first check though since the app could just be taking
|
||||||
|
# a while to get going; we don't want to report that.
|
||||||
|
duration = time.monotonic() - starttime
|
||||||
|
if duration > threshold:
|
||||||
|
dump_app_state(
|
||||||
|
reason=f'Logic thread unresponsive'
|
||||||
|
f' for {threshold} seconds.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# We just do one alert for now.
|
||||||
|
return
|
||||||
|
|
||||||
|
time.sleep(1.042)
|
||||||
|
|
||||||
|
self._first_check = False
|
||||||
|
|
||||||
|
def on_app_pause(self) -> None:
|
||||||
|
"""Should be called when the app pauses."""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def on_app_resume(self) -> None:
|
||||||
|
"""Should be called when the app resumes."""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
self._running = True
|
||||||
|
|
||||||
|
|
||||||
|
def on_too_many_file_descriptors() -> None:
|
||||||
|
"""Called when too many file descriptors are open; trying to debug."""
|
||||||
|
from ba._generated.enums import TimeType
|
||||||
|
|
||||||
|
real_time = _ba.time(TimeType.REAL)
|
||||||
|
|
||||||
|
def _do_log() -> None:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
pid = os.getpid()
|
||||||
|
out = f'TOO MANY FDS at {real_time}.\nWe are pid {pid}\n'
|
||||||
|
|
||||||
|
out += (
|
||||||
|
'FD Count: '
|
||||||
|
+ subprocess.run(
|
||||||
|
f'ls -l /proc/{pid}/fd | wc -l',
|
||||||
|
shell=True,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
).stdout.decode()
|
||||||
|
+ '\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
out += (
|
||||||
|
'lsof output:\n'
|
||||||
|
+ subprocess.run(
|
||||||
|
f'lsof -p {pid}',
|
||||||
|
shell=True,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
).stdout.decode()
|
||||||
|
+ '\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.warning(out)
|
||||||
|
|
||||||
|
Thread(target=_do_log, daemon=True).start()
|
||||||
|
|
||||||
|
# import io
|
||||||
|
# from efro.debug import printtypes
|
||||||
|
|
||||||
|
# with io.StringIO() as fstr:
|
||||||
|
# fstr.write('Too many FDs.\n')
|
||||||
|
# printtypes(file=fstr)
|
||||||
|
# fstr.seek(0)
|
||||||
|
# logging.warning(fstr.read())
|
||||||
|
# import socket
|
||||||
|
|
||||||
|
# objs: list[Any] = []
|
||||||
|
# for obj in gc.get_objects():
|
||||||
|
# if isinstance(obj, socket.socket):
|
||||||
|
# objs.append(obj)
|
||||||
|
# test = open('/Users/ericf/.zshrc', 'r', encoding='utf-8')
|
||||||
|
# reveal_type(test)
|
||||||
|
# print('FOUND', len(objs))
|
||||||
|
|
|
||||||
5
dist/ba_data/python/ba/_bootstrap.py
vendored
5
dist/ba_data/python/ba/_bootstrap.py
vendored
|
|
@ -47,7 +47,7 @@ def bootstrap() -> None:
|
||||||
|
|
||||||
# Give a soft warning if we're being used with a different binary
|
# Give a soft warning if we're being used with a different binary
|
||||||
# version than we expect.
|
# version than we expect.
|
||||||
expected_build = 20921
|
expected_build = 20982
|
||||||
running_build: int = env['build_number']
|
running_build: int = env['build_number']
|
||||||
if running_build != expected_build:
|
if running_build != expected_build:
|
||||||
print(
|
print(
|
||||||
|
|
@ -120,7 +120,8 @@ def bootstrap() -> None:
|
||||||
import __main__
|
import __main__
|
||||||
|
|
||||||
# Clear out the standard quit/exit messages since they don't
|
# Clear out the standard quit/exit messages since they don't
|
||||||
# work for us.
|
# work in our embedded situation (should revisit this once we're
|
||||||
|
# usable from a standard interpreter).
|
||||||
del __main__.__builtins__.quit
|
del __main__.__builtins__.quit
|
||||||
del __main__.__builtins__.exit
|
del __main__.__builtins__.exit
|
||||||
|
|
||||||
|
|
|
||||||
45
dist/ba_data/python/ba/_cloud.py
vendored
45
dist/ba_data/python/ba/_cloud.py
vendored
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING, overload
|
from typing import TYPE_CHECKING, overload
|
||||||
|
|
||||||
import _ba
|
import _ba
|
||||||
|
|
@ -14,6 +15,8 @@ if TYPE_CHECKING:
|
||||||
from efro.message import Message, Response
|
from efro.message import Message, Response
|
||||||
import bacommon.cloud
|
import bacommon.cloud
|
||||||
|
|
||||||
|
DEBUG_LOG = False
|
||||||
|
|
||||||
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
||||||
# autogenerate this. That would give us type safety between this and
|
# autogenerate this. That would give us type safety between this and
|
||||||
# internal protocols.
|
# internal protocols.
|
||||||
|
|
@ -30,6 +33,21 @@ class CloudSubsystem:
|
||||||
"""
|
"""
|
||||||
return False # Needs to be overridden
|
return False # Needs to be overridden
|
||||||
|
|
||||||
|
def on_app_pause(self) -> None:
|
||||||
|
"""Should be called when the app pauses."""
|
||||||
|
|
||||||
|
def on_app_resume(self) -> None:
|
||||||
|
"""Should be called when the app resumes."""
|
||||||
|
|
||||||
|
def on_connectivity_changed(self, connected: bool) -> None:
|
||||||
|
"""Called when cloud connectivity state changes."""
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
|
||||||
|
|
||||||
|
# Inform things that use this.
|
||||||
|
# (TODO: should generalize this into some sort of registration system)
|
||||||
|
_ba.app.accounts_v2.on_cloud_connectivity_changed(connected)
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def send_message_cb(
|
def send_message_cb(
|
||||||
self,
|
self,
|
||||||
|
|
@ -66,6 +84,26 @@ class CloudSubsystem:
|
||||||
) -> None:
|
) -> None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def send_message_cb(
|
||||||
|
self,
|
||||||
|
msg: bacommon.cloud.SignInMessage,
|
||||||
|
on_response: Callable[
|
||||||
|
[bacommon.cloud.SignInResponse | Exception], None
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def send_message_cb(
|
||||||
|
self,
|
||||||
|
msg: bacommon.cloud.ManageAccountMessage,
|
||||||
|
on_response: Callable[
|
||||||
|
[bacommon.cloud.ManageAccountResponse | Exception], None
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
def send_message_cb(
|
def send_message_cb(
|
||||||
self,
|
self,
|
||||||
msg: Message,
|
msg: Message,
|
||||||
|
|
@ -93,6 +131,12 @@ class CloudSubsystem:
|
||||||
) -> bacommon.cloud.WorkspaceFetchResponse:
|
) -> bacommon.cloud.WorkspaceFetchResponse:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def send_message(
|
||||||
|
self, msg: bacommon.cloud.MerchAvailabilityMessage
|
||||||
|
) -> bacommon.cloud.MerchAvailabilityResponse:
|
||||||
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def send_message(
|
def send_message(
|
||||||
self, msg: bacommon.cloud.TestMessage
|
self, msg: bacommon.cloud.TestMessage
|
||||||
|
|
@ -110,7 +154,6 @@ class CloudSubsystem:
|
||||||
def cloud_console_exec(code: str) -> None:
|
def cloud_console_exec(code: str) -> None:
|
||||||
"""Called by the cloud console to run code in the logic thread."""
|
"""Called by the cloud console to run code in the logic thread."""
|
||||||
import sys
|
import sys
|
||||||
import logging
|
|
||||||
import __main__
|
import __main__
|
||||||
from ba._generated.enums import TimeType
|
from ba._generated.enums import TimeType
|
||||||
|
|
||||||
|
|
|
||||||
52
dist/ba_data/python/ba/_coopgame.py
vendored
52
dist/ba_data/python/ba/_coopgame.py
vendored
|
|
@ -11,7 +11,7 @@ from ba._gameactivity import GameActivity
|
||||||
from ba._general import WeakCall
|
from ba._general import WeakCall
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any, Sequence
|
from typing import Sequence
|
||||||
from bastd.actor.playerspaz import PlayerSpaz
|
from bastd.actor.playerspaz import PlayerSpaz
|
||||||
import ba
|
import ba
|
||||||
|
|
||||||
|
|
@ -56,56 +56,6 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
||||||
# Preload achievement images in case we get some.
|
# Preload achievement images in case we get some.
|
||||||
_ba.timer(2.0, WeakCall(self._preload_achievements))
|
_ba.timer(2.0, WeakCall(self._preload_achievements))
|
||||||
|
|
||||||
def _show_standard_scores_to_beat_ui(
|
|
||||||
self, scores: list[dict[str, Any]]
|
|
||||||
) -> None:
|
|
||||||
from efro.util import asserttype
|
|
||||||
from ba._gameutils import timestring, animate
|
|
||||||
from ba._nodeactor import NodeActor
|
|
||||||
from ba._generated.enums import TimeFormat
|
|
||||||
|
|
||||||
display_type = self.get_score_type()
|
|
||||||
if scores is not None:
|
|
||||||
|
|
||||||
# Sort by originating date so that the most recent is first.
|
|
||||||
scores.sort(reverse=True, key=lambda s: asserttype(s['time'], int))
|
|
||||||
|
|
||||||
# Now make a display for the most recent challenge.
|
|
||||||
for score in scores:
|
|
||||||
if score['type'] == 'score_challenge':
|
|
||||||
tval = (
|
|
||||||
score['player']
|
|
||||||
+ ': '
|
|
||||||
+ timestring(
|
|
||||||
int(score['value']) * 10,
|
|
||||||
timeformat=TimeFormat.MILLISECONDS,
|
|
||||||
).evaluate()
|
|
||||||
if display_type == 'time'
|
|
||||||
else str(score['value'])
|
|
||||||
)
|
|
||||||
hattach = 'center' if display_type == 'time' else 'left'
|
|
||||||
halign = 'center' if display_type == 'time' else 'left'
|
|
||||||
pos = (20, -70) if display_type == 'time' else (20, -130)
|
|
||||||
txt = NodeActor(
|
|
||||||
_ba.newnode(
|
|
||||||
'text',
|
|
||||||
attrs={
|
|
||||||
'v_attach': 'top',
|
|
||||||
'h_attach': hattach,
|
|
||||||
'h_align': halign,
|
|
||||||
'color': (0.7, 0.4, 1, 1),
|
|
||||||
'shadow': 0.5,
|
|
||||||
'flatness': 1.0,
|
|
||||||
'position': pos,
|
|
||||||
'scale': 0.6,
|
|
||||||
'text': tval,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
).autoretain()
|
|
||||||
assert txt.node is not None
|
|
||||||
animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
|
|
||||||
break
|
|
||||||
|
|
||||||
# FIXME: this is now redundant with activityutils.getscoreconfig();
|
# FIXME: this is now redundant with activityutils.getscoreconfig();
|
||||||
# need to kill this.
|
# need to kill this.
|
||||||
def get_score_type(self) -> str:
|
def get_score_type(self) -> str:
|
||||||
|
|
|
||||||
2
dist/ba_data/python/ba/_gameutils.py
vendored
2
dist/ba_data/python/ba/_gameutils.py
vendored
|
|
@ -278,7 +278,7 @@ def show_damage_count(
|
||||||
|
|
||||||
|
|
||||||
def timestring(
|
def timestring(
|
||||||
timeval: float,
|
timeval: float | int,
|
||||||
centi: bool = True,
|
centi: bool = True,
|
||||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||||
suppress_format_warning: bool = False,
|
suppress_format_warning: bool = False,
|
||||||
|
|
|
||||||
46
dist/ba_data/python/ba/_hooks.py
vendored
46
dist/ba_data/python/ba/_hooks.py
vendored
|
|
@ -247,6 +247,14 @@ def google_play_purchases_not_available_message() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def google_play_services_not_available_message() -> None:
|
||||||
|
from ba._language import Lstr
|
||||||
|
|
||||||
|
_ba.screenmessage(
|
||||||
|
Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def empty_call() -> None:
|
def empty_call() -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -449,3 +457,41 @@ def hash_strings(inputs: list[str]) -> str:
|
||||||
def have_account_v2_credentials() -> bool:
|
def have_account_v2_credentials() -> bool:
|
||||||
"""Do we have primary account-v2 credentials set?"""
|
"""Do we have primary account-v2 credentials set?"""
|
||||||
return _ba.app.accounts_v2.have_primary_credentials()
|
return _ba.app.accounts_v2.have_primary_credentials()
|
||||||
|
|
||||||
|
|
||||||
|
def implicit_sign_in(
|
||||||
|
login_type_str: str, login_id: str, display_name: str
|
||||||
|
) -> None:
|
||||||
|
"""An implicit login happened."""
|
||||||
|
from bacommon.login import LoginType
|
||||||
|
|
||||||
|
_ba.app.accounts_v2.on_implicit_sign_in(
|
||||||
|
login_type=LoginType(login_type_str),
|
||||||
|
login_id=login_id,
|
||||||
|
display_name=display_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def implicit_sign_out(login_type_str: str) -> None:
|
||||||
|
"""An implicit logout happened."""
|
||||||
|
from bacommon.login import LoginType
|
||||||
|
|
||||||
|
_ba.app.accounts_v2.on_implicit_sign_out(
|
||||||
|
login_type=LoginType(login_type_str)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def login_adapter_get_sign_in_token_response(
|
||||||
|
login_type_str: str, attempt_id_str: str, result_str: str
|
||||||
|
) -> None:
|
||||||
|
"""Login adapter do-sign-in completed."""
|
||||||
|
from bacommon.login import LoginType
|
||||||
|
from ba._login import LoginAdapterNative
|
||||||
|
|
||||||
|
login_type = LoginType(login_type_str)
|
||||||
|
attempt_id = int(attempt_id_str)
|
||||||
|
result = None if result_str == '' else result_str
|
||||||
|
with _ba.Context('ui'):
|
||||||
|
adapter = _ba.app.accounts_v2.login_adapters[login_type]
|
||||||
|
assert isinstance(adapter, LoginAdapterNative)
|
||||||
|
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
|
||||||
|
|
|
||||||
6
dist/ba_data/python/ba/_internal.py
vendored
6
dist/ba_data/python/ba/_internal.py
vendored
|
|
@ -104,7 +104,6 @@ def submit_score(
|
||||||
name: Any,
|
name: Any,
|
||||||
score: int | None,
|
score: int | None,
|
||||||
callback: Callable,
|
callback: Callable,
|
||||||
friend_callback: Callable | None,
|
|
||||||
order: str = 'increasing',
|
order: str = 'increasing',
|
||||||
tournament_id: str | None = None,
|
tournament_id: str | None = None,
|
||||||
score_type: str = 'points',
|
score_type: str = 'points',
|
||||||
|
|
@ -125,7 +124,6 @@ def submit_score(
|
||||||
name=name,
|
name=name,
|
||||||
score=score,
|
score=score,
|
||||||
callback=callback,
|
callback=callback,
|
||||||
friend_callback=friend_callback,
|
|
||||||
order=order,
|
order=order,
|
||||||
tournament_id=tournament_id,
|
tournament_id=tournament_id,
|
||||||
score_type=score_type,
|
score_type=score_type,
|
||||||
|
|
@ -321,7 +319,9 @@ def get_v1_account_state() -> str:
|
||||||
"""(internal)"""
|
"""(internal)"""
|
||||||
if HAVE_INTERNAL:
|
if HAVE_INTERNAL:
|
||||||
return _bainternal.get_v1_account_state()
|
return _bainternal.get_v1_account_state()
|
||||||
raise _no_bainternal_error()
|
|
||||||
|
# Without internal present just consider ourself always signed out.
|
||||||
|
return 'signed_out'
|
||||||
|
|
||||||
|
|
||||||
def get_v1_account_display_string(full: bool = True) -> str:
|
def get_v1_account_display_string(full: bool = True) -> str:
|
||||||
|
|
|
||||||
55
dist/ba_data/python/ba/_language.py
vendored
55
dist/ba_data/python/ba/_language.py
vendored
|
|
@ -67,38 +67,39 @@ class LanguageSubsystem:
|
||||||
|
|
||||||
def _get_default_language(self) -> str:
|
def _get_default_language(self) -> str:
|
||||||
languages = {
|
languages = {
|
||||||
|
'ar': 'Arabic',
|
||||||
|
'be': 'Belarussian',
|
||||||
|
'zh': 'Chinese',
|
||||||
|
'hr': 'Croatian',
|
||||||
|
'cs': 'Czech',
|
||||||
|
'da': 'Danish',
|
||||||
|
'nl': 'Dutch',
|
||||||
|
'eo': 'Esperanto',
|
||||||
|
'fil': 'Filipino',
|
||||||
|
'fr': 'French',
|
||||||
'de': 'German',
|
'de': 'German',
|
||||||
|
'el': 'Greek',
|
||||||
|
'hi': 'Hindi',
|
||||||
|
'hu': 'Hungarian',
|
||||||
|
'id': 'Indonesian',
|
||||||
|
'it': 'Italian',
|
||||||
|
'ko': 'Korean',
|
||||||
|
'ms': 'Malay',
|
||||||
|
'fa': 'Persian',
|
||||||
|
'pl': 'Polish',
|
||||||
|
'pt': 'Portuguese',
|
||||||
|
'ro': 'Romanian',
|
||||||
|
'ru': 'Russian',
|
||||||
|
'sr': 'Serbian',
|
||||||
'es': 'Spanish',
|
'es': 'Spanish',
|
||||||
'sk': 'Slovak',
|
'sk': 'Slovak',
|
||||||
'it': 'Italian',
|
|
||||||
'nl': 'Dutch',
|
|
||||||
'da': 'Danish',
|
|
||||||
'pt': 'Portuguese',
|
|
||||||
'fr': 'French',
|
|
||||||
'el': 'Greek',
|
|
||||||
'ru': 'Russian',
|
|
||||||
'pl': 'Polish',
|
|
||||||
'sv': 'Swedish',
|
'sv': 'Swedish',
|
||||||
'eo': 'Esperanto',
|
|
||||||
'cs': 'Czech',
|
|
||||||
'hr': 'Croatian',
|
|
||||||
'hu': 'Hungarian',
|
|
||||||
'be': 'Belarussian',
|
|
||||||
'ro': 'Romanian',
|
|
||||||
'ko': 'Korean',
|
|
||||||
'fa': 'Persian',
|
|
||||||
'ar': 'Arabic',
|
|
||||||
'zh': 'Chinese',
|
|
||||||
'tr': 'Turkish',
|
|
||||||
'th': 'Thai',
|
|
||||||
'id': 'Indonesian',
|
|
||||||
'sr': 'Serbian',
|
|
||||||
'uk': 'Ukrainian',
|
|
||||||
'vi': 'Vietnamese',
|
|
||||||
'vec': 'Venetian',
|
|
||||||
'hi': 'Hindi',
|
|
||||||
'ta': 'Tamil',
|
'ta': 'Tamil',
|
||||||
'fil': 'Filipino',
|
'th': 'Thai',
|
||||||
|
'tr': 'Turkish',
|
||||||
|
'uk': 'Ukrainian',
|
||||||
|
'vec': 'Venetian',
|
||||||
|
'vi': 'Vietnamese',
|
||||||
}
|
}
|
||||||
|
|
||||||
# Special case for Chinese: map specific variations to traditional.
|
# Special case for Chinese: map specific variations to traditional.
|
||||||
|
|
|
||||||
330
dist/ba_data/python/ba/_login.py
vendored
Normal file
330
dist/ba_data/python/ba/_login.py
vendored
Normal file
|
|
@ -0,0 +1,330 @@
|
||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""Login related functionality."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, final
|
||||||
|
|
||||||
|
from bacommon.login import LoginType
|
||||||
|
import _ba
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
|
DEBUG_LOG = False
|
||||||
|
|
||||||
|
|
||||||
|
class LoginAdapter:
|
||||||
|
"""Allows using implicit login types in an explicit way.
|
||||||
|
|
||||||
|
Some login types such as Google Play Game Services or Game Center are
|
||||||
|
basically always present and often do not provide a way to log out
|
||||||
|
from within a running app, so this adapter exists to use them in a
|
||||||
|
flexible manner by 'attaching' and 'detaching' from an always-present
|
||||||
|
login, allowing for its use alongside other login types. It also
|
||||||
|
provides common functionality for server-side account verification and
|
||||||
|
other handy bits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SignInResult:
|
||||||
|
"""Describes the final result of a sign-in attempt."""
|
||||||
|
|
||||||
|
credentials: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ImplicitLoginState:
|
||||||
|
"""Describes the current state of an implicit login."""
|
||||||
|
|
||||||
|
login_id: str
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
def __init__(self, login_type: LoginType):
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
self.login_type = login_type
|
||||||
|
self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = (
|
||||||
|
None
|
||||||
|
)
|
||||||
|
self._on_app_launch_called = False
|
||||||
|
self._implicit_login_state_dirty = False
|
||||||
|
self._back_end_active = False
|
||||||
|
|
||||||
|
# Which login of our type (if any) is associated with the
|
||||||
|
# current active primary account.
|
||||||
|
self._active_login_id: str | None = None
|
||||||
|
|
||||||
|
def on_app_launch(self) -> None:
|
||||||
|
"""Should be called for each adapter in on_app_launch."""
|
||||||
|
|
||||||
|
assert not self._on_app_launch_called
|
||||||
|
self._on_app_launch_called = True
|
||||||
|
|
||||||
|
# Any implicit state we received up until now needs to be pushed
|
||||||
|
# to the app account subsystem.
|
||||||
|
self._update_implicit_login_state()
|
||||||
|
|
||||||
|
def set_implicit_login_state(
|
||||||
|
self, state: ImplicitLoginState | None
|
||||||
|
) -> None:
|
||||||
|
"""Keep the adapter informed of implicit login states.
|
||||||
|
|
||||||
|
This should be called by the adapter back-end when an account
|
||||||
|
of their associated type gets logged in or out.
|
||||||
|
"""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
|
||||||
|
# Ignore redundant sets.
|
||||||
|
if state == self._implicit_login_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
if DEBUG_LOG:
|
||||||
|
if state is None:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s implicit state changed;'
|
||||||
|
' now signed out.',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s implicit state changed;'
|
||||||
|
' now signed in as %s.',
|
||||||
|
self.login_type.name,
|
||||||
|
state.display_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._implicit_login_state = state
|
||||||
|
self._implicit_login_state_dirty = True
|
||||||
|
|
||||||
|
# (possibly) push it to the app for handling.
|
||||||
|
self._update_implicit_login_state()
|
||||||
|
|
||||||
|
# This might affect whether we consider that back-end as 'active'.
|
||||||
|
self._update_back_end_active()
|
||||||
|
|
||||||
|
def set_active_logins(self, logins: dict[LoginType, str]) -> None:
|
||||||
|
"""Keep the adapter informed of actively used logins.
|
||||||
|
|
||||||
|
This should be called by the app's account subsystem to
|
||||||
|
keep adapters up to date on the full set of logins attached
|
||||||
|
to the currently-in-use account.
|
||||||
|
Note that the logins dict passed in should be immutable as
|
||||||
|
only a reference to it is stored, not a copy.
|
||||||
|
"""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter got active logins %s.',
|
||||||
|
self.login_type.name,
|
||||||
|
{k: v[:4] + '...' + v[-4:] for k, v in logins.items()},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._active_login_id = logins.get(self.login_type)
|
||||||
|
self._update_back_end_active()
|
||||||
|
|
||||||
|
def on_back_end_active_change(self, active: bool) -> None:
|
||||||
|
"""Called when active state for the back-end is (possibly) changing.
|
||||||
|
|
||||||
|
Meant to be overridden by subclasses.
|
||||||
|
Being active means that the implicit login provided by the back-end
|
||||||
|
is actually being used by the app. It should therefore register
|
||||||
|
unlocked achievements, leaderboard scores, allow viewing native
|
||||||
|
UIs, etc. When not active it should ignore everything and behave
|
||||||
|
as if logged out, even if it technically is still logged in.
|
||||||
|
"""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
del active # Unused.
|
||||||
|
|
||||||
|
@final
|
||||||
|
def sign_in(
|
||||||
|
self,
|
||||||
|
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
|
||||||
|
) -> None:
|
||||||
|
"""Attempt an explicit sign in via this adapter.
|
||||||
|
|
||||||
|
This can be called even if the back-end is not implicitly signed in;
|
||||||
|
the adapter will attempt to sign in if possible. An exception will
|
||||||
|
be returned if the sign-in attempt fails.
|
||||||
|
"""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
from ba._general import Call
|
||||||
|
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter sign_in() called;'
|
||||||
|
' fetching sign-in-token...',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _got_sign_in_token_result(result: str | None) -> None:
|
||||||
|
import bacommon.cloud
|
||||||
|
|
||||||
|
# Failed to get a sign-in-token.
|
||||||
|
if result is None:
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter sign-in-token fetch failed;'
|
||||||
|
' aborting sign-in.',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
_ba.pushcall(
|
||||||
|
Call(
|
||||||
|
result_cb,
|
||||||
|
self,
|
||||||
|
RuntimeError('fetch-sign-in-token failed.'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Got a sign-in token! Now pass it to the cloud which will use
|
||||||
|
# it to verify our identity and give us app credentials on
|
||||||
|
# success.
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter sign-in-token fetch succeeded;'
|
||||||
|
' passing to cloud for verification...',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _got_sign_in_response(
|
||||||
|
response: bacommon.cloud.SignInResponse | Exception,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter got error'
|
||||||
|
' sign-in response: %s',
|
||||||
|
self.login_type.name,
|
||||||
|
response,
|
||||||
|
)
|
||||||
|
_ba.pushcall(Call(result_cb, self, response))
|
||||||
|
else:
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter got successful'
|
||||||
|
' sign-in response',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
if response.credentials is None:
|
||||||
|
result2: LoginAdapter.SignInResult | Exception = (
|
||||||
|
RuntimeError(
|
||||||
|
'No credentials returned after'
|
||||||
|
' submitting sign-in-token.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result2 = self.SignInResult(
|
||||||
|
credentials=response.credentials
|
||||||
|
)
|
||||||
|
_ba.pushcall(Call(result_cb, self, result2))
|
||||||
|
|
||||||
|
_ba.app.cloud.send_message_cb(
|
||||||
|
bacommon.cloud.SignInMessage(self.login_type, result),
|
||||||
|
on_response=_got_sign_in_response,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Kick off the process by fetching a sign-in token.
|
||||||
|
self.get_sign_in_token(completion_cb=_got_sign_in_token_result)
|
||||||
|
|
||||||
|
def is_back_end_active(self) -> bool:
|
||||||
|
"""Is this adapter's back-end currently active?"""
|
||||||
|
return self._back_end_active
|
||||||
|
|
||||||
|
def get_sign_in_token(
|
||||||
|
self, completion_cb: Callable[[str | None], None]
|
||||||
|
) -> None:
|
||||||
|
"""Get a sign-in token from the adapter back end.
|
||||||
|
|
||||||
|
This token is then passed to the master-server to complete the
|
||||||
|
login process.
|
||||||
|
The adapter can use this opportunity to bring up account creation
|
||||||
|
UI, call its internal sign_in function, etc. as needed.
|
||||||
|
The provided completion_cb should then be called with either a token
|
||||||
|
or None if sign in failed or was cancelled.
|
||||||
|
"""
|
||||||
|
from ba._general import Call
|
||||||
|
|
||||||
|
# Default implementation simply fails immediately.
|
||||||
|
_ba.pushcall(Call(completion_cb, None))
|
||||||
|
|
||||||
|
def _update_implicit_login_state(self) -> None:
|
||||||
|
# If we've received an implicit login state, schedule it to be
|
||||||
|
# sent along to the app. We wait until on-app-launch has been
|
||||||
|
# called so that account-client-v2 has had a chance to load
|
||||||
|
# any existing state so it can properly respond to this.
|
||||||
|
if self._implicit_login_state_dirty and self._on_app_launch_called:
|
||||||
|
from ba._general import Call
|
||||||
|
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter sending'
|
||||||
|
' implicit-state-changed to app.',
|
||||||
|
self.login_type.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
_ba.pushcall(
|
||||||
|
Call(
|
||||||
|
_ba.app.accounts_v2.on_implicit_login_state_changed,
|
||||||
|
self.login_type,
|
||||||
|
self._implicit_login_state,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._implicit_login_state_dirty = False
|
||||||
|
|
||||||
|
def _update_back_end_active(self) -> None:
|
||||||
|
was_active = self._back_end_active
|
||||||
|
if self._implicit_login_state is None:
|
||||||
|
is_active = False
|
||||||
|
else:
|
||||||
|
is_active = (
|
||||||
|
self._implicit_login_state.login_id == self._active_login_id
|
||||||
|
)
|
||||||
|
if was_active != is_active:
|
||||||
|
if DEBUG_LOG:
|
||||||
|
logging.debug(
|
||||||
|
'LoginAdapter: %s adapter back-end-active is now %s.',
|
||||||
|
self.login_type.name,
|
||||||
|
is_active,
|
||||||
|
)
|
||||||
|
self.on_back_end_active_change(is_active)
|
||||||
|
self._back_end_active = is_active
|
||||||
|
|
||||||
|
|
||||||
|
class LoginAdapterNative(LoginAdapter):
|
||||||
|
"""A login adapter that does its work in the native layer."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(LoginType.GPGS)
|
||||||
|
|
||||||
|
# Store int ids for in-flight attempts since they may go through
|
||||||
|
# various platform layers and back.
|
||||||
|
self._sign_in_attempt_num = 123
|
||||||
|
self._sign_in_attempts: dict[int, Callable[[str | None], None]] = {}
|
||||||
|
|
||||||
|
def get_sign_in_token(
|
||||||
|
self, completion_cb: Callable[[str | None], None]
|
||||||
|
) -> None:
|
||||||
|
attempt_id = self._sign_in_attempt_num
|
||||||
|
self._sign_in_attempts[attempt_id] = completion_cb
|
||||||
|
self._sign_in_attempt_num += 1
|
||||||
|
_ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id)
|
||||||
|
|
||||||
|
def on_back_end_active_change(self, active: bool) -> None:
|
||||||
|
_ba.login_adapter_back_end_active_change(self.login_type.value, active)
|
||||||
|
|
||||||
|
def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None:
|
||||||
|
"""Called by the native layer on a completed attempt."""
|
||||||
|
assert _ba.in_logic_thread()
|
||||||
|
if attempt_id not in self._sign_in_attempts:
|
||||||
|
logging.exception('sign-in attempt_id %d not found', attempt_id)
|
||||||
|
return
|
||||||
|
callback = self._sign_in_attempts.pop(attempt_id)
|
||||||
|
callback(result)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginAdapterGPGS(LoginAdapterNative):
|
||||||
|
"""Google Play Game Services adapter."""
|
||||||
5
dist/ba_data/python/ba/_playlist.py
vendored
5
dist/ba_data/python/ba/_playlist.py
vendored
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import _ba
|
||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, TYPE_CHECKING
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
@ -39,6 +40,7 @@ def filter_playlist(
|
||||||
|
|
||||||
goodlist: list[dict] = []
|
goodlist: list[dict] = []
|
||||||
unowned_maps: Sequence[str]
|
unowned_maps: Sequence[str]
|
||||||
|
available_maps: list[str] = list(_ba.app.maps.keys())
|
||||||
if remove_unowned or mark_unowned:
|
if remove_unowned or mark_unowned:
|
||||||
unowned_maps = get_unowned_maps()
|
unowned_maps = get_unowned_maps()
|
||||||
unowned_game_types = get_unowned_game_types()
|
unowned_game_types = get_unowned_game_types()
|
||||||
|
|
@ -157,6 +159,9 @@ def filter_playlist(
|
||||||
|
|
||||||
gameclass = getclass(entry['type'], GameActivity)
|
gameclass = getclass(entry['type'], GameActivity)
|
||||||
|
|
||||||
|
if entry['settings']['map'] not in available_maps:
|
||||||
|
raise ImportError(f"Map not found: '{entry['settings']['map']}'")
|
||||||
|
|
||||||
if remove_unowned and gameclass in unowned_game_types:
|
if remove_unowned and gameclass in unowned_game_types:
|
||||||
continue
|
continue
|
||||||
if add_resolved_type:
|
if add_resolved_type:
|
||||||
|
|
|
||||||
2
dist/ba_data/python/ba/_plugin.py
vendored
2
dist/ba_data/python/ba/_plugin.py
vendored
|
|
@ -173,7 +173,7 @@ class PluginSubsystem:
|
||||||
color=(1, 1, 0),
|
color=(1, 1, 0),
|
||||||
)
|
)
|
||||||
plugnames = ', '.join(disappeared_plugs)
|
plugnames = ', '.join(disappeared_plugs)
|
||||||
logging.warning(
|
logging.info(
|
||||||
'%d plugin(s) no longer found: %s.',
|
'%d plugin(s) no longer found: %s.',
|
||||||
len(disappeared_plugs),
|
len(disappeared_plugs),
|
||||||
plugnames,
|
plugnames,
|
||||||
|
|
|
||||||
1
dist/ba_data/python/ba/_servermode.py
vendored
1
dist/ba_data/python/ba/_servermode.py
vendored
|
|
@ -418,6 +418,7 @@ class ServerController:
|
||||||
|
|
||||||
# Call set-enabled last (will push state to the cloud).
|
# Call set-enabled last (will push state to the cloud).
|
||||||
_ba.set_public_party_max_size(self._config.max_party_size)
|
_ba.set_public_party_max_size(self._config.max_party_size)
|
||||||
|
_ba.set_public_party_queue_enabled(self._config.enable_queue)
|
||||||
_ba.set_public_party_name(self._config.party_name)
|
_ba.set_public_party_name(self._config.party_name)
|
||||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
_ba.set_public_party_enabled(self._config.party_is_public)
|
||||||
|
|
|
||||||
27
dist/ba_data/python/ba/_store.py
vendored
27
dist/ba_data/python/ba/_store.py
vendored
|
|
@ -30,6 +30,8 @@ def get_store_item_name_translated(item_name: str) -> ba.Lstr:
|
||||||
return _language.Lstr(
|
return _language.Lstr(
|
||||||
translate=('characterNames', item_info['character'])
|
translate=('characterNames', item_info['character'])
|
||||||
)
|
)
|
||||||
|
if item_name in ['merch']:
|
||||||
|
return _language.Lstr(resource='merchText')
|
||||||
if item_name in ['upgrades.pro', 'pro']:
|
if item_name in ['upgrades.pro', 'pro']:
|
||||||
return _language.Lstr(
|
return _language.Lstr(
|
||||||
resource='store.bombSquadProNameText',
|
resource='store.bombSquadProNameText',
|
||||||
|
|
@ -50,8 +52,17 @@ def get_store_item_display_size(item_name: str) -> tuple[float, float]:
|
||||||
"""(internal)"""
|
"""(internal)"""
|
||||||
if item_name.startswith('characters.'):
|
if item_name.startswith('characters.'):
|
||||||
return 340 * 0.6, 430 * 0.6
|
return 340 * 0.6, 430 * 0.6
|
||||||
if item_name in ['pro', 'upgrades.pro']:
|
if item_name in ['pro', 'upgrades.pro', 'merch']:
|
||||||
return 650 * 0.9, 500 * 0.85
|
from ba._generated.enums import UIScale
|
||||||
|
|
||||||
|
return 650 * 0.9, 500 * (
|
||||||
|
0.72
|
||||||
|
if (
|
||||||
|
_ba.app.config.get('Merch Link')
|
||||||
|
and _ba.app.ui.uiscale is UIScale.SMALL
|
||||||
|
)
|
||||||
|
else 0.85
|
||||||
|
)
|
||||||
if item_name.startswith('maps.'):
|
if item_name.startswith('maps.'):
|
||||||
return 510 * 0.6, 450 * 0.6
|
return 510 * 0.6, 450 * 0.6
|
||||||
if item_name.startswith('icons.'):
|
if item_name.startswith('icons.'):
|
||||||
|
|
@ -96,6 +107,7 @@ def get_store_items() -> dict[str, dict]:
|
||||||
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
||||||
'characters.santa': {'character': 'Santa Claus'},
|
'characters.santa': {'character': 'Santa Claus'},
|
||||||
'characters.bunny': {'character': 'Easter Bunny'},
|
'characters.bunny': {'character': 'Easter Bunny'},
|
||||||
|
'merch': {},
|
||||||
'pro': {},
|
'pro': {},
|
||||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||||
'games.ninja_fight': {
|
'games.ninja_fight': {
|
||||||
|
|
@ -193,9 +205,7 @@ def get_store_items() -> dict[str, dict]:
|
||||||
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
|
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
|
||||||
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
|
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
|
||||||
}
|
}
|
||||||
store_items = _ba.app.store_items
|
return _ba.app.store_items
|
||||||
assert store_items is not None
|
|
||||||
return store_items
|
|
||||||
|
|
||||||
|
|
||||||
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||||
|
|
@ -261,7 +271,6 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
store_layout = _ba.app.store_layout
|
store_layout = _ba.app.store_layout
|
||||||
assert store_layout is not None
|
|
||||||
store_layout['characters'] = [
|
store_layout['characters'] = [
|
||||||
{
|
{
|
||||||
'items': [
|
'items': [
|
||||||
|
|
@ -302,6 +311,12 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||||
'items': ['games.easter_egg_hunt'],
|
'items': ['games.easter_egg_hunt'],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# This will cause merch to show only if the master-server has
|
||||||
|
# given us a link (which means merch is available in our region).
|
||||||
|
store_layout['extras'] = [{'items': ['pro']}]
|
||||||
|
if _ba.app.config.get('Merch Link'):
|
||||||
|
store_layout['extras'][0]['items'].append('merch')
|
||||||
return store_layout
|
return store_layout
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
9
dist/ba_data/python/ba/internal.py
vendored
9
dist/ba_data/python/ba/internal.py
vendored
|
|
@ -40,6 +40,7 @@ from _ba import (
|
||||||
get_public_party_max_size,
|
get_public_party_max_size,
|
||||||
set_public_party_name,
|
set_public_party_name,
|
||||||
set_public_party_max_size,
|
set_public_party_max_size,
|
||||||
|
set_public_party_queue_enabled,
|
||||||
set_authenticate_clients,
|
set_authenticate_clients,
|
||||||
set_public_party_enabled,
|
set_public_party_enabled,
|
||||||
reset_random_player_names,
|
reset_random_player_names,
|
||||||
|
|
@ -80,6 +81,7 @@ from _ba import (
|
||||||
get_replays_dir,
|
get_replays_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ba._login import LoginAdapter
|
||||||
from ba._map import (
|
from ba._map import (
|
||||||
get_map_class,
|
get_map_class,
|
||||||
register_map,
|
register_map,
|
||||||
|
|
@ -99,6 +101,8 @@ from ba._apputils import (
|
||||||
is_browser_likely_available,
|
is_browser_likely_available,
|
||||||
get_remote_app_name,
|
get_remote_app_name,
|
||||||
should_submit_debug_info,
|
should_submit_debug_info,
|
||||||
|
dump_app_state,
|
||||||
|
log_dumped_app_state,
|
||||||
)
|
)
|
||||||
from ba._benchmark import (
|
from ba._benchmark import (
|
||||||
run_gpu_benchmark,
|
run_gpu_benchmark,
|
||||||
|
|
@ -178,6 +182,7 @@ from ba._internal import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
'LoginAdapter',
|
||||||
'show_online_score_ui',
|
'show_online_score_ui',
|
||||||
'set_ui_input_device',
|
'set_ui_input_device',
|
||||||
'is_party_icon_visible',
|
'is_party_icon_visible',
|
||||||
|
|
@ -209,6 +214,7 @@ __all__ = [
|
||||||
'get_public_party_max_size',
|
'get_public_party_max_size',
|
||||||
'set_public_party_name',
|
'set_public_party_name',
|
||||||
'set_public_party_max_size',
|
'set_public_party_max_size',
|
||||||
|
'set_public_party_queue_enabled',
|
||||||
'set_authenticate_clients',
|
'set_authenticate_clients',
|
||||||
'set_public_party_enabled',
|
'set_public_party_enabled',
|
||||||
'reset_random_player_names',
|
'reset_random_player_names',
|
||||||
|
|
@ -247,7 +253,6 @@ __all__ = [
|
||||||
'set_telnet_access_enabled',
|
'set_telnet_access_enabled',
|
||||||
'new_replay_session',
|
'new_replay_session',
|
||||||
'get_replays_dir',
|
'get_replays_dir',
|
||||||
# DIVIDER
|
|
||||||
'get_unowned_maps',
|
'get_unowned_maps',
|
||||||
'get_unowned_game_types',
|
'get_unowned_game_types',
|
||||||
'get_map_class',
|
'get_map_class',
|
||||||
|
|
@ -329,4 +334,6 @@ __all__ = [
|
||||||
'sign_out_v1',
|
'sign_out_v1',
|
||||||
'sign_in_v1',
|
'sign_in_v1',
|
||||||
'mark_config_dirty',
|
'mark_config_dirty',
|
||||||
|
'dump_app_state',
|
||||||
|
'log_dumped_app_state',
|
||||||
]
|
]
|
||||||
|
|
|
||||||
2
dist/ba_data/python/ba/modutils.py
vendored
2
dist/ba_data/python/ba/modutils.py
vendored
|
|
@ -87,7 +87,7 @@ def show_user_scripts() -> None:
|
||||||
' See settings/advanced'
|
' See settings/advanced'
|
||||||
' in the game for more info.'
|
' in the game for more info.'
|
||||||
)
|
)
|
||||||
_ba.android_media_scan_file(file_name)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
from ba import _error
|
from ba import _error
|
||||||
|
|
||||||
|
|
|
||||||
58
dist/ba_data/python/bacommon/cloud.py
vendored
58
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -10,6 +10,7 @@ from enum import Enum
|
||||||
from efro.message import Message, Response
|
from efro.message import Message, Response
|
||||||
from efro.dataclassio import ioprepped, IOAttrs
|
from efro.dataclassio import ioprepped, IOAttrs
|
||||||
from bacommon.transfer import DirectoryManifest
|
from bacommon.transfer import DirectoryManifest
|
||||||
|
from bacommon.login import LoginType
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
pass
|
||||||
|
|
@ -154,3 +155,60 @@ class WorkspaceFetchResponse(Response):
|
||||||
] = field(default_factory=dict)
|
] = field(default_factory=dict)
|
||||||
|
|
||||||
done: Annotated[bool, IOAttrs('d')] = False
|
done: Annotated[bool, IOAttrs('d')] = False
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class MerchAvailabilityMessage(Message):
|
||||||
|
"""Can we show merch link?"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_response_types(cls) -> list[type[Response] | None]:
|
||||||
|
return [MerchAvailabilityResponse]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class MerchAvailabilityResponse(Response):
|
||||||
|
"""About that merch..."""
|
||||||
|
|
||||||
|
url: Annotated[str | None, IOAttrs('u')]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class SignInMessage(Message):
|
||||||
|
"""Can I sign in please?"""
|
||||||
|
|
||||||
|
login_type: Annotated[LoginType, IOAttrs('l')]
|
||||||
|
sign_in_token: Annotated[str, IOAttrs('t')]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_response_types(cls) -> list[type[Response] | None]:
|
||||||
|
return [SignInResponse]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class SignInResponse(Response):
|
||||||
|
"""Here's that sign-in result you asked for, boss."""
|
||||||
|
|
||||||
|
credentials: Annotated[str | None, IOAttrs('c')]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class ManageAccountMessage(Message):
|
||||||
|
"""Message asking for a manage-account url."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_response_types(cls) -> list[type[Response] | None]:
|
||||||
|
return [ManageAccountResponse]
|
||||||
|
|
||||||
|
|
||||||
|
@ioprepped
|
||||||
|
@dataclass
|
||||||
|
class ManageAccountResponse(Response):
|
||||||
|
"""Here's that sign-in result you asked for, boss."""
|
||||||
|
|
||||||
|
url: Annotated[str | None, IOAttrs('u')]
|
||||||
|
|
|
||||||
31
dist/ba_data/python/bacommon/login.py
vendored
Normal file
31
dist/ba_data/python/bacommon/login.py
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""Functionality related to cloud based assets."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoginType(Enum):
|
||||||
|
"""Types of logins available."""
|
||||||
|
|
||||||
|
# Email/password
|
||||||
|
EMAIL = 'email'
|
||||||
|
|
||||||
|
# Google Play Game Services
|
||||||
|
GPGS = 'gpgs'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayname(self) -> str:
|
||||||
|
"""Human readable name for this value."""
|
||||||
|
cls = type(self)
|
||||||
|
match self:
|
||||||
|
case cls.EMAIL:
|
||||||
|
return 'Email/Password'
|
||||||
|
case cls.GPGS:
|
||||||
|
return 'Google Play Games'
|
||||||
|
|
@ -138,6 +138,11 @@ class ServerConfig:
|
||||||
tuple[float, float, float], tuple[float, float, float]
|
tuple[float, float, float], tuple[float, float, float]
|
||||||
] | None = None
|
] | None = None
|
||||||
|
|
||||||
|
# Whether to enable the queue where players can line up before entering
|
||||||
|
# your server. Disabling this can be used as a workaround to deal with
|
||||||
|
# queue spamming attacks.
|
||||||
|
enable_queue: bool = True
|
||||||
|
|
||||||
# (internal) stress-testing mode.
|
# (internal) stress-testing mode.
|
||||||
stress_test_players: int | None = None
|
stress_test_players: int | None = None
|
||||||
|
|
||||||
|
|
|
||||||
59
dist/ba_data/python/bastd/activity/coopscore.py
vendored
59
dist/ba_data/python/bastd/activity/coopscore.py
vendored
|
|
@ -116,7 +116,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
self._newly_complete: bool | None = None
|
self._newly_complete: bool | None = None
|
||||||
self._is_more_levels: bool | None = None
|
self._is_more_levels: bool | None = None
|
||||||
self._next_level_name: str | None = None
|
self._next_level_name: str | None = None
|
||||||
self._show_friend_scores: bool | None = None
|
|
||||||
self._show_info: dict[str, Any] | None = None
|
self._show_info: dict[str, Any] | None = None
|
||||||
self._name_str: str | None = None
|
self._name_str: str | None = None
|
||||||
self._friends_loading_status: ba.Actor | None = None
|
self._friends_loading_status: ba.Actor | None = None
|
||||||
|
|
@ -177,12 +176,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
.replace(' ', '_')
|
.replace(' ', '_')
|
||||||
)
|
)
|
||||||
|
|
||||||
# If game-center/etc scores are available we show our friends'
|
|
||||||
# scores. Otherwise we show our local high scores.
|
|
||||||
self._show_friend_scores = ba.internal.game_service_has_leaderboard(
|
|
||||||
self._game_name_str, self._game_config_str
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._old_best_rank = self._campaign.getlevel(
|
self._old_best_rank = self._campaign.getlevel(
|
||||||
self._level_name
|
self._level_name
|
||||||
|
|
@ -366,21 +359,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
|
|
||||||
ba.internal.set_ui_input_device(None) # Menu is up for grabs.
|
ba.internal.set_ui_input_device(None) # Menu is up for grabs.
|
||||||
|
|
||||||
if self._show_friend_scores:
|
|
||||||
ba.buttonwidget(
|
|
||||||
parent=rootc,
|
|
||||||
color=(0.45, 0.4, 0.5),
|
|
||||||
position=(h_offs - 520, v_offs + 480),
|
|
||||||
size=(300, 60),
|
|
||||||
label=ba.Lstr(resource='topFriendsText'),
|
|
||||||
on_activate_call=ba.WeakCall(self._ui_gc),
|
|
||||||
transition_delay=delay + 0.5,
|
|
||||||
icon=self._game_service_leaderboards_texture,
|
|
||||||
icon_color=self._game_service_icon_color,
|
|
||||||
autoselect=True,
|
|
||||||
selectable=can_select_extra_buttons,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self._have_achievements and self._account_has_achievements:
|
if self._have_achievements and self._account_has_achievements:
|
||||||
ba.buttonwidget(
|
ba.buttonwidget(
|
||||||
parent=rootc,
|
parent=rootc,
|
||||||
|
|
@ -773,18 +751,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
[p.name for p in self._playerinfos]
|
[p.name for p in self._playerinfos]
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._show_friend_scores:
|
|
||||||
self._friends_loading_status = Text(
|
|
||||||
ba.Lstr(
|
|
||||||
value='${A}...',
|
|
||||||
subs=[('${A}', ba.Lstr(resource='loadingText'))],
|
|
||||||
),
|
|
||||||
position=(-405, 150 + 30),
|
|
||||||
color=(1, 1, 1, 0.4),
|
|
||||||
transition=Text.Transition.FADE_IN,
|
|
||||||
scale=0.7,
|
|
||||||
transition_delay=2.0,
|
|
||||||
)
|
|
||||||
self._score_loading_status = Text(
|
self._score_loading_status = Text(
|
||||||
ba.Lstr(
|
ba.Lstr(
|
||||||
value='${A}...',
|
value='${A}...',
|
||||||
|
|
@ -850,8 +816,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
# We expect this only in kiosk mode; complain otherwise.
|
# We expect this only in kiosk mode; complain otherwise.
|
||||||
if not (ba.app.demo_mode or ba.app.arcade_mode):
|
if not (ba.app.demo_mode or ba.app.arcade_mode):
|
||||||
print('got not-signed-in at score-submit; unexpected')
|
print('got not-signed-in at score-submit; unexpected')
|
||||||
if self._show_friend_scores:
|
|
||||||
ba.pushcall(ba.WeakCall(self._got_friend_score_results, None))
|
|
||||||
ba.pushcall(ba.WeakCall(self._got_score_results, None))
|
ba.pushcall(ba.WeakCall(self._got_score_results, None))
|
||||||
else:
|
else:
|
||||||
assert self._game_name_str is not None
|
assert self._game_name_str is not None
|
||||||
|
|
@ -862,9 +826,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
name_str,
|
name_str,
|
||||||
self._score,
|
self._score,
|
||||||
ba.WeakCall(self._got_score_results),
|
ba.WeakCall(self._got_score_results),
|
||||||
ba.WeakCall(self._got_friend_score_results)
|
|
||||||
if self._show_friend_scores
|
|
||||||
else None,
|
|
||||||
order=self._score_order,
|
order=self._score_order,
|
||||||
tournament_id=self.session.tournament_id,
|
tournament_id=self.session.tournament_id,
|
||||||
score_type=self._score_type,
|
score_type=self._score_type,
|
||||||
|
|
@ -899,26 +860,6 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||||
assert txt.node
|
assert txt.node
|
||||||
txt.node.client_only = True
|
txt.node.client_only = True
|
||||||
|
|
||||||
# If we have no friend scores, display local best scores.
|
|
||||||
if self._show_friend_scores:
|
|
||||||
|
|
||||||
# Host has a button, so we need client-only text.
|
|
||||||
ts_height = 300
|
|
||||||
ts_h_offs = -480
|
|
||||||
v_offs = 40
|
|
||||||
txt = Text(
|
|
||||||
ba.Lstr(resource='topFriendsText'),
|
|
||||||
maxwidth=210,
|
|
||||||
position=(ts_h_offs - 10, ts_height / 2 + 25 + v_offs + 20),
|
|
||||||
transition=Text.Transition.IN_RIGHT,
|
|
||||||
v_align=Text.VAlign.CENTER,
|
|
||||||
scale=1.2,
|
|
||||||
transition_delay=1.8,
|
|
||||||
).autoretain()
|
|
||||||
assert txt.node
|
|
||||||
txt.node.client_only = True
|
|
||||||
else:
|
|
||||||
|
|
||||||
ts_height = 300
|
ts_height = 300
|
||||||
ts_h_offs = -480
|
ts_h_offs = -480
|
||||||
v_offs = 40
|
v_offs = 40
|
||||||
|
|
|
||||||
3
dist/ba_data/python/bastd/game/football.py
vendored
3
dist/ba_data/python/bastd/game/football.py
vendored
|
|
@ -616,9 +616,6 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
|
||||||
for bottype in self._bot_types_initial:
|
for bottype in self._bot_types_initial:
|
||||||
self._spawn_bot(bottype)
|
self._spawn_bot(bottype)
|
||||||
|
|
||||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
|
||||||
self._show_standard_scores_to_beat_ui(scores)
|
|
||||||
|
|
||||||
def _on_bot_spawn(self, spaz: SpazBot) -> None:
|
def _on_bot_spawn(self, spaz: SpazBot) -> None:
|
||||||
# We want to move to the left by default.
|
# We want to move to the left by default.
|
||||||
spaz.target_point_default = ba.Vec3(0, 0, 0)
|
spaz.target_point_default = ba.Vec3(0, 0, 0)
|
||||||
|
|
|
||||||
3
dist/ba_data/python/bastd/game/onslaught.py
vendored
3
dist/ba_data/python/bastd/game/onslaught.py
vendored
|
|
@ -682,9 +682,6 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
|
||||||
self._bots = SpazBotSet()
|
self._bots = SpazBotSet()
|
||||||
ba.timer(4.0, self._start_updating_waves)
|
ba.timer(4.0, self._start_updating_waves)
|
||||||
|
|
||||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
|
||||||
self._show_standard_scores_to_beat_ui(scores)
|
|
||||||
|
|
||||||
def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]:
|
def _get_dist_grp_totals(self, grps: list[Any]) -> tuple[int, int]:
|
||||||
totalpts = 0
|
totalpts = 0
|
||||||
totaldudes = 0
|
totaldudes = 0
|
||||||
|
|
|
||||||
3
dist/ba_data/python/bastd/game/runaround.py
vendored
3
dist/ba_data/python/bastd/game/runaround.py
vendored
|
|
@ -684,9 +684,6 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
|
||||||
self._show_standard_scores_to_beat_ui(scores)
|
|
||||||
|
|
||||||
def _update_waves(self) -> None:
|
def _update_waves(self) -> None:
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -326,9 +326,6 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
|
||||||
else:
|
else:
|
||||||
super().handlemessage(msg)
|
super().handlemessage(msg)
|
||||||
|
|
||||||
def _on_got_scores_to_beat(self, scores: list[dict[str, Any]]) -> None:
|
|
||||||
self._show_standard_scores_to_beat_ui(scores)
|
|
||||||
|
|
||||||
def end_game(self) -> None:
|
def end_game(self) -> None:
|
||||||
# Tell our bots to celebrate just to rub it in.
|
# Tell our bots to celebrate just to rub it in.
|
||||||
self._bots.final_celebrate()
|
self._bots.final_celebrate()
|
||||||
|
|
|
||||||
554
dist/ba_data/python/bastd/ui/account/settings.py
vendored
554
dist/ba_data/python/bastd/ui/account/settings.py
vendored
|
|
@ -6,13 +6,21 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import bacommon.cloud
|
||||||
|
from bacommon.login import LoginType
|
||||||
import ba
|
import ba
|
||||||
import ba.internal
|
import ba.internal
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
pass
|
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):
|
class AccountSettingsWindow(ba.Window):
|
||||||
|
|
@ -27,12 +35,17 @@ class AccountSettingsWindow(ba.Window):
|
||||||
):
|
):
|
||||||
# pylint: disable=too-many-statements
|
# pylint: disable=too-many-statements
|
||||||
|
|
||||||
self._sign_in_v2_button: ba.Widget | None = None
|
self._sign_in_v2_proxy_button: ba.Widget | None = None
|
||||||
self._sign_in_device_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
|
self._close_once_signed_in = close_once_signed_in
|
||||||
ba.set_analytics_screen('Account Window')
|
ba.set_analytics_screen('Account Window')
|
||||||
|
|
||||||
|
self._explicitly_signed_out_of_gpgs = False
|
||||||
|
|
||||||
# If they provided an origin-widget, scale up from that.
|
# If they provided an origin-widget, scale up from that.
|
||||||
scale_origin: tuple[float, float] | None
|
scale_origin: tuple[float, float] | None
|
||||||
if origin_widget is not None:
|
if origin_widget is not None:
|
||||||
|
|
@ -46,14 +59,8 @@ class AccountSettingsWindow(ba.Window):
|
||||||
self._r = 'accountSettingsWindow'
|
self._r = 'accountSettingsWindow'
|
||||||
self._modal = modal
|
self._modal = modal
|
||||||
self._needs_refresh = False
|
self._needs_refresh = False
|
||||||
self._signed_in = ba.internal.get_v1_account_state() == 'signed_in'
|
self._v1_signed_in = ba.internal.get_v1_account_state() == 'signed_in'
|
||||||
self._account_state_num = ba.internal.get_v1_account_state_num()
|
self._v1_account_state_num = ba.internal.get_v1_account_state_num()
|
||||||
self._show_linked = (
|
|
||||||
self._signed_in
|
|
||||||
and ba.internal.get_v1_account_misc_read_val(
|
|
||||||
'allowAccountLinking2', False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self._check_sign_in_timer = ba.Timer(
|
self._check_sign_in_timer = ba.Timer(
|
||||||
1.0,
|
1.0,
|
||||||
ba.WeakCall(self._update),
|
ba.WeakCall(self._update),
|
||||||
|
|
@ -62,12 +69,12 @@ class AccountSettingsWindow(ba.Window):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Currently we can only reset achievements on game-center.
|
# Currently we can only reset achievements on game-center.
|
||||||
account_type: str | None
|
v1_account_type: str | None
|
||||||
if self._signed_in:
|
if self._v1_signed_in:
|
||||||
account_type = ba.internal.get_v1_account_type()
|
v1_account_type = ba.internal.get_v1_account_type()
|
||||||
else:
|
else:
|
||||||
account_type = None
|
v1_account_type = None
|
||||||
self._can_reset_achievements = account_type == 'Game Center'
|
self._can_reset_achievements = v1_account_type == 'Game Center'
|
||||||
|
|
||||||
app = ba.app
|
app = ba.app
|
||||||
uiscale = app.ui.uiscale
|
uiscale = app.ui.uiscale
|
||||||
|
|
@ -92,16 +99,15 @@ class AccountSettingsWindow(ba.Window):
|
||||||
# Determine which sign-in/sign-out buttons we should show.
|
# Determine which sign-in/sign-out buttons we should show.
|
||||||
self._show_sign_in_buttons: list[str] = []
|
self._show_sign_in_buttons: list[str] = []
|
||||||
|
|
||||||
if app.platform == 'android' and app.subplatform == 'google':
|
if LoginType.GPGS in ba.app.accounts_v2.login_adapters:
|
||||||
self._show_sign_in_buttons.append('Google Play')
|
self._show_sign_in_buttons.append('Google Play')
|
||||||
|
|
||||||
# Local accounts are generally always available with a few key
|
# Always want to show our web-based v2 login option.
|
||||||
# exceptions.
|
self._show_sign_in_buttons.append('V2Proxy')
|
||||||
self._show_sign_in_buttons.append('Local')
|
|
||||||
|
|
||||||
# Ditto with shiny new V2 ones.
|
# Legacy v1 device accounts are currently always available
|
||||||
if bool(True):
|
# (though we need to start phasing them out at some point).
|
||||||
self._show_sign_in_buttons.append('V2')
|
self._show_sign_in_buttons.append('Device')
|
||||||
|
|
||||||
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
|
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|
@ -180,31 +186,25 @@ class AccountSettingsWindow(ba.Window):
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
|
|
||||||
# If they want us to close once we're signed in, do so.
|
# If they want us to close once we're signed in, do so.
|
||||||
if self._close_once_signed_in and self._signed_in:
|
if self._close_once_signed_in and self._v1_signed_in:
|
||||||
self._back()
|
self._back()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Hmm should update this to use get_account_state_num.
|
# Hmm should update this to use get_account_state_num.
|
||||||
# Theoretically if we switch from one signed-in account to another
|
# Theoretically if we switch from one signed-in account to another
|
||||||
# in the background this would break.
|
# in the background this would break.
|
||||||
account_state_num = ba.internal.get_v1_account_state_num()
|
v1_account_state_num = ba.internal.get_v1_account_state_num()
|
||||||
account_state = ba.internal.get_v1_account_state()
|
v1_account_state = ba.internal.get_v1_account_state()
|
||||||
|
show_legacy_unlink_button = self._should_show_legacy_unlink_button()
|
||||||
show_linked = (
|
|
||||||
self._signed_in
|
|
||||||
and ba.internal.get_v1_account_misc_read_val(
|
|
||||||
'allowAccountLinking2', False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
account_state_num != self._account_state_num
|
v1_account_state_num != self._v1_account_state_num
|
||||||
or self._show_linked != show_linked
|
or show_legacy_unlink_button != self._show_legacy_unlink_button
|
||||||
or self._needs_refresh
|
or self._needs_refresh
|
||||||
):
|
):
|
||||||
self._show_linked = show_linked
|
self._v1_account_state_num = v1_account_state_num
|
||||||
self._account_state_num = account_state_num
|
self._v1_signed_in = v1_account_state == 'signed_in'
|
||||||
self._signed_in = account_state == 'signed_in'
|
self._show_legacy_unlink_button = show_legacy_unlink_button
|
||||||
self._refresh()
|
self._refresh()
|
||||||
|
|
||||||
# Go ahead and refresh some individual things
|
# Go ahead and refresh some individual things
|
||||||
|
|
@ -226,128 +226,138 @@ class AccountSettingsWindow(ba.Window):
|
||||||
# pylint: disable=cyclic-import
|
# pylint: disable=cyclic-import
|
||||||
from bastd.ui import confirm
|
from bastd.ui import confirm
|
||||||
|
|
||||||
account_state = ba.internal.get_v1_account_state()
|
primary_v2_account = ba.app.accounts_v2.primary
|
||||||
account_type = (
|
|
||||||
|
v1_state = ba.internal.get_v1_account_state()
|
||||||
|
v1_account_type = (
|
||||||
ba.internal.get_v1_account_type()
|
ba.internal.get_v1_account_type()
|
||||||
if account_state == 'signed_in'
|
if v1_state == 'signed_in'
|
||||||
else 'unknown'
|
else 'unknown'
|
||||||
)
|
)
|
||||||
|
|
||||||
is_google = account_type == 'Google Play'
|
# 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_local_signed_in_as = False
|
show_signed_in_as = self._v1_signed_in
|
||||||
local_signed_in_as_space = 50.0
|
|
||||||
|
|
||||||
show_signed_in_as = self._signed_in
|
|
||||||
signed_in_as_space = 95.0
|
signed_in_as_space = 95.0
|
||||||
|
|
||||||
show_sign_in_benefits = not self._signed_in
|
show_sign_in_benefits = not self._v1_signed_in
|
||||||
sign_in_benefits_space = 80.0
|
sign_in_benefits_space = 80.0
|
||||||
|
|
||||||
show_signing_in_text = account_state == 'signing_in'
|
show_signing_in_text = (
|
||||||
|
v1_state == 'signing_in' or self._signing_in_adapter is not None
|
||||||
|
)
|
||||||
signing_in_text_space = 80.0
|
signing_in_text_space = 80.0
|
||||||
|
|
||||||
show_google_play_sign_in_button = (
|
show_google_play_sign_in_button = (
|
||||||
account_state == 'signed_out'
|
v1_state == 'signed_out'
|
||||||
|
and self._signing_in_adapter is None
|
||||||
and 'Google Play' in self._show_sign_in_buttons
|
and 'Google Play' in self._show_sign_in_buttons
|
||||||
)
|
)
|
||||||
show_device_sign_in_button = (
|
show_v2_proxy_sign_in_button = (
|
||||||
account_state == 'signed_out'
|
v1_state == 'signed_out'
|
||||||
and 'Local' in self._show_sign_in_buttons
|
and self._signing_in_adapter is None
|
||||||
|
and 'V2Proxy' in self._show_sign_in_buttons
|
||||||
)
|
)
|
||||||
show_v2_sign_in_button = (
|
show_device_sign_in_button = (
|
||||||
account_state == 'signed_out' and 'V2' in self._show_sign_in_buttons
|
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
|
sign_in_button_space = 70.0
|
||||||
|
deprecated_space = 60
|
||||||
|
|
||||||
show_game_service_button = self._signed_in and account_type in [
|
show_game_service_button = self._v1_signed_in and v1_account_type in [
|
||||||
'Game Center'
|
'Game Center'
|
||||||
]
|
]
|
||||||
game_service_button_space = 60.0
|
game_service_button_space = 60.0
|
||||||
|
|
||||||
show_linked_accounts_text = (
|
show_what_is_v2 = self._v1_signed_in and v1_account_type == 'V2'
|
||||||
self._signed_in
|
|
||||||
and ba.internal.get_v1_account_misc_read_val(
|
show_linked_accounts_text = self._v1_signed_in
|
||||||
'allowAccountLinking2', False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
linked_accounts_text_space = 60.0
|
linked_accounts_text_space = 60.0
|
||||||
|
|
||||||
show_achievements_button = self._signed_in and account_type in (
|
show_achievements_button = self._v1_signed_in and v1_account_type in (
|
||||||
'Google Play',
|
'Google Play',
|
||||||
'Alibaba',
|
|
||||||
'Local',
|
'Local',
|
||||||
'OUYA',
|
|
||||||
'V2',
|
'V2',
|
||||||
)
|
)
|
||||||
achievements_button_space = 60.0
|
achievements_button_space = 60.0
|
||||||
|
|
||||||
show_achievements_text = (
|
show_achievements_text = (
|
||||||
self._signed_in and not show_achievements_button
|
self._v1_signed_in and not show_achievements_button
|
||||||
)
|
)
|
||||||
achievements_text_space = 27.0
|
achievements_text_space = 27.0
|
||||||
|
|
||||||
show_leaderboards_button = self._signed_in and is_google
|
show_leaderboards_button = self._v1_signed_in and is_gpgs
|
||||||
leaderboards_button_space = 60.0
|
leaderboards_button_space = 60.0
|
||||||
|
|
||||||
show_campaign_progress = self._signed_in
|
show_campaign_progress = self._v1_signed_in
|
||||||
campaign_progress_space = 27.0
|
campaign_progress_space = 27.0
|
||||||
|
|
||||||
show_tickets = self._signed_in
|
show_tickets = self._v1_signed_in
|
||||||
tickets_space = 27.0
|
tickets_space = 27.0
|
||||||
|
|
||||||
show_reset_progress_button = False
|
show_reset_progress_button = False
|
||||||
reset_progress_button_space = 70.0
|
reset_progress_button_space = 70.0
|
||||||
|
|
||||||
show_manage_v2_account_button = (
|
show_manage_v2_account_button = (
|
||||||
self._signed_in and account_type == 'V2' and bool(False)
|
self._v1_signed_in and v1_account_type == 'V2'
|
||||||
) # Disabled for now.
|
)
|
||||||
manage_v2_account_button_space = 100.0
|
manage_v2_account_button_space = 100.0
|
||||||
|
|
||||||
show_player_profiles_button = self._signed_in
|
show_player_profiles_button = self._v1_signed_in
|
||||||
player_profiles_button_space = (
|
player_profiles_button_space = (
|
||||||
70.0 if show_manage_v2_account_button else 100.0
|
70.0 if show_manage_v2_account_button else 100.0
|
||||||
)
|
)
|
||||||
|
|
||||||
show_link_accounts_button = (
|
show_link_accounts_button = self._v1_signed_in and (
|
||||||
self._signed_in
|
primary_v2_account is None or FORCE_ENABLE_V1_LINKING
|
||||||
and ba.internal.get_v1_account_misc_read_val(
|
|
||||||
'allowAccountLinking2', False
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
link_accounts_button_space = 70.0
|
link_accounts_button_space = 70.0
|
||||||
|
|
||||||
show_unlink_accounts_button = show_link_accounts_button
|
show_unlink_accounts_button = show_link_accounts_button
|
||||||
unlink_accounts_button_space = 90.0
|
unlink_accounts_button_space = 90.0
|
||||||
|
|
||||||
show_sign_out_button = self._signed_in and account_type in [
|
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',
|
'Local',
|
||||||
'Google Play',
|
'Google Play',
|
||||||
'V2',
|
'V2',
|
||||||
]
|
]
|
||||||
sign_out_button_space = 70.0
|
sign_out_button_space = 70.0
|
||||||
|
|
||||||
show_cancel_v2_sign_in_button = (
|
# We can show cancel if we're either waiting on an adapter to
|
||||||
account_state == 'signing_in'
|
# provide us with v2 credentials or waiting for those credentials
|
||||||
and ba.app.accounts_v2.have_primary_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_v2_sign_in_button_space = 70.0
|
cancel_sign_in_button_space = 70.0
|
||||||
|
|
||||||
if self._subcontainer is not None:
|
if self._subcontainer is not None:
|
||||||
self._subcontainer.delete()
|
self._subcontainer.delete()
|
||||||
self._sub_height = 60.0
|
self._sub_height = 60.0
|
||||||
if show_local_signed_in_as:
|
|
||||||
self._sub_height += local_signed_in_as_space
|
|
||||||
if show_signed_in_as:
|
if show_signed_in_as:
|
||||||
self._sub_height += signed_in_as_space
|
self._sub_height += signed_in_as_space
|
||||||
if show_signing_in_text:
|
if show_signing_in_text:
|
||||||
self._sub_height += signing_in_text_space
|
self._sub_height += signing_in_text_space
|
||||||
if show_google_play_sign_in_button:
|
if show_google_play_sign_in_button:
|
||||||
self._sub_height += sign_in_button_space
|
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:
|
if show_device_sign_in_button:
|
||||||
self._sub_height += sign_in_button_space
|
self._sub_height += sign_in_button_space + deprecated_space
|
||||||
if show_v2_sign_in_button:
|
|
||||||
self._sub_height += sign_in_button_space
|
|
||||||
if show_game_service_button:
|
if show_game_service_button:
|
||||||
self._sub_height += game_service_button_space
|
self._sub_height += game_service_button_space
|
||||||
if show_linked_accounts_text:
|
if show_linked_accounts_text:
|
||||||
|
|
@ -374,10 +384,14 @@ class AccountSettingsWindow(ba.Window):
|
||||||
self._sub_height += link_accounts_button_space
|
self._sub_height += link_accounts_button_space
|
||||||
if show_unlink_accounts_button:
|
if show_unlink_accounts_button:
|
||||||
self._sub_height += unlink_accounts_button_space
|
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:
|
if show_sign_out_button:
|
||||||
self._sub_height += sign_out_button_space
|
self._sub_height += sign_out_button_space
|
||||||
if show_cancel_v2_sign_in_button:
|
if show_cancel_sign_in_button:
|
||||||
self._sub_height += cancel_v2_sign_in_button_space
|
self._sub_height += cancel_sign_in_button_space
|
||||||
self._subcontainer = ba.containerwidget(
|
self._subcontainer = ba.containerwidget(
|
||||||
parent=self._scrollwidget,
|
parent=self._scrollwidget,
|
||||||
size=(self._sub_width, self._sub_height),
|
size=(self._sub_width, self._sub_height),
|
||||||
|
|
@ -390,27 +404,8 @@ class AccountSettingsWindow(ba.Window):
|
||||||
first_selectable = None
|
first_selectable = None
|
||||||
v = self._sub_height - 10.0
|
v = self._sub_height - 10.0
|
||||||
|
|
||||||
if show_local_signed_in_as:
|
self._account_name_what_is_text: ba.Widget | None
|
||||||
v -= local_signed_in_as_space * 0.6
|
self._account_name_what_is_y = 0.0
|
||||||
ba.textwidget(
|
|
||||||
parent=self._subcontainer,
|
|
||||||
position=(self._sub_width * 0.5, v),
|
|
||||||
size=(0, 0),
|
|
||||||
text=ba.Lstr(
|
|
||||||
resource='accountSettingsWindow.deviceSpecificAccountText',
|
|
||||||
subs=[
|
|
||||||
('${NAME}', ba.internal.get_v1_account_display_string())
|
|
||||||
],
|
|
||||||
),
|
|
||||||
scale=0.7,
|
|
||||||
color=(0.5, 0.5, 0.6),
|
|
||||||
maxwidth=self._sub_width * 0.9,
|
|
||||||
flatness=1.0,
|
|
||||||
h_align='center',
|
|
||||||
v_align='center',
|
|
||||||
)
|
|
||||||
v -= local_signed_in_as_space * 0.4
|
|
||||||
|
|
||||||
self._account_name_text: ba.Widget | None
|
self._account_name_text: ba.Widget | None
|
||||||
if show_signed_in_as:
|
if show_signed_in_as:
|
||||||
v -= signed_in_as_space * 0.2
|
v -= signed_in_as_space * 0.2
|
||||||
|
|
@ -429,7 +424,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
h_align='center',
|
h_align='center',
|
||||||
v_align='center',
|
v_align='center',
|
||||||
)
|
)
|
||||||
v -= signed_in_as_space * 0.4
|
v -= signed_in_as_space * 0.5
|
||||||
self._account_name_text = ba.textwidget(
|
self._account_name_text = ba.textwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
position=(self._sub_width * 0.5, v),
|
position=(self._sub_width * 0.5, v),
|
||||||
|
|
@ -441,10 +436,39 @@ class AccountSettingsWindow(ba.Window):
|
||||||
h_align='center',
|
h_align='center',
|
||||||
v_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()
|
self._refresh_account_name_text()
|
||||||
|
|
||||||
v -= signed_in_as_space * 0.4
|
v -= signed_in_as_space * 0.4
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._account_name_text = None
|
self._account_name_text = None
|
||||||
|
self._account_name_what_is_text = None
|
||||||
|
|
||||||
if self._back_button is None:
|
if self._back_button is None:
|
||||||
bbtn = ba.internal.get_special_widget('back_button')
|
bbtn = ba.internal.get_special_widget('back_button')
|
||||||
|
|
@ -528,7 +552,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
on_activate_call=lambda: self._sign_in_press('Google Play'),
|
on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
|
||||||
)
|
)
|
||||||
if first_selectable is None:
|
if first_selectable is None:
|
||||||
first_selectable = btn
|
first_selectable = btn
|
||||||
|
|
@ -541,16 +565,16 @@ class AccountSettingsWindow(ba.Window):
|
||||||
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
|
ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
|
||||||
self._sign_in_text = None
|
self._sign_in_text = None
|
||||||
|
|
||||||
if show_v2_sign_in_button:
|
if show_v2_proxy_sign_in_button:
|
||||||
button_width = 350
|
button_width = 350
|
||||||
v -= sign_in_button_space
|
v -= sign_in_button_space
|
||||||
self._sign_in_v2_button = btn = ba.buttonwidget(
|
self._sign_in_v2_proxy_button = btn = ba.buttonwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
position=((self._sub_width - button_width) * 0.5, v - 20),
|
position=((self._sub_width - button_width) * 0.5, v - 20),
|
||||||
autoselect=True,
|
autoselect=True,
|
||||||
size=(button_width, 60),
|
size=(button_width, 60),
|
||||||
label='',
|
label='',
|
||||||
on_activate_call=self._v2_sign_in_press,
|
on_activate_call=self._v2_proxy_sign_in_press,
|
||||||
)
|
)
|
||||||
ba.textwidget(
|
ba.textwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
|
|
@ -598,7 +622,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
|
|
||||||
if show_device_sign_in_button:
|
if show_device_sign_in_button:
|
||||||
button_width = 350
|
button_width = 350
|
||||||
v -= sign_in_button_space
|
v -= sign_in_button_space + deprecated_space
|
||||||
self._sign_in_device_button = btn = ba.buttonwidget(
|
self._sign_in_device_button = btn = ba.buttonwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
position=((self._sub_width - button_width) * 0.5, v - 20),
|
position=((self._sub_width - button_width) * 0.5, v - 20),
|
||||||
|
|
@ -607,6 +631,18 @@ class AccountSettingsWindow(ba.Window):
|
||||||
label='',
|
label='',
|
||||||
on_activate_call=lambda: self._sign_in_press('Local'),
|
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(
|
ba.textwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
draw_controller=btn,
|
draw_controller=btn,
|
||||||
|
|
@ -663,9 +699,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
color=(0.55, 0.5, 0.6),
|
color=(0.55, 0.5, 0.6),
|
||||||
icon=ba.gettexture('settingsIcon'),
|
icon=ba.gettexture('settingsIcon'),
|
||||||
textcolor=(0.75, 0.7, 0.8),
|
textcolor=(0.75, 0.7, 0.8),
|
||||||
on_activate_call=lambda: ba.open_url(
|
on_activate_call=ba.WeakCall(self._on_manage_account_press),
|
||||||
'https://ballistica.net/accountsettings'
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if first_selectable is None:
|
if first_selectable is None:
|
||||||
first_selectable = btn
|
first_selectable = btn
|
||||||
|
|
@ -703,12 +737,12 @@ class AccountSettingsWindow(ba.Window):
|
||||||
if show_game_service_button:
|
if show_game_service_button:
|
||||||
button_width = 300
|
button_width = 300
|
||||||
v -= game_service_button_space * 0.85
|
v -= game_service_button_space * 0.85
|
||||||
account_type = ba.internal.get_v1_account_type()
|
v1_account_type = ba.internal.get_v1_account_type()
|
||||||
if account_type == 'Game Center':
|
if v1_account_type == 'Game Center':
|
||||||
account_type_name = ba.Lstr(resource='gameCenterText')
|
v1_account_type_name = ba.Lstr(resource='gameCenterText')
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"unknown account type: '" + str(account_type) + "'"
|
"unknown account type: '" + str(v1_account_type) + "'"
|
||||||
)
|
)
|
||||||
self._game_service_button = btn = ba.buttonwidget(
|
self._game_service_button = btn = ba.buttonwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
|
|
@ -718,7 +752,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
autoselect=True,
|
autoselect=True,
|
||||||
on_activate_call=ba.internal.show_online_score_ui,
|
on_activate_call=ba.internal.show_online_score_ui,
|
||||||
size=(button_width, 50),
|
size=(button_width, 50),
|
||||||
label=account_type_name,
|
label=v1_account_type_name,
|
||||||
)
|
)
|
||||||
if first_selectable is None:
|
if first_selectable is None:
|
||||||
first_selectable = btn
|
first_selectable = btn
|
||||||
|
|
@ -761,11 +795,15 @@ class AccountSettingsWindow(ba.Window):
|
||||||
autoselect=True,
|
autoselect=True,
|
||||||
icon=ba.gettexture(
|
icon=ba.gettexture(
|
||||||
'googlePlayAchievementsIcon'
|
'googlePlayAchievementsIcon'
|
||||||
if is_google
|
if is_gpgs
|
||||||
else 'achievementsIcon'
|
else 'achievementsIcon'
|
||||||
),
|
),
|
||||||
icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9),
|
icon_color=(0.8, 0.95, 0.7) if is_gpgs else (0.85, 0.8, 0.9),
|
||||||
on_activate_call=self._on_achievements_press,
|
on_activate_call=(
|
||||||
|
self._on_custom_achievements_press
|
||||||
|
if is_gpgs
|
||||||
|
else self._on_achievements_press
|
||||||
|
),
|
||||||
size=(button_width, 50),
|
size=(button_width, 50),
|
||||||
label='',
|
label='',
|
||||||
)
|
)
|
||||||
|
|
@ -897,6 +935,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
scale=0.9,
|
scale=0.9,
|
||||||
color=(0.75, 0.7, 0.8),
|
color=(0.75, 0.7, 0.8),
|
||||||
maxwidth=self._sub_width * 0.95,
|
maxwidth=self._sub_width * 0.95,
|
||||||
|
text=ba.Lstr(resource=self._r + '.linkedAccountsText'),
|
||||||
h_align='center',
|
h_align='center',
|
||||||
v_align='center',
|
v_align='center',
|
||||||
)
|
)
|
||||||
|
|
@ -905,6 +944,8 @@ class AccountSettingsWindow(ba.Window):
|
||||||
else:
|
else:
|
||||||
self._linked_accounts_text = None
|
self._linked_accounts_text = None
|
||||||
|
|
||||||
|
# Show link/unlink buttons only for V1 accounts.
|
||||||
|
|
||||||
if show_link_accounts_button:
|
if show_link_accounts_button:
|
||||||
v -= link_accounts_button_space
|
v -= link_accounts_button_space
|
||||||
self._link_accounts_button = btn = ba.buttonwidget(
|
self._link_accounts_button = btn = ba.buttonwidget(
|
||||||
|
|
@ -984,6 +1025,50 @@ class AccountSettingsWindow(ba.Window):
|
||||||
else:
|
else:
|
||||||
self._unlink_accounts_button = None
|
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:
|
if show_sign_out_button:
|
||||||
v -= sign_out_button_space
|
v -= sign_out_button_space
|
||||||
self._sign_out_button = btn = ba.buttonwidget(
|
self._sign_out_button = btn = ba.buttonwidget(
|
||||||
|
|
@ -1005,9 +1090,9 @@ class AccountSettingsWindow(ba.Window):
|
||||||
)
|
)
|
||||||
ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
|
ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
|
||||||
|
|
||||||
if show_cancel_v2_sign_in_button:
|
if show_cancel_sign_in_button:
|
||||||
v -= cancel_v2_sign_in_button_space
|
v -= cancel_sign_in_button_space
|
||||||
self._cancel_v2_sign_in_button = btn = ba.buttonwidget(
|
self._cancel_sign_in_button = btn = ba.buttonwidget(
|
||||||
parent=self._subcontainer,
|
parent=self._subcontainer,
|
||||||
position=((self._sub_width - button_width) * 0.5, v),
|
position=((self._sub_width - button_width) * 0.5, v),
|
||||||
size=(button_width, 60),
|
size=(button_width, 60),
|
||||||
|
|
@ -1015,7 +1100,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
color=(0.55, 0.5, 0.6),
|
color=(0.55, 0.5, 0.6),
|
||||||
textcolor=(0.75, 0.7, 0.8),
|
textcolor=(0.75, 0.7, 0.8),
|
||||||
autoselect=True,
|
autoselect=True,
|
||||||
on_activate_call=self._cancel_v2_sign_in_press,
|
on_activate_call=self._cancel_sign_in_press,
|
||||||
)
|
)
|
||||||
if first_selectable is None:
|
if first_selectable is None:
|
||||||
first_selectable = btn
|
first_selectable = btn
|
||||||
|
|
@ -1038,33 +1123,51 @@ class AccountSettingsWindow(ba.Window):
|
||||||
)
|
)
|
||||||
self._needs_refresh = False
|
self._needs_refresh = False
|
||||||
|
|
||||||
def _on_achievements_press(self) -> None:
|
def _on_custom_achievements_press(self) -> None:
|
||||||
# pylint: disable=cyclic-import
|
|
||||||
from bastd.ui import achievements
|
|
||||||
|
|
||||||
account_state = ba.internal.get_v1_account_state()
|
|
||||||
account_type = (
|
|
||||||
ba.internal.get_v1_account_type()
|
|
||||||
if account_state == 'signed_in'
|
|
||||||
else 'unknown'
|
|
||||||
)
|
|
||||||
# for google play we use the built-in UI; otherwise pop up our own
|
|
||||||
if account_type == 'Google Play':
|
|
||||||
ba.timer(
|
ba.timer(
|
||||||
0.15,
|
0.15,
|
||||||
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
|
ba.Call(ba.internal.show_online_score_ui, 'achievements'),
|
||||||
timetype=ba.TimeType.REAL,
|
timetype=ba.TimeType.REAL,
|
||||||
)
|
)
|
||||||
elif account_type != 'unknown':
|
|
||||||
|
def _on_achievements_press(self) -> None:
|
||||||
|
# pylint: disable=cyclic-import
|
||||||
|
from bastd.ui import achievements
|
||||||
|
|
||||||
assert self._achievements_button is not None
|
assert self._achievements_button is not None
|
||||||
achievements.AchievementsWindow(
|
achievements.AchievementsWindow(
|
||||||
position=self._achievements_button.get_screen_space_center()
|
position=self._achievements_button.get_screen_space_center()
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
print(
|
def _on_what_is_v2_press(self) -> None:
|
||||||
'ERROR: unknown account type in on_achievements_press:',
|
show_what_is_v2_page()
|
||||||
account_type,
|
|
||||||
|
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:
|
def _on_leaderboards_press(self) -> None:
|
||||||
ba.timer(
|
ba.timer(
|
||||||
|
|
@ -1073,7 +1176,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
timetype=ba.TimeType.REAL,
|
timetype=ba.TimeType.REAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _have_unlinkable_accounts(self) -> bool:
|
def _have_unlinkable_v1_accounts(self) -> bool:
|
||||||
# if this is not present, we haven't had contact from the server so
|
# if this is not present, we haven't had contact from the server so
|
||||||
# let's not proceed..
|
# let's not proceed..
|
||||||
if ba.internal.get_public_login_id() is None:
|
if ba.internal.get_public_login_id() is None:
|
||||||
|
|
@ -1086,16 +1189,33 @@ class AccountSettingsWindow(ba.Window):
|
||||||
def _update_unlink_accounts_button(self) -> None:
|
def _update_unlink_accounts_button(self) -> None:
|
||||||
if self._unlink_accounts_button is None:
|
if self._unlink_accounts_button is None:
|
||||||
return
|
return
|
||||||
if self._have_unlinkable_accounts():
|
if self._have_unlinkable_v1_accounts():
|
||||||
clr = (0.75, 0.7, 0.8, 1.0)
|
clr = (0.75, 0.7, 0.8, 1.0)
|
||||||
else:
|
else:
|
||||||
clr = (1.0, 1.0, 1.0, 0.25)
|
clr = (1.0, 1.0, 1.0, 0.25)
|
||||||
ba.textwidget(edit=self._unlink_accounts_button_label, color=clr)
|
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:
|
def _update_linked_accounts_text(self) -> None:
|
||||||
if self._linked_accounts_text is None:
|
if self._linked_accounts_text is None:
|
||||||
return
|
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
|
# if this is not present, we haven't had contact from the server so
|
||||||
# let's not proceed..
|
# let's not proceed..
|
||||||
if ba.internal.get_public_login_id() is None:
|
if ba.internal.get_public_login_id() is None:
|
||||||
|
|
@ -1105,13 +1225,9 @@ class AccountSettingsWindow(ba.Window):
|
||||||
accounts = ba.internal.get_v1_account_misc_read_val_2(
|
accounts = ba.internal.get_v1_account_misc_read_val_2(
|
||||||
'linkedAccounts', []
|
'linkedAccounts', []
|
||||||
)
|
)
|
||||||
# our_account = _bs.get_v1_account_display_string()
|
|
||||||
# accounts = [a for a in accounts if a != our_account]
|
|
||||||
# accounts_str = u', '.join(accounts) if accounts else
|
|
||||||
# ba.Lstr(translate=('settingNames', 'None'))
|
|
||||||
# UPDATE - we now just print the number here; not the actual
|
# UPDATE - we now just print the number here; not the actual
|
||||||
# accounts
|
# accounts (they can see that in the unlink section if they're
|
||||||
# (they can see that in the unlink section if they're curious)
|
# curious)
|
||||||
accounts_str = str(max(0, len(accounts) - 1))
|
accounts_str = str(max(0, len(accounts) - 1))
|
||||||
ba.textwidget(
|
ba.textwidget(
|
||||||
edit=self._linked_accounts_text,
|
edit=self._linked_accounts_text,
|
||||||
|
|
@ -1162,6 +1278,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _refresh_account_name_text(self) -> None:
|
def _refresh_account_name_text(self) -> None:
|
||||||
|
|
||||||
if self._account_name_text is None:
|
if self._account_name_text is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|
@ -1169,7 +1286,20 @@ class AccountSettingsWindow(ba.Window):
|
||||||
except Exception:
|
except Exception:
|
||||||
ba.print_exception()
|
ba.print_exception()
|
||||||
name_str = '??'
|
name_str = '??'
|
||||||
|
|
||||||
ba.textwidget(edit=self._account_name_text, text=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:
|
def _refresh_achievements(self) -> None:
|
||||||
if (
|
if (
|
||||||
|
|
@ -1199,7 +1329,7 @@ class AccountSettingsWindow(ba.Window):
|
||||||
# pylint: disable=cyclic-import
|
# pylint: disable=cyclic-import
|
||||||
from bastd.ui.account import unlink
|
from bastd.ui.account import unlink
|
||||||
|
|
||||||
if not self._have_unlinkable_accounts():
|
if not self._have_unlinkable_v1_accounts():
|
||||||
ba.playsound(ba.getsound('error'))
|
ba.playsound(ba.getsound('error'))
|
||||||
return
|
return
|
||||||
unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
|
unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
|
||||||
|
|
@ -1214,16 +1344,27 @@ class AccountSettingsWindow(ba.Window):
|
||||||
origin_widget=self._player_profiles_button
|
origin_widget=self._player_profiles_button
|
||||||
)
|
)
|
||||||
|
|
||||||
def _cancel_v2_sign_in_press(self) -> None:
|
def _cancel_sign_in_press(self) -> None:
|
||||||
# Just say we don't wanna be signed in anymore.
|
|
||||||
|
# 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)
|
ba.app.accounts_v2.set_primary_credentials(None)
|
||||||
|
|
||||||
|
self._needs_refresh = True
|
||||||
|
|
||||||
# Speed UI updates along.
|
# Speed UI updates along.
|
||||||
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
||||||
|
|
||||||
def _sign_out_press(self) -> None:
|
def _sign_out_press(self) -> None:
|
||||||
|
|
||||||
if ba.app.accounts_v2.have_primary_credentials():
|
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)
|
ba.app.accounts_v2.set_primary_credentials(None)
|
||||||
else:
|
else:
|
||||||
ba.internal.sign_out_v1()
|
ba.internal.sign_out_v1()
|
||||||
|
|
@ -1242,25 +1383,90 @@ class AccountSettingsWindow(ba.Window):
|
||||||
# Speed UI updates along.
|
# Speed UI updates along.
|
||||||
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
||||||
|
|
||||||
def _sign_in_press(
|
def _sign_in_press(self, login_type: str | LoginType) -> None:
|
||||||
self, account_type: str, show_test_warning: bool = True
|
|
||||||
) -> None:
|
|
||||||
del show_test_warning # unused
|
|
||||||
ba.internal.sign_in_v1(account_type)
|
|
||||||
|
|
||||||
# Make note of the type account we're *wanting* to be signed in with.
|
# 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 = ba.app.config
|
||||||
cfg['Auto Account State'] = account_type
|
cfg['Auto Account State'] = login_type
|
||||||
cfg.commit()
|
cfg.commit()
|
||||||
self._needs_refresh = True
|
self._needs_refresh = True
|
||||||
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
|
||||||
|
return
|
||||||
|
|
||||||
def _v2_sign_in_press(self) -> None:
|
# 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)
|
||||||
|
)
|
||||||
|
# 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
|
# pylint: disable=cyclic-import
|
||||||
from bastd.ui.account.v2 import V2SignInWindow
|
from bastd.ui.account.v2proxy import V2ProxySignInWindow
|
||||||
|
|
||||||
assert self._sign_in_v2_button is not None
|
assert self._sign_in_v2_proxy_button is not None
|
||||||
V2SignInWindow(origin_widget=self._sign_in_v2_button)
|
V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
|
||||||
|
|
||||||
def _reset_progress(self) -> None:
|
def _reset_progress(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1319,3 +1525,15 @@ class AccountSettingsWindow(ba.Window):
|
||||||
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
ba.containerwidget(edit=self._root_widget, selected_child=sel)
|
||||||
except Exception:
|
except Exception:
|
||||||
ba.print_exception(f'Error restoring state for {self}.')
|
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')
|
||||||
|
|
|
||||||
236
dist/ba_data/python/bastd/ui/account/v2proxy.py
vendored
Normal file
236
dist/ba_data/python/bastd/ui/account/v2proxy.py
vendored
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""V2 account ui bits."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import ba
|
||||||
|
import ba.internal
|
||||||
|
|
||||||
|
from efro.error import CommunicationError
|
||||||
|
import bacommon.cloud
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
STATUS_CHECK_INTERVAL_SECONDS = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
class V2ProxySignInWindow(ba.Window):
|
||||||
|
"""A window allowing signing in to a v2 account."""
|
||||||
|
|
||||||
|
def __init__(self, origin_widget: ba.Widget):
|
||||||
|
self._width = 600
|
||||||
|
self._height = 550
|
||||||
|
self._proxyid: str | None = None
|
||||||
|
self._proxykey: str | None = None
|
||||||
|
|
||||||
|
uiscale = ba.app.ui.uiscale
|
||||||
|
super().__init__(
|
||||||
|
root_widget=ba.containerwidget(
|
||||||
|
size=(self._width, self._height),
|
||||||
|
transition='in_scale',
|
||||||
|
scale_origin_stack_offset=(
|
||||||
|
origin_widget.get_screen_space_center()
|
||||||
|
),
|
||||||
|
scale=(
|
||||||
|
1.25
|
||||||
|
if uiscale is ba.UIScale.SMALL
|
||||||
|
else 1.05
|
||||||
|
if uiscale is ba.UIScale.MEDIUM
|
||||||
|
else 0.9
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._loading_text = ba.textwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5, self._height * 0.5),
|
||||||
|
h_align='center',
|
||||||
|
v_align='center',
|
||||||
|
size=(0, 0),
|
||||||
|
maxwidth=0.9 * self._width,
|
||||||
|
text=ba.Lstr(
|
||||||
|
value='${A}...',
|
||||||
|
subs=[('${A}', ba.Lstr(resource='loadingText'))],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self._cancel_button = ba.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(30, self._height - 65),
|
||||||
|
size=(130, 50),
|
||||||
|
scale=0.8,
|
||||||
|
label=ba.Lstr(resource='cancelText'),
|
||||||
|
on_activate_call=self._done,
|
||||||
|
autoselect=True,
|
||||||
|
color=(0.55, 0.5, 0.6),
|
||||||
|
textcolor=(0.75, 0.7, 0.8),
|
||||||
|
)
|
||||||
|
|
||||||
|
if bool(False):
|
||||||
|
ba.containerwidget(
|
||||||
|
edit=self._root_widget, cancel_button=self._cancel_button
|
||||||
|
)
|
||||||
|
|
||||||
|
self._update_timer: ba.Timer | None = None
|
||||||
|
|
||||||
|
# Ask the cloud for a proxy login id.
|
||||||
|
ba.app.cloud.send_message_cb(
|
||||||
|
bacommon.cloud.LoginProxyRequestMessage(),
|
||||||
|
on_response=ba.WeakCall(self._on_proxy_request_response),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_proxy_request_response(
|
||||||
|
self, response: bacommon.cloud.LoginProxyRequestResponse | Exception
|
||||||
|
) -> None:
|
||||||
|
from ba.internal import is_browser_likely_available
|
||||||
|
|
||||||
|
# Something went wrong. Show an error message and that's it.
|
||||||
|
if isinstance(response, Exception):
|
||||||
|
ba.textwidget(
|
||||||
|
edit=self._loading_text,
|
||||||
|
text=ba.Lstr(resource='internal.unavailableNoConnectionText'),
|
||||||
|
color=(1, 0, 0),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Show link(s) the user can use to sign in.
|
||||||
|
address = (
|
||||||
|
ba.internal.get_master_server_address(version=2) + response.url
|
||||||
|
)
|
||||||
|
address_pretty = address.removeprefix('https://')
|
||||||
|
|
||||||
|
ba.textwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5, self._height - 95),
|
||||||
|
size=(0, 0),
|
||||||
|
text=ba.Lstr(
|
||||||
|
resource='accountSettingsWindow.v2LinkInstructionsText'
|
||||||
|
),
|
||||||
|
color=ba.app.ui.title_color,
|
||||||
|
maxwidth=self._width * 0.9,
|
||||||
|
h_align='center',
|
||||||
|
v_align='center',
|
||||||
|
)
|
||||||
|
button_width = 450
|
||||||
|
if is_browser_likely_available():
|
||||||
|
ba.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(
|
||||||
|
(self._width * 0.5 - button_width * 0.5),
|
||||||
|
self._height - 185,
|
||||||
|
),
|
||||||
|
autoselect=True,
|
||||||
|
size=(button_width, 60),
|
||||||
|
label=ba.Lstr(value=address_pretty),
|
||||||
|
color=(0.55, 0.5, 0.6),
|
||||||
|
textcolor=(0.75, 0.7, 0.8),
|
||||||
|
on_activate_call=lambda: ba.open_url(address),
|
||||||
|
)
|
||||||
|
qroffs = 0.0
|
||||||
|
else:
|
||||||
|
ba.textwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5, self._height - 145),
|
||||||
|
size=(0, 0),
|
||||||
|
text=ba.Lstr(value=address_pretty),
|
||||||
|
flatness=1.0,
|
||||||
|
maxwidth=self._width,
|
||||||
|
scale=0.75,
|
||||||
|
h_align='center',
|
||||||
|
v_align='center',
|
||||||
|
)
|
||||||
|
qroffs = 20.0
|
||||||
|
|
||||||
|
qr_size = 270
|
||||||
|
ba.imagewidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(
|
||||||
|
self._width * 0.5 - qr_size * 0.5,
|
||||||
|
self._height * 0.36 + qroffs - qr_size * 0.5,
|
||||||
|
),
|
||||||
|
size=(qr_size, qr_size),
|
||||||
|
texture=ba.internal.get_qrcode_texture(address),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Start querying for results.
|
||||||
|
self._proxyid = response.proxyid
|
||||||
|
self._proxykey = response.proxykey
|
||||||
|
ba.timer(
|
||||||
|
STATUS_CHECK_INTERVAL_SECONDS, ba.WeakCall(self._ask_for_status)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ask_for_status(self) -> None:
|
||||||
|
assert self._proxyid is not None
|
||||||
|
assert self._proxykey is not None
|
||||||
|
ba.app.cloud.send_message_cb(
|
||||||
|
bacommon.cloud.LoginProxyStateQueryMessage(
|
||||||
|
proxyid=self._proxyid, proxykey=self._proxykey
|
||||||
|
),
|
||||||
|
on_response=ba.WeakCall(self._got_status),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _got_status(
|
||||||
|
self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# For now, if anything goes wrong on the server-side, just abort
|
||||||
|
# with a vague error message. Can be more verbose later if need be.
|
||||||
|
if (
|
||||||
|
isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
|
||||||
|
and response.state is response.State.FAIL
|
||||||
|
):
|
||||||
|
ba.playsound(ba.getsound('error'))
|
||||||
|
ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
|
||||||
|
self._done()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we got a token, set ourself as signed in. Hooray!
|
||||||
|
if (
|
||||||
|
isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
|
||||||
|
and response.state is response.State.SUCCESS
|
||||||
|
):
|
||||||
|
assert response.credentials is not None
|
||||||
|
ba.app.accounts_v2.set_primary_credentials(response.credentials)
|
||||||
|
|
||||||
|
# As a courtesy, tell the server we're done with this proxy
|
||||||
|
# so it can clean up (not a huge deal if this fails)
|
||||||
|
assert self._proxyid is not None
|
||||||
|
try:
|
||||||
|
ba.app.cloud.send_message_cb(
|
||||||
|
bacommon.cloud.LoginProxyCompleteMessage(
|
||||||
|
proxyid=self._proxyid
|
||||||
|
),
|
||||||
|
on_response=ba.WeakCall(self._proxy_complete_response),
|
||||||
|
)
|
||||||
|
except CommunicationError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logging.warning(
|
||||||
|
'Unexpected error sending login-proxy-complete message',
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._done()
|
||||||
|
return
|
||||||
|
|
||||||
|
# If we're still waiting, ask again soon.
|
||||||
|
if (
|
||||||
|
isinstance(response, Exception)
|
||||||
|
or response.state is response.State.WAITING
|
||||||
|
):
|
||||||
|
ba.timer(
|
||||||
|
STATUS_CHECK_INTERVAL_SECONDS, ba.WeakCall(self._ask_for_status)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _proxy_complete_response(self, response: None | Exception) -> None:
|
||||||
|
del response # Not used.
|
||||||
|
# We could do something smart like retry on exceptions here, but
|
||||||
|
# this isn't critical so we'll just let anything slide.
|
||||||
|
|
||||||
|
def _done(self) -> None:
|
||||||
|
ba.containerwidget(edit=self._root_widget, transition='out_scale')
|
||||||
3
dist/ba_data/python/bastd/ui/appinvite.py
vendored
3
dist/ba_data/python/bastd/ui/appinvite.py
vendored
|
|
@ -413,6 +413,9 @@ def handle_app_invites_press(force_code: bool = False) -> None:
|
||||||
and ba.internal.get_v1_account_misc_read_val('enableAppInvites', False)
|
and ba.internal.get_v1_account_misc_read_val('enableAppInvites', False)
|
||||||
and not app.on_tv
|
and not app.on_tv
|
||||||
)
|
)
|
||||||
|
# Update: google's app invites are deprecated.
|
||||||
|
do_app_invites = False
|
||||||
|
|
||||||
if force_code:
|
if force_code:
|
||||||
do_app_invites = False
|
do_app_invites = False
|
||||||
|
|
||||||
|
|
|
||||||
6
dist/ba_data/python/bastd/ui/configerror.py
vendored
6
dist/ba_data/python/bastd/ui/configerror.py
vendored
|
|
@ -20,12 +20,12 @@ class ConfigErrorWindow(ba.Window):
|
||||||
self._config_file_path = ba.app.config_file_path
|
self._config_file_path = ba.app.config_file_path
|
||||||
width = 800
|
width = 800
|
||||||
super().__init__(
|
super().__init__(
|
||||||
ba.containerwidget(size=(width, 300), transition='in_right')
|
ba.containerwidget(size=(width, 400), transition='in_right')
|
||||||
)
|
)
|
||||||
padding = 20
|
padding = 20
|
||||||
ba.textwidget(
|
ba.textwidget(
|
||||||
parent=self._root_widget,
|
parent=self._root_widget,
|
||||||
position=(padding, 220),
|
position=(padding, 220 + 60),
|
||||||
size=(width - 2 * padding, 100 - 2 * padding),
|
size=(width - 2 * padding, 100 - 2 * padding),
|
||||||
h_align='center',
|
h_align='center',
|
||||||
v_align='top',
|
v_align='top',
|
||||||
|
|
@ -41,7 +41,7 @@ class ConfigErrorWindow(ba.Window):
|
||||||
)
|
)
|
||||||
ba.textwidget(
|
ba.textwidget(
|
||||||
parent=self._root_widget,
|
parent=self._root_widget,
|
||||||
position=(padding, 198),
|
position=(padding, 198 + 60),
|
||||||
size=(width - 2 * padding, 100 - 2 * padding),
|
size=(width - 2 * padding, 100 - 2 * padding),
|
||||||
h_align='center',
|
h_align='center',
|
||||||
v_align='top',
|
v_align='top',
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class GatherTab:
|
||||||
The tab should create and return a container widget covering the
|
The tab should create and return a container widget covering the
|
||||||
specified region.
|
specified region.
|
||||||
"""
|
"""
|
||||||
|
raise RuntimeError('Should not get here.')
|
||||||
|
|
||||||
def on_deactivate(self) -> None:
|
def on_deactivate(self) -> None:
|
||||||
"""Called when the tab will no longer be the active one."""
|
"""Called when the tab will no longer be the active one."""
|
||||||
|
|
|
||||||
|
|
@ -1094,7 +1094,6 @@ class PublicGatherTab(GatherTab):
|
||||||
|
|
||||||
self._parties_sorted.sort(
|
self._parties_sorted.sort(
|
||||||
key=lambda p: (
|
key=lambda p: (
|
||||||
p[1].queue is None, # Show non-queued last.
|
|
||||||
p[1].ping if p[1].ping is not None else 999999.0,
|
p[1].ping if p[1].ping is not None else 999999.0,
|
||||||
p[1].index,
|
p[1].index,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
15
dist/ba_data/python/bastd/ui/mainmenu.py
vendored
15
dist/ba_data/python/bastd/ui/mainmenu.py
vendored
|
|
@ -515,7 +515,20 @@ class MainMenuWindow(ba.Window):
|
||||||
self._tdelay = 2.0
|
self._tdelay = 2.0
|
||||||
self._t_delay_inc = 0.02
|
self._t_delay_inc = 0.02
|
||||||
self._t_delay_play = 1.7
|
self._t_delay_play = 1.7
|
||||||
self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.01
|
|
||||||
|
def _set_allow_time() -> None:
|
||||||
|
self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.5
|
||||||
|
|
||||||
|
# Slight hack: widget transitions currently only progress when
|
||||||
|
# frames are being drawn, but this tends to get called before
|
||||||
|
# frame drawing even starts, meaning we don't know exactly how
|
||||||
|
# long we should wait before refreshing to avoid interrupting
|
||||||
|
# the transition. To make things a bit better, let's do a
|
||||||
|
# redundant set of the time in a deferred call which hopefully
|
||||||
|
# happens closer to actual frame draw times.
|
||||||
|
_set_allow_time()
|
||||||
|
ba.pushcall(_set_allow_time)
|
||||||
|
|
||||||
ba.app.did_menu_intro = True
|
ba.app.did_menu_intro = True
|
||||||
self._width = 400.0
|
self._width = 400.0
|
||||||
self._height = 200.0
|
self._height = 200.0
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class PlaylistBrowserWindow(ba.Window):
|
||||||
)
|
)
|
||||||
|
|
||||||
uiscale = ba.app.ui.uiscale
|
uiscale = ba.app.ui.uiscale
|
||||||
self._width = 900 if uiscale is ba.UIScale.SMALL else 800
|
self._width = 900.0 if uiscale is ba.UIScale.SMALL else 800.0
|
||||||
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
x_inset = 50 if uiscale is ba.UIScale.SMALL else 0
|
||||||
self._height = (
|
self._height = (
|
||||||
480
|
480
|
||||||
|
|
@ -365,7 +365,7 @@ class PlaylistBrowserWindow(ba.Window):
|
||||||
|
|
||||||
self._sub_width = self._scroll_width
|
self._sub_width = self._scroll_width
|
||||||
self._sub_height = (
|
self._sub_height = (
|
||||||
40 + rows * (button_height + 2 * button_buffer_v) + 90
|
40.0 + rows * (button_height + 2 * button_buffer_v) + 90
|
||||||
)
|
)
|
||||||
assert self._sub_width is not None
|
assert self._sub_width is not None
|
||||||
assert self._sub_height is not None
|
assert self._sub_height is not None
|
||||||
|
|
|
||||||
88
dist/ba_data/python/bastd/ui/store/browser.py
vendored
88
dist/ba_data/python/bastd/ui/store/browser.py
vendored
|
|
@ -4,18 +4,25 @@
|
||||||
# pylint: disable=too-many-lines
|
# pylint: disable=too-many-lines
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
import copy
|
import copy
|
||||||
import math
|
import math
|
||||||
|
import logging
|
||||||
import weakref
|
import weakref
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from threading import Thread
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from efro.error import CommunicationError
|
||||||
|
import bacommon.cloud
|
||||||
import ba
|
import ba
|
||||||
import ba.internal
|
import ba.internal
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Any, Callable, Sequence
|
from typing import Any, Callable, Sequence
|
||||||
|
|
||||||
|
MERCH_LINK_KEY = 'Merch Link'
|
||||||
|
|
||||||
|
|
||||||
class StoreBrowserWindow(ba.Window):
|
class StoreBrowserWindow(ba.Window):
|
||||||
"""Window for browsing the store."""
|
"""Window for browsing the store."""
|
||||||
|
|
@ -593,8 +600,14 @@ class StoreBrowserWindow(ba.Window):
|
||||||
else:
|
else:
|
||||||
self._last_buy_time = curtime
|
self._last_buy_time = curtime
|
||||||
|
|
||||||
# Pro is an actual IAP; the rest are ticket purchases.
|
# Merch is a special case - just a link.
|
||||||
if item == 'pro':
|
if item == 'merch':
|
||||||
|
url = ba.app.config.get('Merch Link')
|
||||||
|
if isinstance(url, str):
|
||||||
|
ba.open_url(url)
|
||||||
|
|
||||||
|
# Pro is an actual IAP, and the rest are ticket purchases.
|
||||||
|
elif item == 'pro':
|
||||||
ba.playsound(ba.getsound('click01'))
|
ba.playsound(ba.getsound('click01'))
|
||||||
|
|
||||||
# Purchase either pro or pro_sale depending on whether
|
# Purchase either pro or pro_sale depending on whether
|
||||||
|
|
@ -681,7 +694,9 @@ class StoreBrowserWindow(ba.Window):
|
||||||
assert self.button_infos is not None
|
assert self.button_infos is not None
|
||||||
for b_type, b_info in self.button_infos.items():
|
for b_type, b_info in self.button_infos.items():
|
||||||
|
|
||||||
if b_type in ['upgrades.pro', 'pro']:
|
if b_type == 'merch':
|
||||||
|
purchased = False
|
||||||
|
elif b_type in ['upgrades.pro', 'pro']:
|
||||||
purchased = ba.app.accounts_v1.have_pro()
|
purchased = ba.app.accounts_v1.have_pro()
|
||||||
else:
|
else:
|
||||||
purchased = ba.internal.get_purchased(b_type)
|
purchased = ba.internal.get_purchased(b_type)
|
||||||
|
|
@ -707,7 +722,11 @@ class StoreBrowserWindow(ba.Window):
|
||||||
color = (0.4, 0.8, 0.1)
|
color = (0.4, 0.8, 0.1)
|
||||||
extra_image_opacity = 1.0
|
extra_image_opacity = 1.0
|
||||||
call = b_info['call'] if 'call' in b_info else None
|
call = b_info['call'] if 'call' in b_info else None
|
||||||
if b_type in ['upgrades.pro', 'pro']:
|
if b_type == 'merch':
|
||||||
|
price_text = ''
|
||||||
|
price_text_left = ''
|
||||||
|
price_text_right = ''
|
||||||
|
elif b_type in ['upgrades.pro', 'pro']:
|
||||||
sale_time = get_available_sale_time('extras')
|
sale_time = get_available_sale_time('extras')
|
||||||
if sale_time is not None:
|
if sale_time is not None:
|
||||||
priceraw = ba.internal.get_price('pro')
|
priceraw = ba.internal.get_price('pro')
|
||||||
|
|
@ -888,7 +907,14 @@ class StoreBrowserWindow(ba.Window):
|
||||||
dummy_name
|
dummy_name
|
||||||
)
|
)
|
||||||
section['v_spacing'] = (
|
section['v_spacing'] = (
|
||||||
-17 if self._tab == 'characters' else 0
|
-25
|
||||||
|
if (
|
||||||
|
self._tab == 'extras'
|
||||||
|
and uiscale is ba.UIScale.SMALL
|
||||||
|
)
|
||||||
|
else -17
|
||||||
|
if self._tab == 'characters'
|
||||||
|
else 0
|
||||||
)
|
)
|
||||||
if 'title' not in section:
|
if 'title' not in section:
|
||||||
section['title'] = ''
|
section['title'] = ''
|
||||||
|
|
@ -900,7 +926,13 @@ class StoreBrowserWindow(ba.Window):
|
||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
section['y_offs'] = (
|
section['y_offs'] = (
|
||||||
55
|
20
|
||||||
|
if (
|
||||||
|
self._tab == 'extras'
|
||||||
|
and uiscale is ba.UIScale.SMALL
|
||||||
|
and ba.app.config.get('Merch Link')
|
||||||
|
)
|
||||||
|
else 55
|
||||||
if (
|
if (
|
||||||
self._tab == 'extras'
|
self._tab == 'extras'
|
||||||
and uiscale is ba.UIScale.SMALL
|
and uiscale is ba.UIScale.SMALL
|
||||||
|
|
@ -917,7 +949,9 @@ class StoreBrowserWindow(ba.Window):
|
||||||
# pylint: disable=too-many-locals
|
# pylint: disable=too-many-locals
|
||||||
# pylint: disable=too-many-branches
|
# pylint: disable=too-many-branches
|
||||||
# pylint: disable=too-many-nested-blocks
|
# pylint: disable=too-many-nested-blocks
|
||||||
from bastd.ui.store import item as storeitemui
|
from bastd.ui.store.item import (
|
||||||
|
instantiate_store_item_display,
|
||||||
|
)
|
||||||
|
|
||||||
title_spacing = 40
|
title_spacing = 40
|
||||||
button_border = 20
|
button_border = 20
|
||||||
|
|
@ -1102,7 +1136,7 @@ class StoreBrowserWindow(ba.Window):
|
||||||
+ (b_width + button_spacing) * col,
|
+ (b_width + button_spacing) * col,
|
||||||
v - b_height + boffs_v2,
|
v - b_height + boffs_v2,
|
||||||
)
|
)
|
||||||
storeitemui.instantiate_store_item_display(
|
instantiate_store_item_display(
|
||||||
item_name,
|
item_name,
|
||||||
item,
|
item,
|
||||||
parent_widget=cnt2,
|
parent_widget=cnt2,
|
||||||
|
|
@ -1325,3 +1359,41 @@ class StoreBrowserWindow(ba.Window):
|
||||||
)
|
)
|
||||||
if self._on_close_call is not None:
|
if self._on_close_call is not None:
|
||||||
self._on_close_call()
|
self._on_close_call()
|
||||||
|
|
||||||
|
|
||||||
|
def _check_merch_availability_in_bg_thread() -> None:
|
||||||
|
# pylint: disable=cell-var-from-loop
|
||||||
|
|
||||||
|
# Merch is available from some countries only.
|
||||||
|
# Make a reasonable check to ask the master-server about this at
|
||||||
|
# launch and store the results.
|
||||||
|
for _i in range(15):
|
||||||
|
try:
|
||||||
|
if ba.app.cloud.is_connected():
|
||||||
|
response = ba.app.cloud.send_message(
|
||||||
|
bacommon.cloud.MerchAvailabilityMessage()
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_in_logic_thread() -> None:
|
||||||
|
cfg = ba.app.config
|
||||||
|
current: str | None = cfg.get(MERCH_LINK_KEY)
|
||||||
|
if not isinstance(current, str | None):
|
||||||
|
current = None
|
||||||
|
if current != response.url:
|
||||||
|
cfg[MERCH_LINK_KEY] = response.url
|
||||||
|
cfg.commit()
|
||||||
|
|
||||||
|
# If we successfully get a response, kick it over to the
|
||||||
|
# logic thread to store and we're done.
|
||||||
|
ba.pushcall(_store_in_logic_thread, from_other_thread=True)
|
||||||
|
return
|
||||||
|
except CommunicationError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logging.warning(
|
||||||
|
'Unexpected error in merch-availability-check.', exc_info=True
|
||||||
|
)
|
||||||
|
time.sleep(1.1934) # A bit randomized to avoid aliasing.
|
||||||
|
|
||||||
|
|
||||||
|
Thread(target=_check_merch_availability_in_bg_thread, daemon=True).start()
|
||||||
|
|
|
||||||
30
dist/ba_data/python/bastd/ui/store/item.py
vendored
30
dist/ba_data/python/bastd/ui/store/item.py
vendored
|
|
@ -45,19 +45,23 @@ def instantiate_store_item_display(
|
||||||
item['name'] = title = get_store_item_name_translated(item_name)
|
item['name'] = title = get_store_item_name_translated(item_name)
|
||||||
|
|
||||||
btn: ba.Widget | None
|
btn: ba.Widget | None
|
||||||
|
|
||||||
|
# Hack; showbuffer stuff isn't working well when we're showing merch.
|
||||||
|
showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0
|
||||||
|
|
||||||
if button:
|
if button:
|
||||||
item['button'] = btn = ba.buttonwidget(
|
item['button'] = btn = ba.buttonwidget(
|
||||||
parent=parent_widget,
|
parent=parent_widget,
|
||||||
position=b_pos,
|
position=b_pos,
|
||||||
transition_delay=delay,
|
transition_delay=delay,
|
||||||
show_buffer_top=76.0,
|
show_buffer_top=showbuffer,
|
||||||
enable_sound=False,
|
enable_sound=False,
|
||||||
button_type='square',
|
button_type='square',
|
||||||
size=(b_width, b_height),
|
size=(b_width, b_height),
|
||||||
autoselect=True,
|
autoselect=True,
|
||||||
label='',
|
label='',
|
||||||
)
|
)
|
||||||
ba.widget(edit=btn, show_buffer_bottom=76.0)
|
ba.widget(edit=btn, show_buffer_bottom=showbuffer)
|
||||||
else:
|
else:
|
||||||
btn = None
|
btn = None
|
||||||
|
|
||||||
|
|
@ -92,6 +96,10 @@ def instantiate_store_item_display(
|
||||||
tint_tex = character.icon_mask_texture
|
tint_tex = character.icon_mask_texture
|
||||||
title_v = 0.255
|
title_v = 0.255
|
||||||
price_v = 0.145
|
price_v = 0.145
|
||||||
|
elif item_name == 'merch':
|
||||||
|
base_text_scale = 0.6
|
||||||
|
title_v = 0.85
|
||||||
|
price_v = 0.15
|
||||||
elif item_name in ['upgrades.pro', 'pro']:
|
elif item_name in ['upgrades.pro', 'pro']:
|
||||||
base_text_scale = 0.6
|
base_text_scale = 0.6
|
||||||
title_v = 0.85
|
title_v = 0.85
|
||||||
|
|
@ -165,6 +173,23 @@ def instantiate_store_item_display(
|
||||||
tint2_color=tint2_color,
|
tint2_color=tint2_color,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if item_name == 'merch':
|
||||||
|
frame_size = b_width * 0.65
|
||||||
|
im_dim = frame_size * (100.0 / 113.0)
|
||||||
|
im_pos = (
|
||||||
|
b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
|
||||||
|
b_pos[1] + b_height * 0.47 - im_dim * 0.5,
|
||||||
|
)
|
||||||
|
ba.imagewidget(
|
||||||
|
parent=parent_widget,
|
||||||
|
position=im_pos,
|
||||||
|
size=(im_dim, im_dim),
|
||||||
|
transition_delay=delay,
|
||||||
|
draw_controller=btn,
|
||||||
|
opacity=1.0,
|
||||||
|
texture=ba.gettexture('merch'),
|
||||||
|
)
|
||||||
|
|
||||||
if item_name in ['pro', 'upgrades.pro']:
|
if item_name in ['pro', 'upgrades.pro']:
|
||||||
frame_size = b_width * 0.5
|
frame_size = b_width * 0.5
|
||||||
im_dim = frame_size * (100.0 / 113.0)
|
im_dim = frame_size * (100.0 / 113.0)
|
||||||
|
|
@ -184,7 +209,6 @@ def instantiate_store_item_display(
|
||||||
)
|
)
|
||||||
txt = ba.Lstr(resource='store.bombSquadProNewDescriptionText')
|
txt = ba.Lstr(resource='store.bombSquadProNewDescriptionText')
|
||||||
|
|
||||||
# t = 'foo\nfoo\nfoo\nfoo\nfoo\nfoo'
|
|
||||||
item['descriptionText'] = ba.textwidget(
|
item['descriptionText'] = ba.textwidget(
|
||||||
parent=parent_widget,
|
parent=parent_widget,
|
||||||
text=txt,
|
text=txt,
|
||||||
|
|
|
||||||
117
dist/ba_data/python/bastd/ui/v2upgrade.py
vendored
Normal file
117
dist/ba_data/python/bastd/ui/v2upgrade.py
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
# Released under the MIT License. See LICENSE for details.
|
||||||
|
#
|
||||||
|
"""UI for upgrading V1 accounts to V2."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ba
|
||||||
|
import ba.internal
|
||||||
|
|
||||||
|
|
||||||
|
class V2UpgradeWindow(ba.Window):
|
||||||
|
"""A window presenting a URL to the user visually."""
|
||||||
|
|
||||||
|
def __init__(self, login_name: str, code: str):
|
||||||
|
from bastd.ui.account.settings import show_what_is_v2_page
|
||||||
|
|
||||||
|
app = ba.app
|
||||||
|
uiscale = app.ui.uiscale
|
||||||
|
|
||||||
|
self._code = code
|
||||||
|
|
||||||
|
self._width = 700
|
||||||
|
self._height = 270
|
||||||
|
super().__init__(
|
||||||
|
root_widget=ba.containerwidget(
|
||||||
|
size=(self._width, self._height + 40),
|
||||||
|
transition='in_right',
|
||||||
|
scale=(
|
||||||
|
1.25
|
||||||
|
if uiscale is ba.UIScale.SMALL
|
||||||
|
else 1.25
|
||||||
|
if uiscale is ba.UIScale.MEDIUM
|
||||||
|
else 1.25
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ba.playsound(ba.getsound('error'))
|
||||||
|
|
||||||
|
ba.textwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5, self._height - 46),
|
||||||
|
size=(0, 0),
|
||||||
|
color=ba.app.ui.title_color,
|
||||||
|
h_align='center',
|
||||||
|
v_align='center',
|
||||||
|
text=ba.Lstr(
|
||||||
|
resource='deviceAccountUpgradeText',
|
||||||
|
subs=[('${NAME}', login_name)],
|
||||||
|
),
|
||||||
|
maxwidth=self._width * 0.95,
|
||||||
|
)
|
||||||
|
ba.textwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5, 125),
|
||||||
|
size=(0, 0),
|
||||||
|
scale=0.8,
|
||||||
|
color=(0.7, 0.8, 0.7),
|
||||||
|
h_align='center',
|
||||||
|
v_align='center',
|
||||||
|
text=(
|
||||||
|
ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)
|
||||||
|
+ login_name
|
||||||
|
+ ' ----> '
|
||||||
|
+ ba.charstr(ba.SpecialChar.V2_LOGO)
|
||||||
|
+ login_name
|
||||||
|
),
|
||||||
|
maxwidth=self._width * 0.95,
|
||||||
|
)
|
||||||
|
button_width = 200
|
||||||
|
|
||||||
|
cancel_button = ba.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(20, 25),
|
||||||
|
size=(button_width, 65),
|
||||||
|
autoselect=True,
|
||||||
|
label=ba.Lstr(resource='notNowText'),
|
||||||
|
on_activate_call=self._done,
|
||||||
|
)
|
||||||
|
|
||||||
|
_what_is_this_button = ba.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width * 0.5 - button_width * 0.5, 25),
|
||||||
|
size=(button_width, 65),
|
||||||
|
autoselect=True,
|
||||||
|
label=ba.Lstr(resource='whatIsThisText'),
|
||||||
|
color=(0.55, 0.5, 0.6),
|
||||||
|
textcolor=(0.75, 0.7, 0.8),
|
||||||
|
on_activate_call=show_what_is_v2_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
upgrade_button = ba.buttonwidget(
|
||||||
|
parent=self._root_widget,
|
||||||
|
position=(self._width - button_width - 20, 25),
|
||||||
|
size=(button_width, 65),
|
||||||
|
autoselect=True,
|
||||||
|
label=ba.Lstr(resource='upgradeText'),
|
||||||
|
on_activate_call=self._upgrade_press,
|
||||||
|
)
|
||||||
|
|
||||||
|
ba.containerwidget(
|
||||||
|
edit=self._root_widget,
|
||||||
|
selected_child=upgrade_button,
|
||||||
|
cancel_button=cancel_button,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _upgrade_press(self) -> None:
|
||||||
|
# Get rid of the window and sign out before kicking the
|
||||||
|
# user over to a browser to do the upgrade. This hopefully
|
||||||
|
# makes it more clear when they come back that they need to
|
||||||
|
# sign in with the 'BombSquad account' option.
|
||||||
|
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||||
|
ba.internal.sign_out_v1()
|
||||||
|
bamasteraddr = ba.internal.get_master_server_address(version=2)
|
||||||
|
ba.open_url(f'{bamasteraddr}/v2uda/{self._code}')
|
||||||
|
|
||||||
|
def _done(self) -> None:
|
||||||
|
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||||
38
dist/ba_data/python/efro/call.py
vendored
38
dist/ba_data/python/efro/call.py
vendored
|
|
@ -274,6 +274,44 @@ if TYPE_CHECKING:
|
||||||
) -> _CallNoArgs[OutT]:
|
) -> _CallNoArgs[OutT]:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# 4 arg call; 3 args bundled.
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@overload
|
||||||
|
def Call(
|
||||||
|
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||||
|
arg1: In1T,
|
||||||
|
arg2: In2T,
|
||||||
|
arg3: In3T,
|
||||||
|
) -> _Call1Arg[In4T, OutT]:
|
||||||
|
...
|
||||||
|
|
||||||
|
# 4 arg call; 2 args bundled.
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@overload
|
||||||
|
def Call(
|
||||||
|
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||||
|
arg1: In1T,
|
||||||
|
arg2: In2T,
|
||||||
|
) -> _Call2Args[In3T, In4T, OutT]:
|
||||||
|
...
|
||||||
|
|
||||||
|
# 4 arg call; 1 arg bundled.
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@overload
|
||||||
|
def Call(
|
||||||
|
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||||
|
arg1: In1T,
|
||||||
|
) -> _Call3Args[In2T, In3T, In4T, OutT]:
|
||||||
|
...
|
||||||
|
|
||||||
|
# 4 arg call; no args bundled.
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@overload
|
||||||
|
def Call(
|
||||||
|
call: Callable[[In1T, In2T, In3T, In4T], OutT],
|
||||||
|
) -> _Call4Args[In1T, In2T, In3T, In4T, OutT]:
|
||||||
|
...
|
||||||
|
|
||||||
# 5 arg call; 5 args bundled.
|
# 5 arg call; 5 args bundled.
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
@overload
|
@overload
|
||||||
|
|
|
||||||
13
dist/ba_data/python/efro/dataclassio/_base.py
vendored
13
dist/ba_data/python/efro/dataclassio/_base.py
vendored
|
|
@ -111,6 +111,8 @@ class IOAttrs:
|
||||||
boundaries (see efro.util.utc_today()).
|
boundaries (see efro.util.utc_today()).
|
||||||
'whole_hours', if True, requires datetime values to lie exactly on hour
|
'whole_hours', if True, requires datetime values to lie exactly on hour
|
||||||
boundaries (see efro.util.utc_this_hour()).
|
boundaries (see efro.util.utc_this_hour()).
|
||||||
|
'whole_minutes', if True, requires datetime values to lie exactly on minute
|
||||||
|
boundaries (see efro.util.utc_this_minute()).
|
||||||
'soft_default', if passed, injects a default value into dataclass
|
'soft_default', if passed, injects a default value into dataclass
|
||||||
instantiation when the field is not present in the input data.
|
instantiation when the field is not present in the input data.
|
||||||
This allows dataclasses to add new non-optional fields while
|
This allows dataclasses to add new non-optional fields while
|
||||||
|
|
@ -136,6 +138,7 @@ class IOAttrs:
|
||||||
store_default: bool = True
|
store_default: bool = True
|
||||||
whole_days: bool = False
|
whole_days: bool = False
|
||||||
whole_hours: bool = False
|
whole_hours: bool = False
|
||||||
|
whole_minutes: bool = False
|
||||||
soft_default: Any = MISSING
|
soft_default: Any = MISSING
|
||||||
soft_default_factory: Callable[[], Any] | _MissingType = MISSING
|
soft_default_factory: Callable[[], Any] | _MissingType = MISSING
|
||||||
|
|
||||||
|
|
@ -145,6 +148,7 @@ class IOAttrs:
|
||||||
store_default: bool = store_default,
|
store_default: bool = store_default,
|
||||||
whole_days: bool = whole_days,
|
whole_days: bool = whole_days,
|
||||||
whole_hours: bool = whole_hours,
|
whole_hours: bool = whole_hours,
|
||||||
|
whole_minutes: bool = whole_minutes,
|
||||||
soft_default: Any = MISSING,
|
soft_default: Any = MISSING,
|
||||||
soft_default_factory: Callable[[], Any] | _MissingType = MISSING,
|
soft_default_factory: Callable[[], Any] | _MissingType = MISSING,
|
||||||
):
|
):
|
||||||
|
|
@ -160,6 +164,8 @@ class IOAttrs:
|
||||||
self.whole_days = whole_days
|
self.whole_days = whole_days
|
||||||
if whole_hours != cls.whole_hours:
|
if whole_hours != cls.whole_hours:
|
||||||
self.whole_hours = whole_hours
|
self.whole_hours = whole_hours
|
||||||
|
if whole_minutes != cls.whole_minutes:
|
||||||
|
self.whole_minutes = whole_minutes
|
||||||
if soft_default is not cls.soft_default:
|
if soft_default is not cls.soft_default:
|
||||||
|
|
||||||
# Do what dataclasses does with its default types and
|
# Do what dataclasses does with its default types and
|
||||||
|
|
@ -216,13 +222,18 @@ class IOAttrs:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Value {value} at {fieldpath} is not a whole day.'
|
f'Value {value} at {fieldpath} is not a whole day.'
|
||||||
)
|
)
|
||||||
if self.whole_hours:
|
elif self.whole_hours:
|
||||||
if any(
|
if any(
|
||||||
x != 0 for x in (value.minute, value.second, value.microsecond)
|
x != 0 for x in (value.minute, value.second, value.microsecond)
|
||||||
):
|
):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Value {value} at {fieldpath}' f' is not a whole hour.'
|
f'Value {value} at {fieldpath}' f' is not a whole hour.'
|
||||||
)
|
)
|
||||||
|
elif self.whole_minutes:
|
||||||
|
if any(x != 0 for x in (value.second, value.microsecond)):
|
||||||
|
raise ValueError(
|
||||||
|
f'Value {value} at {fieldpath}' f' is not a whole minute.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_origin(anntype: Any) -> Any:
|
def _get_origin(anntype: Any) -> Any:
|
||||||
|
|
|
||||||
14
dist/ba_data/python/efro/error.py
vendored
14
dist/ba_data/python/efro/error.py
vendored
|
|
@ -80,6 +80,15 @@ class IntegrityError(ValueError):
|
||||||
"""Data has been tampered with or corrupted in some form."""
|
"""Data has been tampered with or corrupted in some form."""
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationError(Exception):
|
||||||
|
"""Authentication has failed for some operation.
|
||||||
|
|
||||||
|
This can be raised if server-side-verification does not match
|
||||||
|
client-supplied credentials, if an invalid password is supplied
|
||||||
|
for a sign-in attempt, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
|
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
|
||||||
"""Is the provided exception from urllib a communication-related error?
|
"""Is the provided exception from urllib a communication-related error?
|
||||||
|
|
||||||
|
|
@ -188,6 +197,7 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
|
||||||
firewall/connectivity issues, etc. These issues can often be safely
|
firewall/connectivity issues, etc. These issues can often be safely
|
||||||
ignored or presented to the user as general 'connection-lost' events.
|
ignored or presented to the user as general 'connection-lost' events.
|
||||||
"""
|
"""
|
||||||
|
# pylint: disable=too-many-return-statements
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
if isinstance(
|
if isinstance(
|
||||||
|
|
@ -227,4 +237,8 @@ def is_asyncio_streams_communication_error(exc: BaseException) -> bool:
|
||||||
if 'SSL: WRONG_VERSION_NUMBER' in excstr:
|
if 'SSL: WRONG_VERSION_NUMBER' in excstr:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# And seeing this very rarely; assuming its just data corruption?
|
||||||
|
if 'SSL: DECRYPTION_FAILED_OR_BAD_RECORD_MAC' in excstr:
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
24
dist/ba_data/python/efro/log.py
vendored
24
dist/ba_data/python/efro/log.py
vendored
|
|
@ -37,7 +37,27 @@ class LogLevel(Enum):
|
||||||
ERROR = 3
|
ERROR = 3
|
||||||
CRITICAL = 4
|
CRITICAL = 4
|
||||||
|
|
||||||
|
@property
|
||||||
|
def python_logging_level(self) -> int:
|
||||||
|
"""Give the corresponding logging level."""
|
||||||
|
return LOG_LEVEL_LEVELNOS[self]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_python_logging_level(cls, levelno: int) -> LogLevel:
|
||||||
|
"""Given a Python logging level, return a LogLevel."""
|
||||||
|
return LEVELNO_LOG_LEVELS[levelno]
|
||||||
|
|
||||||
|
|
||||||
|
# Python logging levels from LogLevels
|
||||||
|
LOG_LEVEL_LEVELNOS = {
|
||||||
|
LogLevel.DEBUG: logging.DEBUG,
|
||||||
|
LogLevel.INFO: logging.INFO,
|
||||||
|
LogLevel.WARNING: logging.WARNING,
|
||||||
|
LogLevel.ERROR: logging.ERROR,
|
||||||
|
LogLevel.CRITICAL: logging.CRITICAL,
|
||||||
|
}
|
||||||
|
|
||||||
|
# LogLevels from Python logging levels
|
||||||
LEVELNO_LOG_LEVELS = {
|
LEVELNO_LOG_LEVELS = {
|
||||||
logging.DEBUG: LogLevel.DEBUG,
|
logging.DEBUG: LogLevel.DEBUG,
|
||||||
logging.INFO: LogLevel.INFO,
|
logging.INFO: LogLevel.INFO,
|
||||||
|
|
@ -128,7 +148,7 @@ class LogHandler(logging.Handler):
|
||||||
self._cache_lock = Lock()
|
self._cache_lock = Lock()
|
||||||
self._printed_callback_error = False
|
self._printed_callback_error = False
|
||||||
self._thread_bootstrapped = False
|
self._thread_bootstrapped = False
|
||||||
self._thread = Thread(target=self._thread_main, daemon=True)
|
self._thread = Thread(target=self._log_thread_main, daemon=True)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
# Spin until our thread is up and running; otherwise we could
|
# Spin until our thread is up and running; otherwise we could
|
||||||
|
|
@ -145,7 +165,7 @@ class LogHandler(logging.Handler):
|
||||||
with self._callbacks_lock:
|
with self._callbacks_lock:
|
||||||
self._callbacks.append(call)
|
self._callbacks.append(call)
|
||||||
|
|
||||||
def _thread_main(self) -> None:
|
def _log_thread_main(self) -> None:
|
||||||
self._event_loop = asyncio.new_event_loop()
|
self._event_loop = asyncio.new_event_loop()
|
||||||
# NOTE: if we ever use default threadpool at all we should allow
|
# NOTE: if we ever use default threadpool at all we should allow
|
||||||
# setting it for our loop.
|
# setting it for our loop.
|
||||||
|
|
|
||||||
14
dist/ba_data/python/efro/message/_message.py
vendored
14
dist/ba_data/python/efro/message/_message.py
vendored
|
|
@ -43,6 +43,20 @@ class SysResponse:
|
||||||
users of the api never see them.
|
users of the api never see them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def set_local_exception(self, exc: Exception) -> None:
|
||||||
|
"""Attach a local exception to facilitate better logging/handling.
|
||||||
|
|
||||||
|
Be aware that this data does not get serialized and only
|
||||||
|
exists on the local object.
|
||||||
|
"""
|
||||||
|
setattr(self, '_sr_local_exception', exc)
|
||||||
|
|
||||||
|
def get_local_exception(self) -> Exception | None:
|
||||||
|
"""Fetch a local attached exception."""
|
||||||
|
value = getattr(self, '_sr_local_exception', None)
|
||||||
|
assert isinstance(value, Exception | None)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Some standard response types:
|
# Some standard response types:
|
||||||
|
|
||||||
|
|
|
||||||
59
dist/ba_data/python/efro/message/_sender.py
vendored
59
dist/ba_data/python/efro/message/_sender.py
vendored
|
|
@ -6,7 +6,6 @@ Supports static typing for message types and possible return types.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from efro.error import CleanError, RemoteError, CommunicationError
|
from efro.error import CleanError, RemoteError, CommunicationError
|
||||||
|
|
@ -158,17 +157,18 @@ class MessageSender:
|
||||||
bound_obj, msg_encoded
|
bound_obj, msg_encoded
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Any error in the raw send call gets recorded as either
|
response = ErrorSysResponse(
|
||||||
# a local or communication error.
|
error_message='Error in MessageSender @send_method.',
|
||||||
return ErrorSysResponse(
|
|
||||||
error_message=f'Error in MessageSender @send_method'
|
|
||||||
f' ({type(exc)}): {exc}',
|
|
||||||
error_type=(
|
error_type=(
|
||||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||||
if isinstance(exc, CommunicationError)
|
if isinstance(exc, CommunicationError)
|
||||||
else ErrorSysResponse.ErrorType.LOCAL
|
else ErrorSysResponse.ErrorType.LOCAL
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
# Can include the actual exception since we'll be looking at
|
||||||
|
# this locally; might be helpful.
|
||||||
|
response.set_local_exception(exc)
|
||||||
|
return response
|
||||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||||
|
|
||||||
async def fetch_raw_response_async(
|
async def fetch_raw_response_async(
|
||||||
|
|
@ -193,17 +193,18 @@ class MessageSender:
|
||||||
bound_obj, msg_encoded
|
bound_obj, msg_encoded
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Any error in the raw send call gets recorded as either
|
response = ErrorSysResponse(
|
||||||
# a local or communication error.
|
error_message='Error in MessageSender @send_async_method.',
|
||||||
return ErrorSysResponse(
|
|
||||||
error_message=f'Error in MessageSender @send_async_method'
|
|
||||||
f' ({type(exc)}): {exc}',
|
|
||||||
error_type=(
|
error_type=(
|
||||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||||
if isinstance(exc, CommunicationError)
|
if isinstance(exc, CommunicationError)
|
||||||
else ErrorSysResponse.ErrorType.LOCAL
|
else ErrorSysResponse.ErrorType.LOCAL
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
# Can include the actual exception since we'll be looking at
|
||||||
|
# this locally; might be helpful.
|
||||||
|
response.set_local_exception(exc)
|
||||||
|
return response
|
||||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||||
|
|
||||||
def unpack_raw_response(
|
def unpack_raw_response(
|
||||||
|
|
@ -250,18 +251,14 @@ class MessageSender:
|
||||||
self._decode_filter_call(
|
self._decode_filter_call(
|
||||||
bound_obj, message, response_dict, response
|
bound_obj, message, response_dict, response
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
# If we got to this point, we successfully communicated
|
|
||||||
# with the other end so errors represent protocol mismatches
|
|
||||||
# or other invalid data. For now let's just log it but perhaps
|
|
||||||
# we'd want to somehow embed it in the ErrorSysResponse to be
|
|
||||||
# available directly to the user later.
|
|
||||||
logging.exception('Error decoding raw response')
|
|
||||||
response = ErrorSysResponse(
|
response = ErrorSysResponse(
|
||||||
error_message='Error decoding raw response;'
|
error_message='Error decoding raw response.',
|
||||||
' see log for details.',
|
|
||||||
error_type=ErrorSysResponse.ErrorType.LOCAL,
|
error_type=ErrorSysResponse.ErrorType.LOCAL,
|
||||||
)
|
)
|
||||||
|
# Since we'll be looking at this locally, we can include
|
||||||
|
# extra info for logging/etc.
|
||||||
|
response.set_local_exception(exc)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _unpack_raw_response(
|
def _unpack_raw_response(
|
||||||
|
|
@ -282,16 +279,24 @@ class MessageSender:
|
||||||
# Some error occurred. Raise a local Exception for it.
|
# Some error occurred. Raise a local Exception for it.
|
||||||
if isinstance(raw_response, ErrorSysResponse):
|
if isinstance(raw_response, ErrorSysResponse):
|
||||||
|
|
||||||
|
# Errors that happened locally can attach their exceptions
|
||||||
|
# here for extra logging goodness.
|
||||||
|
local_exception = raw_response.get_local_exception()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
raw_response.error_type
|
raw_response.error_type
|
||||||
is ErrorSysResponse.ErrorType.COMMUNICATION
|
is ErrorSysResponse.ErrorType.COMMUNICATION
|
||||||
):
|
):
|
||||||
raise CommunicationError(raw_response.error_message)
|
raise CommunicationError(
|
||||||
|
raw_response.error_message
|
||||||
|
) from local_exception
|
||||||
|
|
||||||
# If something went wrong on *our* end of the connection,
|
# If something went wrong on *our* end of the connection,
|
||||||
# don't say it was a remote error.
|
# don't say it was a remote error.
|
||||||
if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL:
|
if raw_response.error_type is ErrorSysResponse.ErrorType.LOCAL:
|
||||||
raise RuntimeError(raw_response.error_message)
|
raise RuntimeError(
|
||||||
|
raw_response.error_message
|
||||||
|
) from local_exception
|
||||||
|
|
||||||
# If they want to support clean errors, do those.
|
# If they want to support clean errors, do those.
|
||||||
if (
|
if (
|
||||||
|
|
@ -299,14 +304,18 @@ class MessageSender:
|
||||||
and raw_response.error_type
|
and raw_response.error_type
|
||||||
is ErrorSysResponse.ErrorType.REMOTE_CLEAN
|
is ErrorSysResponse.ErrorType.REMOTE_CLEAN
|
||||||
):
|
):
|
||||||
raise CleanError(raw_response.error_message)
|
raise CleanError(
|
||||||
|
raw_response.error_message
|
||||||
|
) from local_exception
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.protocol.forward_communication_errors
|
self.protocol.forward_communication_errors
|
||||||
and raw_response.error_type
|
and raw_response.error_type
|
||||||
is ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION
|
is ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION
|
||||||
):
|
):
|
||||||
raise CommunicationError(raw_response.error_message)
|
raise CommunicationError(
|
||||||
|
raw_response.error_message
|
||||||
|
) from local_exception
|
||||||
|
|
||||||
# Everything else gets lumped in as a remote error.
|
# Everything else gets lumped in as a remote error.
|
||||||
raise RemoteError(
|
raise RemoteError(
|
||||||
|
|
@ -316,7 +325,7 @@ class MessageSender:
|
||||||
if self._peer_desc_call is None
|
if self._peer_desc_call is None
|
||||||
else self._peer_desc_call(bound_obj)
|
else self._peer_desc_call(bound_obj)
|
||||||
),
|
),
|
||||||
)
|
) from local_exception
|
||||||
|
|
||||||
assert isinstance(raw_response, Response)
|
assert isinstance(raw_response, Response)
|
||||||
return raw_response
|
return raw_response
|
||||||
|
|
|
||||||
3
dist/ba_data/python/efro/rpc.py
vendored
3
dist/ba_data/python/efro/rpc.py
vendored
|
|
@ -628,7 +628,8 @@ class RPCEndpoint:
|
||||||
|
|
||||||
# Now just sit and handle stuff as it comes in.
|
# Now just sit and handle stuff as it comes in.
|
||||||
while True:
|
while True:
|
||||||
assert not self._closing
|
if self._closing:
|
||||||
|
return
|
||||||
|
|
||||||
# Read message type.
|
# Read message type.
|
||||||
mtype = _PacketType(await self._read_int_8())
|
mtype = _PacketType(await self._read_int_8())
|
||||||
|
|
|
||||||
4
dist/ba_data/python/efro/util.py
vendored
4
dist/ba_data/python/efro/util.py
vendored
|
|
@ -39,6 +39,7 @@ class _EmptyObj:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: kill this and just use efro.call.tpartial
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
Call = Call
|
Call = Call
|
||||||
else:
|
else:
|
||||||
|
|
@ -273,13 +274,14 @@ class DispatchMethodWrapper(Generic[ArgT, RetT]):
|
||||||
"""Type-aware standin for the dispatch func returned by dispatchmethod."""
|
"""Type-aware standin for the dispatch func returned by dispatchmethod."""
|
||||||
|
|
||||||
def __call__(self, arg: ArgT) -> RetT:
|
def __call__(self, arg: ArgT) -> RetT:
|
||||||
pass
|
raise RuntimeError('Should not get here')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register(
|
def register(
|
||||||
func: Callable[[Any, Any], RetT]
|
func: Callable[[Any, Any], RetT]
|
||||||
) -> Callable[[Any, Any], RetT]:
|
) -> Callable[[Any, Any], RetT]:
|
||||||
"""Register a new dispatch handler for this dispatch-method."""
|
"""Register a new dispatch handler for this dispatch-method."""
|
||||||
|
raise RuntimeError('Should not get here')
|
||||||
|
|
||||||
registry: dict[Any, Callable]
|
registry: dict[Any, Callable]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue