def _complete_end_activity(self, activity: ba.Activity, results: Any) -> None: # Run the subclass callback in the session context. try: with _ba.Context(self): self.on_activity_end(activity, results) except Exception: from ba import _error _error.print_exception( 'exception in on_activity_end() for session', self, 'activity', activity, 'with results', results)
def transition_in(self, prev_globals: Optional[ba.Node]) -> None: """Called by Session to kick off transition-in. (internal) """ from ba._general import WeakCall from ba._gameutils import sharedobj assert not self._has_transitioned_in self._has_transitioned_in = True # Set up the globals node based on our settings. with _ba.Context(self): # Now that it's going to be front and center, # set some global values based on what the activity wants. glb = sharedobj('globals') glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay glb.allow_kick_idle_players = self.allow_kick_idle_players if self.inherits_slow_motion and prev_globals is not None: glb.slow_motion = prev_globals.slow_motion else: glb.slow_motion = self.slow_motion if self.inherits_music and prev_globals is not None: glb.music_continuous = True # Prevent restarting same music. glb.music = prev_globals.music glb.music_count += 1 if self.inherits_camera_vr_offset and prev_globals is not None: glb.vr_camera_offset = prev_globals.vr_camera_offset if self.inherits_vr_overlay_center and prev_globals is not None: glb.vr_overlay_center = prev_globals.vr_overlay_center glb.vr_overlay_center_enabled = ( prev_globals.vr_overlay_center_enabled) # If they want to inherit tint from the previous self. if self.inherits_tint and prev_globals is not None: glb.tint = prev_globals.tint glb.vignette_outer = prev_globals.vignette_outer glb.vignette_inner = prev_globals.vignette_inner # Start pruning our transient actors periodically. self._prune_dead_actors_timer = _ba.Timer( 5.17, WeakCall(self._prune_dead_actors), repeat=True) self._prune_dead_actors() # Also start our low-level scene running. self._activity_data.start() try: self.on_transition_in() except Exception: print_exception('Error in on_transition_in for', self) # Tell the C++ layer that this activity is the main one, so it uses # settings from our globals, directs various events to us, etc. self._activity_data.make_foreground()
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None: """Called when a previously-accepted ba.SessionPlayer leaves.""" if sessionplayer not in self.sessionplayers: print('ERROR: Session.on_player_leave called' ' for player not in our list.') return _ba.playsound(_ba.getsound('playerLeft')) activity = self._activity_weak() if not sessionplayer.in_game: # Ok, the player is still in the lobby; simply remove them. with _ba.Context(self): try: self.lobby.remove_chooser(sessionplayer) except Exception: print_exception('Error in Lobby.remove_chooser().') else: # Ok, they've already entered the game. Remove them from # teams/activities/etc. sessionteam = sessionplayer.sessionteam assert sessionteam is not None _ba.screenmessage( Lstr(resource='playerLeftText', subs=[('${PLAYER}', sessionplayer.getname(full=True))])) # Remove them from their SessionTeam. if sessionplayer in sessionteam.players: sessionteam.players.remove(sessionplayer) else: print('SessionPlayer not found in SessionTeam' ' in on_player_leave.') # Grab their activity-specific player instance. player = sessionplayer.activityplayer assert isinstance(player, (Player, type(None))) # Remove them from any current Activity. if activity is not None: if player in activity.players: activity.remove_player(sessionplayer) else: print('Player not found in Activity in on_player_leave.') # If we're a non-team session, remove their team too. if not self.use_teams: self._remove_player_team(sessionteam, activity) # Now remove them from the session list. self.sessionplayers.remove(sessionplayer)
def __init__(self) -> None: super().__init__() # This is used internally by the get_package_xxx calls. self.context = _ba.Context('current') entry = self._dep_entry() assert entry is not None assert isinstance(entry.config, str) self.package_id = entry.config print(f'LOADING ASSET PACKAGE {self.package_id}')
def setactivity(self, activity: Optional[ba.Activity]) -> None: """Set the current activity for this instance.""" self._activity = None if activity is None else weakref.ref(activity) # Load our media into this activity's context. if activity is not None: if activity.expired: print_error('unexpected finalized activity') else: with _ba.Context(activity): self._load_activity_media()
def add_team(self, sessionteam: ba.SessionTeam) -> None: """(internal)""" assert not self.expired with _ba.Context(self): sessionteam.gameteam = team = self.create_team(sessionteam) team.postinit(sessionteam) self.teams.append(team) try: self.on_team_join(team) except Exception: print_exception(f'Error in on_team_join for {self}')
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 __del__(self) -> None: from ba._apputils import garbage_collect, call_after_ad # If the activity has been run then we should have already cleaned # it up, but we still need to run expire calls for un-run activities. if not self._expired: with _ba.Context('empty'): self._expire() # Since we're mostly between activities at this point, lets run a cycle # of garbage collection; hopefully it won't cause hitches here. garbage_collect(session_end=False) # Now that our object is officially gonna be dead, tell the session it # can fire up the next activity. if self._transitioning_out: session = self._session() if session is not None: with _ba.Context(session): if self.can_show_ad_on_death: call_after_ad(session.begin_next_activity) else: _ba.pushcall(session.begin_next_activity)
def __del__(self) -> None: # If the activity has been run then we should have already cleaned # it up, but we still need to run expire calls for un-run activities. if not self._expired: with _ba.Context('empty'): self._expire() # Inform our owner that we officially kicked the bucket. if self._transitioning_out: session = self._session() if session is not None: _ba.pushcall( Call(session.transitioning_out_activity_was_freed, self.can_show_ad_on_death))
def handlemessage(self, msg: Any) -> Any: """General message handling; can be passed any message object.""" from ba._lobby import PlayerReadyMessage from ba._messages import PlayerProfilesChangedMessage, UNHANDLED if isinstance(msg, PlayerReadyMessage): self._on_player_ready(msg.chooser) return None if isinstance(msg, PlayerProfilesChangedMessage): # If we have a current activity with a lobby, ask it to reload # profiles. with _ba.Context(self): self.lobby.reload_profiles() return None return UNHANDLED
def transitioning_out_activity_was_freed( self, can_show_ad_on_death: bool) -> None: """(internal)""" from ba._apputils import garbage_collect # Since things should be generally still right now, it's a good time # to run garbage collection to clear out any circular dependency # loops. We keep this disabled normally to avoid non-deterministic # hitches. garbage_collect() with _ba.Context(self): if can_show_ad_on_death: _ba.app.ads.call_after_ad(self.begin_next_activity) else: _ba.pushcall(self.begin_next_activity)
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 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 __init__(self, request: str, request_type: str, data: Optional[Dict[str, Any]], callback: Optional[ServerCallbackType], response_type: ServerResponseType): super().__init__() self._request = request self._request_type = request_type if not isinstance(response_type, ServerResponseType): raise Exception(f'Invalid response type: {response_type}') self._response_type = response_type self._data = {} if data is None else copy.deepcopy(data) self._callback: Optional[ServerCallbackType] = callback self._context = _ba.Context('current') # Save and restore the context we were created from. activity: Optional[ba.Activity] = _ba.getactivity(doraise=False) self._activity = weakref.ref( activity) if activity is not None else None
def _execute_shutdown(self) -> None: from ba._lang import Lstr if self._executing_shutdown: return self._executing_shutdown = True timestrval = time.strftime('%c') if self._shutdown_reason is ShutdownReason.RESTARTING: _ba.screenmessage(Lstr(resource='internal.serverRestartingText'), color=(1, 0.5, 0.0)) print(f'{Clr.SBLU}Exiting for server-restart' f' at {timestrval}{Clr.RST}') else: _ba.screenmessage(Lstr(resource='internal.serverShuttingDownText'), color=(1, 0.5, 0.0)) print(f'{Clr.SBLU}Exiting for server-shutdown' f' at {timestrval}{Clr.RST}') with _ba.Context('ui'): _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def _continue_choice(self, do_continue: bool) -> None: self._is_waiting_for_continue = False if self.has_ended(): return with _ba.Context(self): if do_continue: _ba.playsound(_ba.getsound('shieldUp')) _ba.playsound(_ba.getsound('cashRegister')) _ba.add_transaction({ 'type': 'CONTINUE', 'cost': self._continue_cost }) _ba.run_transactions() self._continue_cost = ( self._continue_cost * self._continue_cost_mult + self._continue_cost_offset) self.on_continue() else: self.end_game()
def _launch_end_session_activity(self) -> None: """(internal)""" from ba._activitytypes import EndSessionActivity from ba._enums import TimeType with _ba.Context(self): curtime = _ba.time(TimeType.REAL) if self._ending: # Ignore repeats unless its been a while. assert self._launch_end_session_activity_time is not None since_last = (curtime - self._launch_end_session_activity_time) if since_last < 30.0: return print_error( '_launch_end_session_activity called twice (since_last=' + str(since_last) + ')') self._launch_end_session_activity_time = curtime self.setactivity(_ba.newactivity(EndSessionActivity)) self._wants_to_end = False self._ending = True # Prevent further actions.
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 resume(self) -> None: """Resume the game due to a user request or menu closing. If there's a foreground host-activity that's currently paused, tell it to resume. """ # FIXME: Shouldn't be touching scene stuff here; # should just pass the request on to the host-session. activity = _ba.get_foreground_host_activity() if activity is not None: with _ba.Context(activity): globs = activity.globalsnode if globs.paused: _ba.playsound(_ba.getsound('refWhistle')) globs.paused = False # FIXME: This should not be an actor attr. activity.paused_text = None
def restart(self) -> None: """Restart the current game activity.""" # Tell the current activity to end with a 'restart' outcome. # We use 'force' so that we apply even if end has already been called # (but is in its delay period). # Make an exception if there's no players left. Otherwise this # can override the default session end that occurs in that case. if not self.players: return # This method may get called from the UI context so make sure we # explicitly run in the activity's context. activity = self.getactivity() if activity is not None and not activity.is_expired(): activity.can_show_ad_on_death = True with _ba.Context(activity): activity.end(results={'outcome': 'restart'}, force=True)
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 do_remove_in_game_ads_message(self) -> None: """(internal)""" from ba._lang import Lstr from ba._enums import TimeType # Print this message once every 10 minutes at most. tval = _ba.time(TimeType.REAL) if (self.last_in_game_ad_remove_message_show_time is None or (tval - self.last_in_game_ad_remove_message_show_time > 60 * 10)): self.last_in_game_ad_remove_message_show_time = tval with _ba.Context('ui'): _ba.timer( 1.0, lambda: _ba.screenmessage(Lstr( resource='removeInGameAdsText', subs=[('${PRO}', Lstr(resource='store.bombSquadProNameText')), ('${APP_NAME}', Lstr(resource='titleText'))]), color=(1, 1, 0)), timetype=TimeType.REAL)
def _execute_shutdown(self) -> None: from ba._lang import Lstr if self._executing_shutdown: return self._executing_shutdown = True timestrval = time.strftime('%c') if self._shutdown_reason is ShutdownReason.RESTARTING: # FIXME: Should add a server-screen-message call. # (so we could send this an an Lstr) _ba.chat_message( Lstr(resource='internal.serverRestartingText').evaluate()) print(f'{Clr.SBLU}Exiting for server-restart' f' at {timestrval}{Clr.RST}') else: # FIXME: Should add a server-screen-message call. # (so we could send this an an Lstr) print(f'{Clr.SBLU}Exiting for server-shutdown' f' at {timestrval}{Clr.RST}') _ba.chat_message( Lstr(resource='internal.serverShuttingDownText').evaluate()) with _ba.Context('ui'): _ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
def begin(self, session: ba.Session) -> None: """Begin the activity. (internal) """ # Is this ever still happening?... if self._has_begun: print_error("_begin called twice; this shouldn't happen") return # Inherit stats from the session. self._stats = session.stats # Add session's teams in. for team in session.teams: self.add_team(team) # Add session's players in. for player in session.players: self.add_player(player) # And finally tell the game to start. with _ba.Context(self): self._has_begun = True self.on_begin() self._sanity_check_begin_call() # If the whole session wants to die and was waiting on us, # can kick off that process now. if session.wants_to_end: session.launch_end_session_activity() else: # Otherwise, if we've already been told to die, do so now. if self._should_end_immediately: self.end(self._should_end_immediately_results, self._should_end_immediately_delay)
def _handle_empty_activity(self) -> None: """Handle cases where all players have left the current activity.""" from ba._gameactivity import GameActivity activity = self.getactivity() if activity is None: return # Hmm what should we do in this case? # If there are still players in the current activity, we're good. if activity.players: return # If there are *not* players in the current activity but there # *are* in the session: if not activity.players and self.sessionplayers: # If we're in a game, we should restart to pull in players # currently waiting in the session. if isinstance(activity, GameActivity): # Never restart tourney games however; just end the session # if all players are gone. if self.tournament_id is not None: self.end() else: self.restart() # Hmm; no players anywhere. Let's end the entire session if we're # running a GUI (or just the current game if we're running headless). else: if not _ba.app.headless_mode: self.end() else: if isinstance(activity, GameActivity): with _ba.Context(activity): activity.end_game()
def on_app_launch(self) -> None: """Should be run on app launch.""" from ba.ui import UIController, ui_upkeep from ba._generated.enums import TimeType # IMPORTANT: If tweaking UI stuff, make sure it behaves for small, # medium, and large UI modes. (doesn't run off screen, etc). # The overrides below can be used to test with different sizes. # Generally small is used on phones, medium is used on tablets/tvs, # and large is on desktop computers or perhaps large tablets. When # possible, run in windowed mode and resize the window to assure # this holds true at all aspect ratios. # UPDATE: A better way to test this is now by setting the environment # variable BA_UI_SCALE to "small", "medium", or "large". # This will affect system UIs not covered by the values below such # as screen-messages. The below values remain functional, however, # for cases such as Android where environment variables can't be set # easily. if bool(False): # force-test ui scale self._uiscale = UIScale.SMALL with _ba.Context('ui'): _ba.pushcall(lambda: _ba.screenmessage( f'FORCING UISCALE {self._uiscale.name} FOR TESTING', color=(1, 0, 1), log=True)) self.controller = UIController() # Kick off our periodic UI upkeep. # FIXME: Can probably kill this if we do immediate UI death checks. self.upkeeptimer = _ba.Timer(2.6543, ui_upkeep, timetype=TimeType.REAL, repeat=True)
def set_activity(self, activity: ba.Activity) -> None: """Assign a new current ba.Activity for the session. Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.set_activity(foo) and then ba.newnode() to add a node to foo) """ # pylint: disable=too-many-statements # pylint: disable=too-many-branches from ba import _error from ba._gameutils import sharedobj from ba._enums import TimeType # Sanity test: make sure this doesn't get called recursively. if self._in_set_activity: raise Exception( 'Session.set_activity() cannot be called recursively.') if activity.session is not _ba.getsession(): raise Exception("Provided Activity's Session is not current.") # Quietly ignore this if the whole session is going down. if self._ending: return if activity is self._activity_retained: _error.print_error('activity set to already-current activity') return if self._next_activity is not None: raise Exception('Activity switch already in progress (to ' + str(self._next_activity) + ')') self._in_set_activity = True prev_activity = self._activity_retained if prev_activity is not None: with _ba.Context(prev_activity): gprev = sharedobj('globals') else: gprev = None with _ba.Context(activity): # Now that it's going to be front and center, # set some global values based on what the activity wants. glb = sharedobj('globals') glb.use_fixed_vr_overlay = activity.use_fixed_vr_overlay glb.allow_kick_idle_players = activity.allow_kick_idle_players if activity.inherits_slow_motion and gprev is not None: glb.slow_motion = gprev.slow_motion else: glb.slow_motion = activity.slow_motion if activity.inherits_music and gprev is not None: glb.music_continuous = True # Prevent restarting same music. glb.music = gprev.music glb.music_count += 1 if activity.inherits_camera_vr_offset and gprev is not None: glb.vr_camera_offset = gprev.vr_camera_offset if activity.inherits_vr_overlay_center and gprev is not None: glb.vr_overlay_center = gprev.vr_overlay_center glb.vr_overlay_center_enabled = gprev.vr_overlay_center_enabled # If they want to inherit tint from the previous activity. if activity.inherits_tint and gprev is not None: glb.tint = gprev.tint glb.vignette_outer = gprev.vignette_outer glb.vignette_inner = gprev.vignette_inner # Let the activity do its thing. activity.start_transition_in() self._next_activity = activity # If we have a current activity, tell it it's transitioning out; # the next one will become current once this one dies. if prev_activity is not None: # pylint: disable=protected-access prev_activity._transitioning_out = True # pylint: enable=protected-access # Activity will be None until the next one begins. with _ba.Context(prev_activity): prev_activity.on_transition_out() # Setting this to None should free up the old activity to die, # which will call begin_next_activity. # We can still access our old activity through # self._activity_weak() to keep it up to date on player # joins/departures/etc until it dies. self._activity_retained = None # There's no existing activity; lets just go ahead with the begin call. else: self.begin_next_activity() # Tell the C layer that this new activity is now 'foregrounded'. # This means that its globals node controls global stuff and stuff # like console operations, keyboard shortcuts, etc will run in it. # pylint: disable=protected-access # noinspection PyProtectedMember activity._activity_data.make_foreground() # pylint: enable=protected-access # We want to call _destroy() for the previous activity once it should # tear itself down, clear out any self-refs, etc. If the new activity # has a transition-time, set it up to be called after that passes; # otherwise call it immediately. After this call the activity should # have no refs left to it and should die (which will trigger the next # activity to run). if prev_activity is not None: if activity.transition_time > 0.0: # FIXME: We should tweak the activity to not allow # node-creation/etc when we call _destroy (or after). with _ba.Context('ui'): # pylint: disable=protected-access # noinspection PyProtectedMember _ba.timer(activity.transition_time, prev_activity._destroy, timetype=TimeType.REAL) # Just run immediately. else: # noinspection PyProtectedMember prev_activity._destroy() # pylint: disable=protected-access self._in_set_activity = False
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 config_server(config_file: str = None) -> None: """Run the game in server mode with the provided server config file.""" from ba._enums import TimeType app = _ba.app # Read and store the provided server config and then delete the file it # came from. if config_file is not None: with open(config_file) as infile: app.server_config = json.loads(infile.read()) os.remove(config_file) else: app.server_config = {} # Make note if they want us to import a playlist; # we'll need to do that first if so. playlist_code = app.server_config.get('playlist_code') if playlist_code is not None: app.server_playlist_fetch = { 'sent_request': False, 'got_response': False, 'playlist_code': str(playlist_code) } # Apply config stuff that can take effect immediately (party name, etc). _config_server() # Launch the server only the first time through; # after that it will be self-sustaining. if not app.launched_server: # Now sit around until we're signed in and then kick off the server. with _ba.Context('ui'): def do_it() -> None: if _ba.get_account_state() == 'signed_in': can_launch = False # If we're trying to fetch a playlist, we do that first. if app.server_playlist_fetch is not None: # Send request if we haven't. if not app.server_playlist_fetch['sent_request']: def on_playlist_fetch_response( result: Optional[Dict[str, Any]]) -> None: if result is None: print('Error fetching playlist; aborting.') sys.exit(-1) # Once we get here we simply modify our # config to use this playlist. type_name = ('teams' if result['playlistType'] == 'Team Tournament' else 'ffa' if result['playlistType'] == 'Free-for-All' else '??') print(('Playlist \'' + result['playlistName'] + '\' (' + type_name + ') downloaded; running...')) assert app.server_playlist_fetch is not None app.server_playlist_fetch['got_response'] = ( True) app.server_config['session_type'] = type_name app.server_config['playlist_name'] = ( result['playlistName']) print(('Requesting shared-playlist ' + str( app.server_playlist_fetch['playlist_code']) + '...')) app.server_playlist_fetch['sent_request'] = True _ba.add_transaction( { 'type': 'IMPORT_PLAYLIST', 'code': app.server_playlist_fetch['playlist_code'], 'overwrite': True }, callback=on_playlist_fetch_response) _ba.run_transactions() # If we got a valid result, forget the fetch ever # existed and move on. if app.server_playlist_fetch['got_response']: app.server_playlist_fetch = None can_launch = True else: can_launch = True if can_launch: app.run_server_wait_timer = None _ba.pushcall(launch_server_session) app.run_server_wait_timer = _ba.Timer(0.25, do_it, timetype=TimeType.REAL, repeat=True) app.launched_server = True
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')