# Released under the MIT License. See LICENSE for details. # """Account related functionality.""" from __future__ import annotations import copy import time from typing import TYPE_CHECKING import _ba if TYPE_CHECKING: from typing import Any, Optional import ba class AccountSubsystem: """Subsystem for account handling in the app. Category: App Classes Access the single shared instance of this class at 'ba.app.plugins'. """ def __init__(self) -> None: self.account_tournament_list: Optional[tuple[int, list[str]]] = None # FIXME: should abstract/structure these. self.tournament_info: dict = {} self.league_rank_cache: dict = {} self.last_post_purchase_message_time: Optional[float] = None # If we try to run promo-codes due to launch-args/etc we might # not be signed in yet; go ahead and queue them up in that case. self.pending_promo_codes: list[str] = [] def on_app_launch(self) -> None: """Called when the app is done bootstrapping.""" # Auto-sign-in to a local account in a moment if we're set to. def do_auto_sign_in() -> None: if _ba.app.headless_mode or _ba.app.config.get( 'Auto Account State') == 'Local': _ba.sign_in('Local') _ba.pushcall(do_auto_sign_in) def on_app_resume(self) -> None: """Should be called when the app is resumed.""" # Mark our cached tourneys as invalid so anyone using them knows # they might be out of date. for entry in list(self.tournament_info.values()): entry['valid'] = False def handle_account_gained_tickets(self, count: int) -> None: """Called when the current account has been awarded tickets. (internal) """ from ba._language import Lstr _ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText', subs=[('${COUNT}', str(count))]), color=(0, 1, 0)) _ba.playsound(_ba.getsound('cashRegister')) def cache_league_rank_data(self, data: Any) -> None: """(internal)""" self.league_rank_cache['info'] = copy.deepcopy(data) def get_cached_league_rank_data(self) -> Any: """(internal)""" return self.league_rank_cache.get('info', None) def get_league_rank_points(self, data: Optional[dict[str, Any]], subset: str = None) -> int: """(internal)""" if data is None: return 0 # If the data contains an achievement total, use that. otherwise calc # locally. if data['at'] is not None: total_ach_value = data['at'] else: total_ach_value = 0 for ach in _ba.app.ach.achievements: if ach.complete: total_ach_value += ach.power_ranking_value trophies_total: int = (data['t0a'] * data['t0am'] + data['t0b'] * data['t0bm'] + data['t1'] * data['t1m'] + data['t2'] * data['t2m'] + data['t3'] * data['t3m'] + data['t4'] * data['t4m']) if subset == 'trophyCount': val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] + data['t3'] + data['t4']) assert isinstance(val, int) return val if subset == 'trophies': assert isinstance(trophies_total, int) return trophies_total if subset is not None: raise ValueError('invalid subset value: ' + str(subset)) if data['p']: pro_mult = 1.0 + float( _ba.get_account_misc_read_val('proPowerRankingBoost', 0.0)) * 0.01 else: pro_mult = 1.0 # For final value, apply our pro mult and activeness-mult. return int( (total_ach_value + trophies_total) * (data['act'] if data['act'] is not None else 1.0) * pro_mult) def cache_tournament_info(self, info: Any) -> None: """(internal)""" from ba._generated.enums import TimeType, TimeFormat for entry in info: cache_entry = self.tournament_info[entry['tournamentID']] = ( copy.deepcopy(entry)) # Also store the time we received this, so we can adjust # time-remaining values/etc. cache_entry['timeReceived'] = _ba.time(TimeType.REAL, TimeFormat.MILLISECONDS) cache_entry['valid'] = True def get_purchased_icons(self) -> list[str]: """(internal)""" # pylint: disable=cyclic-import from ba import _store if _ba.get_account_state() != 'signed_in': return [] icons = [] store_items = _store.get_store_items() for item_name, item in list(store_items.items()): if item_name.startswith('icons.') and _ba.get_purchased(item_name): icons.append(item['icon']) return icons def ensure_have_account_player_profile(self) -> None: """ Ensure the standard account-named player profile exists; creating if needed. (internal) """ # This only applies when we're signed in. if _ba.get_account_state() != 'signed_in': return # If the short version of our account name currently cant be # displayed by the game, cancel. if not _ba.have_chars(_ba.get_account_display_string(full=False)): return config = _ba.app.config if ('Player Profiles' not in config or '__account__' not in config['Player Profiles']): # Create a spaz with a nice default purply color. _ba.add_transaction({ 'type': 'ADD_PLAYER_PROFILE', 'name': '__account__', 'profile': { 'character': 'Spaz', 'color': [0.5, 0.25, 1.0], 'highlight': [0.5, 0.25, 1.0] } }) _ba.run_transactions() def have_pro(self) -> bool: """Return whether pro is currently unlocked.""" # Check our tickets-based pro upgrade and our two real-IAP based # upgrades. Also always unlock this stuff in ballistica-core builds. return bool( _ba.get_purchased('upgrades.pro') or _ba.get_purchased('static.pro') or _ba.get_purchased('static.pro_sale') or 'ballistica' + 'core' == _ba.appname()) def have_pro_options(self) -> bool: """Return whether pro-options are present. This is True for owners of Pro or for old installs before Pro was a requirement for these options. """ # We expose pro options if the server tells us to # (which is generally just when we own pro), # or also if we've been grandfathered in or are using ballistica-core # builds. return self.have_pro() or bool( _ba.get_account_misc_read_val_2('proOptionsUnlocked', False) or _ba.app.config.get('lc14292', 0) > 1) def show_post_purchase_message(self) -> None: """(internal)""" from ba._language import Lstr from ba._generated.enums import TimeType cur_time = _ba.time(TimeType.REAL) if (self.last_post_purchase_message_time is None or cur_time - self.last_post_purchase_message_time > 3.0): self.last_post_purchase_message_time = cur_time with _ba.Context('ui'): _ba.screenmessage(Lstr(resource='updatingAccountText', fallback_resource='purchasingText'), color=(0, 1, 0)) _ba.playsound(_ba.getsound('click01')) def on_account_state_changed(self) -> None: """(internal)""" from ba._language import Lstr # Run any pending promo codes we had queued up while not signed in. if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes: for code in self.pending_promo_codes: _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) _ba.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) _ba.run_transactions() self.pending_promo_codes = [] def add_pending_promo_code(self, code: str) -> None: """(internal)""" from ba._language import Lstr from ba._generated.enums import TimeType # If we're not signed in, queue up the code to run the next time we # are and issue a warning if we haven't signed in within the next # few seconds. if _ba.get_account_state() != 'signed_in': def check_pending_codes() -> None: """(internal)""" # If we're still not signed in and have pending codes, # inform the user that they need to sign in to use them. if self.pending_promo_codes: _ba.screenmessage(Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)) _ba.playsound(_ba.getsound('error')) self.pending_promo_codes.append(code) _ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL) return _ba.screenmessage(Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)) _ba.add_transaction({ 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code }) _ba.run_transactions()