def expire(self) -> None: """Called when the Team is expiring (due to the Activity expiring). (internal) """ assert self._postinited assert not self._expired self._expired = True try: self.on_expire() except Exception: print_exception(f'Error in on_expire for {self}.') del self._gamedata del self.players
def get_unowned_game_types(self) -> Set[Type[ba.GameActivity]]: """Return present game types not owned by the current account.""" try: from ba import _store unowned_games: Set[Type[ba.GameActivity]] = set() if not _ba.app.headless_mode: for section in _store.get_store_layout()['minigames']: for mname in section['items']: if not _ba.get_purchased(mname): m_info = _store.get_store_item(mname) unowned_games.add(m_info['gametype']) return unowned_games except Exception: from ba import _error _error.print_exception('error calcing un-owned games') return set()
def _play_current_playlist(self) -> None: try: from ba._general import Call assert self._current_playlist is not None if _ba.mac_music_app_play_playlist(self._current_playlist): pass else: _ba.pushcall(Call( _ba.screenmessage, _ba.app.lang.get_resource('playlistNotFoundText') + ': \'' + self._current_playlist + '\'', (1, 0, 0)), from_other_thread=True) except Exception: from ba import _error _error.print_exception( f'error playing playlist {self._current_playlist}')
def _display_next_achievement() -> None: # Pull the first achievement off the list and display it, or kill the # display-timer if the list is empty. app = _ba.app if app.achievements_to_display: try: ach, sound = app.achievements_to_display.pop(0) ach.show_completion_banner(sound) except Exception: from ba import _error _error.print_exception("error showing next achievement") app.achievements_to_display = [] app.achievement_display_timer = None else: app.achievement_display_timer = None
def player_was_killed(self, player: ba.Player, killed: bool = False, killer: ba.Player = None) -> None: """Should be called when a player is killed.""" from ba._lang import Lstr name = player.get_name() prec = self._player_records[name] prec.streak = 0 if killed: prec.accum_killed_count += 1 prec.killed_count += 1 try: if killed and _ba.getactivity().announce_player_deaths: if killer == player: _ba.screenmessage(Lstr(resource='nameSuicideText', subs=[('${NAME}', name)]), top=True, color=player.color, image=player.get_icon()) elif killer is not None: if killer.team == player.team: _ba.screenmessage(Lstr(resource='nameBetrayedText', subs=[('${NAME}', killer.get_name()), ('${VICTIM}', name)]), top=True, color=killer.color, image=killer.get_icon()) else: _ba.screenmessage(Lstr(resource='nameKilledText', subs=[('${NAME}', killer.get_name()), ('${VICTIM}', name)]), top=True, color=killer.color, image=killer.get_icon()) else: _ba.screenmessage(Lstr(resource='nameDiedText', subs=[('${NAME}', name)]), top=True, color=player.color, image=player.get_icon()) except Exception: from ba import _error _error.print_exception('error announcing kill')
def award_local_achievement(self, achname: str) -> None: """For non-game-based achievements such as controller-connection.""" try: ach = self.get_achievement(achname) if not ach.complete: # Report new achievements to the game-service. _ba.report_achievement(achname) # And to our account. _ba.add_transaction({'type': 'ACHIEVEMENT', 'name': achname}) # Now attempt to show a banner. self.display_achievement_banner(achname) except Exception: print_exception()
def continue_or_end_game(self) -> None: """If continues are allowed, prompts the player to purchase a continue and calls either end_game or continue_game depending on the result""" # pylint: disable=too-many-nested-blocks # pylint: disable=cyclic-import from bastd.ui.continues import ContinuesWindow from ba._coopsession import CoopSession from ba._enums import TimeType try: if _ba.get_account_misc_read_val('enableContinues', False): session = self.session # We only support continuing in non-tournament games. tournament_id = session.tournament_id if tournament_id is None: # We currently only support continuing in sequential # co-op campaigns. if isinstance(session, CoopSession): assert session.campaign is not None if session.campaign.sequential: gnode = self.globalsnode # Only attempt this if we're not currently paused # and there appears to be no UI. if (not gnode.paused and not _ba.app.ui.has_main_menu_window()): self._is_waiting_for_continue = True with _ba.Context('ui'): _ba.timer( 0.5, lambda: ContinuesWindow( self, self._continue_cost, continue_call=WeakCall( self._continue_choice, True), cancel_call=WeakCall( self._continue_choice, False)), timetype=TimeType.REAL) return except Exception: print_exception('Error handling continues.') self.end_game()
def display_name(self) -> ba.Lstr: """Return a ba.Lstr for this Achievement's name.""" from ba._language import Lstr name: Union[ba.Lstr, str] try: if self._level_name != '': from ba._campaign import getcampaign campaignname, campaign_level = self._level_name.split(':') name = getcampaign(campaignname).getlevel( campaign_level).displayname else: name = '' except Exception: name = '' print_exception() return Lstr(resource='achievements.' + self._name + '.name', subs=[('${LEVEL}', name)])
def display_achievement_banner(achname: str) -> None: """Display a completion banner for an achievement. Used for server-driven achievements. """ try: # FIXME: Need to get these using the UI context or some other # purely local context somehow instead of trying to inject these # into whatever activity happens to be active # (since that won't work while in client mode). activity = _ba.get_foreground_host_activity() if activity is not None: with _ba.Context(activity): get_achievement(achname).announce_completion() except Exception: from ba import _error _error.print_exception('error showing server ach')
def leave(self) -> None: """Called when the Player leaves a running game. (internal) """ assert self._postinited assert not self._expired try: # If they still have an actor, kill it. if self.actor: self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME)) self.actor = None except Exception: print_exception(f'Error killing actor on leave for {self}') self._nodeactor = None del self._team del self._customdata
def show_user_scripts() -> None: """Open or nicely print the location of the user-scripts directory.""" from ba import _lang from ba._enums import Permission app = _ba.app # First off, if we need permission for this, ask for it. if not _ba.have_permission(Permission.STORAGE): _ba.playsound(_ba.getsound('error')) _ba.screenmessage(_lang.Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)) _ba.request_permission(Permission.STORAGE) return # Secondly, if the dir doesn't exist, attempt to make it. if not os.path.exists(app.python_directory_user): os.makedirs(app.python_directory_user) # On android, attempt to write a file in their user-scripts dir telling # them about modding. This also has the side-effect of allowing us to # media-scan that dir so it shows up in android-file-transfer, since it # doesn't seem like there's a way to inform the media scanner of an empty # directory, which means they would have to reboot their device before # they can see it. if app.platform == 'android': try: usd: Optional[str] = app.python_directory_user if usd is not None and os.path.isdir(usd): file_name = usd + '/about_this_folder.txt' with open(file_name, 'w') as outfile: outfile.write('You can drop files in here to mod the game.' ' See settings/advanced' ' in the game for more info.') _ba.android_media_scan_file(file_name) except Exception: from ba import _error _error.print_exception('error writing about_this_folder stuff') # On a few platforms we try to open the dir in the UI. if app.platform in ['mac', 'windows']: _ba.open_dir_externally(app.python_directory_user) # Otherwise we just print a pretty version of it. else: _ba.screenmessage(get_human_readable_user_scripts_path())
def expire(self) -> None: """Called when the Player is expiring (when its Activity does so). (internal) """ assert self._postinited assert not self._expired self._expired = True try: self.on_expire() except Exception: print_exception(f'Error in on_expire for {self}.') self._nodeactor = None self.actor = None del self._team del self._customdata
def reload_profiles(self) -> None: """Reload available player profiles.""" # pylint: disable=cyclic-import from bastd.actor.spazappearance import get_appearances # We may have gained or lost character names if the user # bought something; reload these too. self.character_names_local_unlocked = get_appearances() self.character_names_local_unlocked.sort(key=lambda x: x.lower()) # Do any overall prep we need to such as creating account profile. _ba.app.accounts.ensure_have_account_player_profile() for chooser in self.choosers: try: chooser.reload_profiles() chooser.update_from_profile() except Exception: print_exception('Error reloading profiles.')
def verify_object_death(obj: object) -> None: """Warn if an object does not get freed within a short period. Category: General Utility Functions This can be handy to detect and prevent memory/resource leaks. """ try: ref = weakref.ref(obj) except Exception: print_exception('Unable to create weak-ref in verify_object_death') # Use a slight range for our checks so they don't all land at once # if we queue a lot of them. delay = random.uniform(2.0, 5.5) with _ba.Context('ui'): _ba.timer(delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL)
def _award_achievement(self, achievement_name: str, sound: bool = True) -> None: """Award an achievement. Returns True if a banner will be shown; False otherwise """ from ba._achievement import get_achievement if achievement_name in self._achievements_awarded: return ach = get_achievement(achievement_name) # If we're in the easy campaign and this achievement is hard-mode-only, # ignore it. try: campaign = self.session.campaign assert campaign is not None if ach.hard_mode_only and campaign.name == 'Easy': return except Exception: from ba._error import print_exception print_exception() # If we haven't awarded this one, check to see if we've got it. # If not, set it through the game service *and* add a transaction # for it. if not ach.complete: self._achievements_awarded.add(achievement_name) # Report new achievements to the game-service. _ba.report_achievement(achievement_name) # ...and to our account. _ba.add_transaction({ 'type': 'ACHIEVEMENT', 'name': achievement_name }) # Now bring up a celebration banner. ach.announce_completion(sound=sound)
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None: """Remove a player from the Activity while it is running. (internal) """ assert not self.expired player: Any = sessionplayer.activityplayer assert isinstance(player, self._playertype) team: Any = sessionplayer.sessionteam.activityteam assert isinstance(team, self._teamtype) assert player in team.players team.players.remove(player) assert player not in team.players assert player in self.players self.players.remove(player) assert player not in self.players # This should allow our ba.Player instance to die. # Complain if that doesn't happen. # verify_object_death(player) with _ba.Context(self): try: self.on_player_leave(player) except Exception: print_exception(f'Error in on_player_leave for {self}.') try: player.leave() except Exception: print_exception(f'Error on leave for {player} in {self}.') self._reset_session_player_for_no_activity(sessionplayer) # Add the player to a list to keep it around for a while. This is # to discourage logic from firing on player object death, which # may not happen until activity end if something is holding refs # to it. self._delay_delete_players.append(player) self._players_that_left.append(weakref.ref(player))
def _getname(self, full: bool = False) -> str: name_raw = name = self._profilenames[self._profileindex] if name[0] in self._glowing: name = name[1:] clamp = False if full: try: if self._profiles[name_raw].get('global', False): icon = (self._profiles[name_raw]['icon'] if 'icon' in self._profiles[name_raw] else _ba.charstr(SpecialChar.LOGO)) name = icon + name except Exception: print_exception('Error applying global icon.') else: clamp = True if clamp and len(name) > 10: name = name[:10] + '...' return name return self._getname_glowing(full)
def get_soundtrack_entry_name(self, entry: Any) -> str: """Given a soundtrack entry, returns its name.""" try: if entry is None: raise TypeError('entry is None') # Simple string denotes an iTunesPlaylist name (legacy entry). if isinstance(entry, str): return entry # For other entries we expect type and name strings in a dict. if (isinstance(entry, dict) and 'type' in entry and isinstance(entry['type'], str) and 'name' in entry and isinstance(entry['name'], str)): return entry['name'] raise ValueError('invalid soundtrack entry:' + str(entry)) except Exception: from ba import _error _error.print_exception() return 'default'
def get_available_purchase_count(tab: str = None) -> int: """(internal)""" try: if _ba.get_account_state() != 'signed_in': return 0 count = 0 our_tickets = _ba.get_account_ticket_count() store_data = get_store_layout() if tab is not None: tabs = [(tab, store_data[tab])] else: tabs = list(store_data.items()) for tab_name, tabval in tabs: if tab_name == 'icons': continue # too many of these; don't show.. count = _calc_count_for_tab(tabval, our_tickets, count) return count except Exception: from ba import _error _error.print_exception('error calcing available purchases') return 0
def get_input_map_hash(inputdevice: ba.InputDevice) -> str: """Given an input device, return a hash based on its raw input values. This lets us avoid sharing mappings across devices that may have the same name but actually produce different input values. (Different Android versions, for example, may return different key codes for button presses on a given type of controller) """ del inputdevice # Currently unused. app = _ba.app try: if app.input_map_hash is None: if app.platform == 'android': app.input_map_hash = _gen_android_input_hash() else: app.input_map_hash = '' return app.input_map_hash except Exception: from ba import _error _error.print_exception('Exception in get_input_map_hash') return ''
def _expire(self) -> None: """Put the activity in a state where it can be garbage-collected. This involves clearing anything that might be holding a reference to it, etc. """ assert not self._expired self._expired = True try: self.on_expire() except Exception: print_exception(f'Error in Activity on_expire() for {self}.') try: self._customdata = None except Exception: print_exception(f'Error clearing customdata for {self}.') # Don't want to be holding any delay-delete refs at this point. self._prune_delay_deletes() self._expire_actors() self._expire_players() self._expire_teams() # This will kill all low level stuff: Timers, Nodes, etc., which # should clear up any remaining refs to our Activity and allow us # to die peacefully. try: self._activity_data.expire() except Exception: print_exception(f'Error expiring _activity_data for {self}.')
def set_main_menu_window(self, window: ba.Widget) -> None: """Set the current 'main' window, replacing any existing.""" existing = self._main_menu_window from ba._generated.enums import TimeType from inspect import currentframe, getframeinfo # Let's grab the location where we were called from to report # if we have to force-kill the existing window (which normally # should not happen). frameline = None try: frame = currentframe() if frame is not None: frame = frame.f_back if frame is not None: frameinfo = getframeinfo(frame) frameline = f'{frameinfo.filename} {frameinfo.lineno}' except Exception: from ba._error import print_exception print_exception('Error calcing line for set_main_menu_window') # With our legacy main-menu system, the caller is responsible for # clearing out the old main menu window when assigning the new. # However there are corner cases where that doesn't happen and we get # old windows stuck under the new main one. So let's guard against # that. However, we can't simply delete the existing main window when # a new one is assigned because the user may transition the old out # *after* the assignment. Sigh. So, as a happy medium, let's check in # on the old after a short bit of time and kill it if its still alive. # That will be a bit ugly on screen but at least should un-break # things. def _delay_kill() -> None: import time if existing: print(f'Killing old main_menu_window' f' when called at: {frameline} t={time.time():.3f}') existing.delete() _ba.timer(1.0, _delay_kill, timetype=TimeType.REAL) self._main_menu_window = window
def _reset_session_player_for_no_activity( self, sessionplayer: ba.SessionPlayer) -> None: # Let's be extra-defensive here: killing a node/input-call/etc # could trigger user-code resulting in errors, but we would still # like to complete the reset if possible. try: sessionplayer.setnode(None) except Exception: print_exception( f'Error resetting SessionPlayer node on {sessionplayer}' f' for {self}.') try: sessionplayer.resetinput() except Exception: print_exception( f'Error resetting SessionPlayer input on {sessionplayer}' f' for {self}.') # These should never fail I think... sessionplayer.setactivity(None) sessionplayer.activityplayer = None
def _check_activity_death(cls, activity_ref: ReferenceType[Activity], counter: List[int]) -> None: """Sanity check to make sure an Activity was destroyed properly. Receives a weakref to a ba.Activity which should have torn itself down due to no longer being referenced anywhere. Will complain and/or print debugging info if the Activity still exists. """ try: import gc import types activity = activity_ref() print('ERROR: Activity is not dying when expected:', activity, '(warning ' + str(counter[0] + 1) + ')') print('This means something is still strong-referencing it.') counter[0] += 1 # FIXME: Running the code below shows us references but winds up # keeping the object alive; need to figure out why. # For now we just print refs if the count gets to 3, and then we # kill the app at 4 so it doesn't matter anyway. if counter[0] == 3: print('Activity references for', activity, ':') refs = list(gc.get_referrers(activity)) i = 1 for ref in refs: if isinstance(ref, types.FrameType): continue print(' reference', i, ':', ref) i += 1 if counter[0] == 4: print('Killing app due to stuck activity... :-(') _ba.quit() except Exception: from ba import _error _error.print_exception('exception on _check_activity_death:')
def _getname(self, full: bool = False) -> str: name_raw = name = self._profilenames[self._profileindex] clamp = False if name == '_random': try: name = ( self._sessionplayer.inputdevice.get_default_player_name()) except Exception: print_exception('Error getting _random chooser name.') name = 'Invalid' clamp = not full elif name == '__account__': try: name = self._sessionplayer.inputdevice.get_v1_account_name( full) except Exception: print_exception('Error getting account name for chooser.') name = 'Invalid' clamp = not full elif name == '_edit': # Explicitly flattening this to a str; it's only relevant on # the host so that's ok. name = (Lstr( resource='createEditPlayerText', fallback_resource='editProfileWindow.titleNewText').evaluate()) else: # If we have a regular profile marked as global with an icon, # use it (for full only). if full: try: if self._profiles[name_raw].get('global', False): icon = (self._profiles[name_raw]['icon'] if 'icon' in self._profiles[name_raw] else _ba.charstr(SpecialChar.LOGO)) name = icon + name except Exception: print_exception('Error applying global icon.') else: # We now clamp non-full versions of names so there's at # least some hope of reading them in-game. clamp = True if clamp: if len(name) > 10: name = name[:10] + '...' return name
def __init__(self, depsets: Sequence[ba.DependencySet], team_names: Sequence[str] = None, team_colors: Sequence[Sequence[float]] = None, use_team_colors: bool = True, min_players: int = 1, max_players: int = 8, allow_mid_activity_joins: bool = True): """Instantiate a session. depsets should be a sequence of successfully resolved ba.DependencySet instances; one for each ba.Activity the session may potentially run. """ # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import from ba._lobby import Lobby from ba._stats import Stats from ba._gameutils import sharedobj from ba._gameactivity import GameActivity from ba._team import Team from ba._error import DependencyError from ba._dependency import Dependency, AssetPackage # First off, resolve all dependency-sets we were passed. # If things are missing, we'll try to gather them into a single # missing-deps exception if possible to give the caller a clean # path to download missing stuff and try again. missing_asset_packages: Set[str] = set() for depset in depsets: try: depset.resolve() except DependencyError as exc: # Gather/report missing assets only; barf on anything else. if all(issubclass(d.cls, AssetPackage) for d in exc.deps): for dep in exc.deps: assert isinstance(dep.config, str) missing_asset_packages.add(dep.config) else: missing_info = [(d.cls, d.config) for d in exc.deps] raise RuntimeError( f'Missing non-asset dependencies: {missing_info}') # Throw a combined exception if we found anything missing. if missing_asset_packages: raise DependencyError([ Dependency(AssetPackage, set_id) for set_id in missing_asset_packages ]) # Ok; looks like our dependencies check out. # Now give the engine a list of asset-set-ids to pass along to clients. required_asset_packages: Set[str] = set() for depset in depsets: required_asset_packages.update(depset.get_asset_package_ids()) # print('Would set host-session asset-reqs to:', # required_asset_packages) # First thing, wire up our internal engine data. self._sessiondata = _ba.register_session(self) self.tournament_id: Optional[str] = None # FIXME: This stuff shouldn't be here. self.sharedobjs: Dict[str, Any] = {} # TeamGameActivity uses this to display a help overlay on the first # activity only. self.have_shown_controls_help_overlay = False self.campaign = None # FIXME: Should be able to kill this I think. self.campaign_state: Dict[str, str] = {} self._use_teams = (team_names is not None) self._use_team_colors = use_team_colors self._in_set_activity = False self._allow_mid_activity_joins = allow_mid_activity_joins self.teams = [] self.players = [] self._next_team_id = 0 self._activity_retained: Optional[ba.Activity] = None self.launch_end_session_activity_time: Optional[float] = None self._activity_end_timer: Optional[ba.Timer] = None # Hacky way to create empty weak ref; must be a better way. class _EmptyObj: pass self._activity_weak: ReferenceType[ba.Activity] self._activity_weak = weakref.ref(_EmptyObj()) # type: ignore if self._activity_weak() is not None: raise Exception('Error creating empty activity weak ref.') self._next_activity: Optional[ba.Activity] = None self.wants_to_end = False self._ending = False self.min_players = min_players self.max_players = max_players # Create Teams. if self._use_teams: assert team_names is not None assert team_colors is not None for i, color in enumerate(team_colors): team = Team(team_id=self._next_team_id, name=GameActivity.get_team_display_string( team_names[i]), color=color) self.teams.append(team) self._next_team_id += 1 try: with _ba.Context(self): self.on_team_join(team) except Exception: from ba import _error _error.print_exception( f'Error in on_team_join for {self}.') self.lobby = Lobby() self.stats = Stats() # Instantiate our session globals node # (so it can apply default settings). sharedobj('globals')
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.Player: # pylint: disable=too-many-statements # pylint: disable=too-many-branches from ba import _error from ba._lang import Lstr from ba._team import Team from ba import _freeforallsession player = chooser.getplayer() if player not in self.players: _error.print_error('player not found in session ' 'player-list after chooser selection') activity = self._activity_weak() assert activity is not None # We need to reset the player's input here, as it is currently # referencing the chooser which could inadvertently keep it alive. player.reset_input() # Pass it to the current activity if it has already begun # (otherwise it'll get passed once begin is called). pass_to_activity = (activity.has_begun() and not activity.is_joining_activity) # If we're not allowing mid-game joins, don't pass; just announce # the arrival. if pass_to_activity: if not self._allow_mid_activity_joins: pass_to_activity = False with _ba.Context(self): _ba.screenmessage(Lstr(resource='playerDelayedJoinText', subs=[('${PLAYER}', player.get_name(full=True)) ]), color=(0, 1, 0)) # If we're a non-team game, each player gets their own team # (keeps mini-game coding simpler if we can always deal with teams). if self._use_teams: team = chooser.get_team() else: our_team_id = self._next_team_id team = Team(team_id=our_team_id, name=chooser.getplayer().get_name(full=True, icon=False), color=chooser.get_color()) self.teams.append(team) self._next_team_id += 1 try: with _ba.Context(self): self.on_team_join(team) except Exception: _error.print_exception(f'exception in on_team_join for {self}') if pass_to_activity: if team in activity.teams: _error.print_error( 'Duplicate team ID in ba.Session._add_chosen_player') activity.teams.append(team) try: with _ba.Context(activity): activity.on_team_join(team) except Exception: _error.print_exception( f'ERROR: exception in on_team_join for {activity}') player.set_data(team=team, character=chooser.get_character_name(), color=chooser.get_color(), highlight=chooser.get_highlight()) self.stats.register_player(player) if pass_to_activity: if isinstance(self, _freeforallsession.FreeForAllSession): if player.team.players: _error.print_error('expected 0 players in FFA team') # Don't actually add the player to their team list if we're not # in an activity. (players get (re)added to their team lists # when the activity begins). player.team.players.append(player) if player in activity.players: _error.print_exception( f'Dup player in ba.Session._add_chosen_player: {player}') else: activity.players.append(player) player.set_activity(activity) pnode = activity.create_player_node(player) player.set_node(pnode) try: with _ba.Context(activity): activity.on_player_join(player) except Exception: _error.print_exception( f'Error on on_player_join for {activity}') return player
def on_player_leave(self, player: ba.Player) -> None: """Called when a previously-accepted ba.Player leaves the session.""" # pylint: disable=too-many-statements # pylint: disable=too-many-branches # pylint: disable=cyclic-import from ba._freeforallsession import FreeForAllSession from ba._lang import Lstr from ba import _error # Remove them from the game rosters. if player in self.players: _ba.playsound(_ba.getsound('playerLeft')) team: Optional[ba.Team] # The player will have no team if they are still in the lobby. try: team = player.team except _error.TeamNotFoundError: team = None activity = self._activity_weak() # If he had no team, he's in the lobby. # If we have a current activity with a lobby, ask them to # remove him. if team is None: with _ba.Context(self): try: self.lobby.remove_chooser(player) except Exception: _error.print_exception( 'Error in Lobby.remove_chooser()') # *If* they were actually in the game, announce their departure. if team is not None: _ba.screenmessage( Lstr(resource='playerLeftText', subs=[('${PLAYER}', player.get_name(full=True))])) # Remove him from his team and session lists. # (he may not be on the team list since player are re-added to # team lists every activity) if team is not None and player in team.players: # Testing; can remove this eventually. if isinstance(self, FreeForAllSession): if len(team.players) != 1: _error.print_error('expected 1 player in FFA team') team.players.remove(player) # Remove player from any current activity. if activity is not None and player in activity.players: activity.players.remove(player) # Run the activity callback unless its been expired. if not activity.is_expired(): try: with _ba.Context(activity): activity.on_player_leave(player) except Exception: _error.print_exception( 'exception in on_player_leave for activity', activity) else: _error.print_error('expired activity in on_player_leave;' " shouldn't happen") player.set_activity(None) player.set_node(None) # Reset the player; this will remove its actor-ref and clear # its calls/etc try: with _ba.Context(activity): player.reset() except Exception: _error.print_exception( 'exception in player.reset in' ' on_player_leave for player', player) # If we're a non-team session, remove the player's team completely. if not self._use_teams and team is not None: # If the team's in an activity, call its on_team_leave # callback. if activity is not None and team in activity.teams: activity.teams.remove(team) if not activity.is_expired(): try: with _ba.Context(activity): activity.on_team_leave(team) except Exception: _error.print_exception( 'exception in on_team_leave for activity', activity) else: _error.print_error( 'expired activity in on_player_leave p2' "; shouldn't happen") # Clear the team's game-data (so dying stuff will # have proper context). try: with _ba.Context(activity): team.reset_gamedata() except Exception: _error.print_exception( 'exception clearing gamedata for team:', team, 'for player:', player, 'in activity:', activity) # Remove the team from the session. self.teams.remove(team) try: with _ba.Context(self): self.on_team_leave(team) except Exception: _error.print_exception( 'exception in on_team_leave for session', self) # Clear the team's session-data (so dying stuff will # have proper context). try: with _ba.Context(self): team.reset_sessiondata() except Exception: _error.print_exception( 'exception clearing sessiondata for team:', team, 'in session:', self) # Now remove them from the session list. self.players.remove(player) else: print('ERROR: Session.on_player_leave called' ' for player not in our list.')
def get_resource(resource: str, fallback_resource: str = None, fallback_value: Any = None) -> Any: """Return a translation resource by name.""" try: # If we have no language set, go ahead and set it. if _ba.app.language_merged is None: language = _ba.app.language try: setlanguage(language, print_change=False, store_to_config=False) except Exception: from ba import _error _error.print_exception('exception setting language to', language) # Try english as a fallback. if language != 'English': print('Resorting to fallback language (English)') try: setlanguage('English', print_change=False, store_to_config=False) except Exception: _error.print_exception( 'error setting language to english fallback') # If they provided a fallback_resource value, try the # target-language-only dict first and then fall back to trying the # fallback_resource value in the merged dict. if fallback_resource is not None: try: values = _ba.app.language_target splits = resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # FIXME: Shouldn't we try the fallback resource in the merged # dict AFTER we try the main resource in the merged dict? try: values = _ba.app.language_merged splits = fallback_resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # If we got nothing for fallback_resource, default to the # normal code which checks or primary value in the merge # dict; there's a chance we can get an english value for # it (which we weren't looking for the first time through). pass values = _ba.app.language_merged splits = resource.split('.') dicts = splits[:-1] key = splits[-1] for dct in dicts: assert values is not None values = values[dct] assert values is not None val = values[key] return val except Exception: # Ok, looks like we couldn't find our main or fallback resource # anywhere. Now if we've been given a fallback value, return it; # otherwise fail. from ba import _error if fallback_value is not None: return fallback_value raise _error.NotFoundError( f"Resource not found: '{resource}'") from None
def setlanguage(language: Optional[str], print_change: bool = True, store_to_config: bool = True) -> None: """Set the active language used for the game. category: General Utility Functions Pass None to use OS default language. """ # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=too-many-branches cfg = _ba.app.config cur_language = cfg.get('Lang', None) # Store this in the config if its changing. if language != cur_language and store_to_config: if language is None: if 'Lang' in cfg: del cfg['Lang'] # Clear it out for default. else: cfg['Lang'] = language cfg.commit() switched = True else: switched = False with open('ba_data/data/languages/english.json') as infile: lenglishvalues = json.loads(infile.read()) # None implies default. if language is None: language = _ba.app.default_language try: if language == 'English': lmodvalues = None else: lmodfile = 'ba_data/data/languages/' + language.lower() + '.json' with open(lmodfile) as infile: lmodvalues = json.loads(infile.read()) except Exception: from ba import _error _error.print_exception('Exception importing language:', language) _ba.screenmessage("Error setting language to '" + language + "'; see log for details", color=(1, 0, 0)) switched = False lmodvalues = None # Create an attrdict of *just* our target language. _ba.app.language_target = AttrDict() langtarget = _ba.app.language_target assert langtarget is not None _add_to_attr_dict(langtarget, lmodvalues if lmodvalues is not None else lenglishvalues) # Create an attrdict of our target language overlaid on our base (english). languages = [lenglishvalues] if lmodvalues is not None: languages.append(lmodvalues) lfull = AttrDict() for lmod in languages: _add_to_attr_dict(lfull, lmod) _ba.app.language_merged = lfull # Pass some keys/values in for low level code to use; # start with everything in their 'internal' section. internal_vals = [ v for v in list(lfull['internal'].items()) if isinstance(v[1], str) ] # Cherry-pick various other values to include. # (should probably get rid of the 'internal' section # and do everything this way) for value in [ 'replayNameDefaultText', 'replayWriteErrorText', 'replayVersionErrorText', 'replayReadErrorText' ]: internal_vals.append((value, lfull[value])) internal_vals.append( ('axisText', lfull['configGamepadWindow']['axisText'])) internal_vals.append(('buttonText', lfull['buttonText'])) lmerged = _ba.app.language_merged assert lmerged is not None random_names = [ n.strip() for n in lmerged['randomPlayerNamesText'].split(',') ] random_names = [n for n in random_names if n != ''] _ba.set_internal_language_keys(internal_vals, random_names) if switched and print_change: _ba.screenmessage(Lstr(resource='languageSetText', subs=[('${LANGUAGE}', Lstr(translate=('languages', language))) ]), color=(0, 1, 0))