Exemple #1
0
    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
Exemple #2
0
 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()
Exemple #3
0
 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}')
Exemple #4
0
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
Exemple #5
0
 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')
Exemple #6
0
    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()
Exemple #8
0
 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)])
Exemple #9
0
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')
Exemple #10
0
    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
Exemple #11
0
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())
Exemple #12
0
    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
Exemple #13
0
    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)
Exemple #16
0
    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))
Exemple #17
0
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)
Exemple #18
0
    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'
Exemple #19
0
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
Exemple #20
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 ''
Exemple #21
0
    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}.')
Exemple #22
0
    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
Exemple #23
0
    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
Exemple #24
0
    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:')
Exemple #25
0
    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
Exemple #26
0
    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')
Exemple #27
0
    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
Exemple #28
0
    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))