diff --git a/.efrocachemap b/.efrocachemap index 4179fd4f..e1ace103 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4132,16 +4132,16 @@ "assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c", "assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb", "assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe", - "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ca/db/9c7cfd4e4f4a1f7a7adc980bca42", - "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4e/0b/231e38ff29d932df7552050891c5", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/56/bb316ec28ee98ece5c0c3a04b77f", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ef/92/d787c99db6cc85f70b7131ff2c0c", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/97/b9/9c6c3c90f10d319250a9f3d287b3", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f0/2a/60bdf1c4d4e13bdbb5f4df121e3e", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/66/25/79ea606983dc91ac0cd79c1e7da6", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/c6/0ab2cdf222ffcadade37dd3b8462", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3c/65/450a67dab189c0832b6bf28a9e9c", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7e/a9/e1ab6defb8bcf536dff46d0c62b2", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ab/fc/d00336dae2b1c7323b31518b52aa", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ed/98/dbea1af1da83bfa1a3283175b234" + "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/95/a8/318c6db7a9c94989c601f9388211", + "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6c/53/dda3c28a824749358279d01d85a6", + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/cb/5e/04efb608c6a0d3fc80baee7f2c0e", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/91/74/c92748de53d860aa936f969c4699", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b9/a0/202236991664bc72a33affee2911", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b3/1a/7f626564f3659f4cbd00d62cbd5a", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/94/b8/4f2c26af58e4386d58f2de2b8f3f", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8e/f3/40da5e70872c27cb1716a0e7bc10", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a0/a9/9ca7e5a2a62c7198f6dccf29a1e2", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/54/e7/fd4f9d4af81fed229a1fd2d486f1", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/54/5f/90a09221a6cb5ca24cf2a6b0f4e7", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c2/14/82ced0d7340340cd09a73987c82f" } \ No newline at end of file diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index 9df9b873..10e4bbe4 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand. """ # (hash we can use to see if this file is out of date) -# SOURCES_HASH=265401783818737452594582363319036908124 +# SOURCES_HASH=317793613698101603244532998583646381571 # I'm sorry Pylint. I know this file saddens you. Be strong. # pylint: disable=useless-suppression @@ -2310,21 +2310,31 @@ def get_ui_input_device() -> ba.InputDevice: return ba.InputDevice() -def getactivity(doraise: bool = True) -> ba.Activity: - """getactivity(doraise: bool = True) -> ba.Activity +# Show that our return type varies based on "doraise" value: +@overload +def getactivity(doraise: Literal[True] = True) -> ba.Activity: + ... - Returns the current ba.Activity instance. + +@overload +def getactivity(doraise: Literal[False]) -> Optional[ba.Activity]: + ... + + +def getactivity(doraise: bool = True) -> Optional[ba.Activity]: + """getactivity(doraise: bool = True) -> + + Return the current ba.Activity instance. Category: Gameplay Functions Note that this is based on context; thus code run in a timer generated in Activity 'foo' will properly return 'foo' here, even if another Activity has since been created or is transitioning in. - If there is no current Activity an Exception is raised, or if doraise is - False then None is returned instead. + If there is no current Activity, raises a ba.ActivityNotFoundError. + If doraise is False, None will be returned instead in that case. """ - import ba # pylint: disable=cyclic-import - return ba.Activity(settings={}) + return None def getcollidemodel(name: str) -> ba.CollideModel: @@ -2422,8 +2432,19 @@ def getnodes() -> list: return list() -def getsession(doraise: bool = True) -> ba.Session: - """getsession(doraise: bool = True) -> ba.Session +# Show that our return type varies based on "doraise" value: +@overload +def getsession(doraise: Literal[True] = True) -> ba.Session: + ... + + +@overload +def getsession(doraise: Literal[False]) -> Optional[ba.Session]: + ... + + +def getsession(doraise: bool = True) -> Optional[ba.Session]: + """getsession(doraise: bool = True) -> Category: Gameplay Functions @@ -2433,8 +2454,7 @@ def getsession(doraise: bool = True) -> ba.Session: exists, etc. If there is no current Session, an Exception is raised, or if doraise is False then None is returned instead. """ - import ba # pylint: disable=cyclic-import - return ba.Session([]) + return None def getsound(name: str) -> ba.Sound: diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 12225c01..6b79f6e0 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -39,7 +39,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice, open_url, widget) from ba._activity import Activity from ba._actor import Actor -from ba._player import Player, playercast, playercast_o +from ba._player import PlayerInfo, Player, playercast, playercast_o from ba._nodeactor import NodeActor from ba._app import App from ba._coopgame import CoopGameActivity @@ -47,13 +47,12 @@ from ba._coopsession import CoopSession from ba._dependency import (Dependency, DependencyComponent, DependencySet, AssetPackage) from ba._enums import TimeType, Permission, TimeFormat, SpecialChar -from ba._error import (print_exception, print_error, NotFoundError, - PlayerNotFoundError, SessionPlayerNotFoundError, - NodeNotFoundError, ActorNotFoundError, - InputDeviceNotFoundError, WidgetNotFoundError, - ActivityNotFoundError, TeamNotFoundError, - SessionTeamNotFoundError, SessionNotFoundError, - DelegateNotFoundError, DependencyError) +from ba._error import ( + print_exception, print_error, ContextError, NotFoundError, + PlayerNotFoundError, SessionPlayerNotFoundError, NodeNotFoundError, + ActorNotFoundError, InputDeviceNotFoundError, WidgetNotFoundError, + ActivityNotFoundError, TeamNotFoundError, SessionTeamNotFoundError, + SessionNotFoundError, DelegateNotFoundError, DependencyError) from ba._freeforallsession import FreeForAllSession from ba._gameactivity import GameActivity from ba._gameresults import TeamGameResults diff --git a/assets/src/ba_data/python/ba/_achievement.py b/assets/src/ba_data/python/ba/_achievement.py index c024dfbd..44292c28 100644 --- a/assets/src/ba_data/python/ba/_achievement.py +++ b/assets/src/ba_data/python/ba/_achievement.py @@ -373,6 +373,7 @@ class Achievement: # pylint: disable=cyclic-import from ba._lang import Lstr from ba._enums import SpecialChar + from ba._coopsession import CoopSession from bastd.actor.image import Image from bastd.actor.text import Text @@ -404,12 +405,16 @@ class Achievement: hmo = False else: try: - campaign = _ba.getsession().campaign - assert campaign is not None - hmo = (self._hard_mode_only and campaign.name == 'Easy') + session = _ba.getsession() + if isinstance(session, CoopSession): + campaign = session.campaign + assert campaign is not None + hmo = (self._hard_mode_only and campaign.name == 'Easy') + else: + hmo = False except Exception: from ba import _error - _error.print_exception('unable to determine campaign') + _error.print_exception('Error determining campaign') hmo = False objs: List[ba.Actor] @@ -678,7 +683,7 @@ class Achievement: # Just piggy-back onto any current activity # (should we use the session instead?..) - activity: Optional[ba.Activity] = _ba.getactivity(doraise=False) + activity = _ba.getactivity(doraise=False) # If this gets called while this achievement is occupying a slot # already, ignore it. (probably should never happen in real diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py index 241d1d70..fe501643 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/assets/src/ba_data/python/ba/_activity.py @@ -351,7 +351,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): raise TypeError('non-actor passed to add_actor_weak_ref') if (self.has_transitioned_in() and _ba.time() - self._last_prune_dead_actors_time > 10.0): - print_error('it looks like nodes/actors are ' + print_error('It looks like nodes/actors are ' 'not being pruned in your activity;' ' did you call Activity.on_transition_in()' ' from your subclass?; ' + str(self) + ' (loc. b)') diff --git a/assets/src/ba_data/python/ba/_apputils.py b/assets/src/ba_data/python/ba/_apputils.py index 1c8b1da5..cf0f0137 100644 --- a/assets/src/ba_data/python/ba/_apputils.py +++ b/assets/src/ba_data/python/ba/_apputils.py @@ -73,7 +73,6 @@ def suppress_debug_reports() -> None: This should be called in devel/debug situations to avoid spamming the master server with spurious logs. """ - # _ba.screenmessage("Suppressing debug reports.", color=(1, 0, 0)) _ba.app.suppress_debug_reports = True @@ -189,21 +188,23 @@ def print_live_object_warnings(when: Any, """Print warnings for remaining objects in the current context.""" # pylint: disable=cyclic-import import gc - from ba import _session as bs_session - from ba import _actor as bs_actor - from ba import _activity as bs_activity + from ba._session import Session + from ba._actor import Actor + from ba._activity import Activity sessions: List[ba.Session] = [] activities: List[ba.Activity] = [] - actors = [] + actors: List[ba.Actor] = [] + + # Once we come across leaked stuff, printing again is probably + # redundant. if _ba.app.printed_live_object_warning: - # print 'skipping live obj check due to previous found live object(s)' return for obj in gc.get_objects(): - if isinstance(obj, bs_actor.Actor): + if isinstance(obj, Actor): actors.append(obj) - elif isinstance(obj, bs_session.Session): + elif isinstance(obj, Session): sessions.append(obj) - elif isinstance(obj, bs_activity.Activity): + elif isinstance(obj, Activity): activities.append(obj) # Complain about any remaining sessions. @@ -211,66 +212,19 @@ def print_live_object_warnings(when: Any, if session is ignore_session: continue _ba.app.printed_live_object_warning = True - print('ERROR: Session found', when, ':', session) - # refs = list(gc.get_referrers(session)) - # i = 1 - # for ref in refs: - # if type(ref) is types.FrameType: continue - # print ' ref', i, ':', ref - # i += 1 - # if type(ref) is list or type(ref) is tuple or type(ref) is dict: - # refs2 = list(gc.get_referrers(ref)) - # j = 1 - # for ref2 in refs2: - # if type(ref2) is types.FrameType: continue - # print ' ref\'s ref', j, ':', ref2 - # j += 1 + print(f'ERROR: Session found {when}: {session}') # Complain about any remaining activities. for activity in activities: if activity is ignore_activity: continue _ba.app.printed_live_object_warning = True - print('ERROR: Activity found', when, ':', activity) - # refs = list(gc.get_referrers(activity)) - # i = 1 - # for ref in refs: - # if type(ref) is types.FrameType: continue - # print ' ref', i, ':', ref - # i += 1 - # if type(ref) is list or type(ref) is tuple or type(ref) is dict: - # refs2 = list(gc.get_referrers(ref)) - # j = 1 - # for ref2 in refs2: - # if type(ref2) is types.FrameType: continue - # print ' ref\'s ref', j, ':', ref2 - # j += 1 + print(f'ERROR: Activity found {when}: {activity}') # Complain about any remaining actors. for actor in actors: _ba.app.printed_live_object_warning = True - print('ERROR: Actor found', when, ':', actor) - # if isinstance(actor, bs_actor.Actor): - # try: - # if actor.node: - # print(' - contains node:', - # actor.node.getnodetype(), ';', - # actor.node.get_name()) - # except Exception as exc: - # print(' - exception checking actor node:', exc) - # refs = list(gc.get_referrers(actor)) - # i = 1 - # for ref in refs: - # if type(ref) is types.FrameType: continue - # print ' ref', i, ':', ref - # i += 1 - # if type(ref) is list or type(ref) is tuple or type(ref) is dict: - # refs2 = list(gc.get_referrers(ref)) - # j = 1 - # for ref2 in refs2: - # if type(ref2) is types.FrameType: continue - # print ' ref\'s ref', j, ':', ref2 - # j += 1 + print(f'ERROR: Actor found {when}: {actor}') def print_corrupt_file_error() -> None: diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py index 89421b82..ee60ea96 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/assets/src/ba_data/python/ba/_coopgame.py @@ -25,6 +25,7 @@ from typing import TYPE_CHECKING, TypeVar import _ba from ba._gameactivity import GameActivity +from ba._general import WeakCall if TYPE_CHECKING: from typing import Type, Dict, Any, Set, List, Sequence, Optional @@ -41,10 +42,13 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): Category: Gameplay Classes """ + # We can assume our session is a CoopSession. + session: ba.CoopSession + @classmethod def supports_session_type(cls, sessiontype: Type[ba.Session]) -> bool: - from ba import _coopsession - return issubclass(sessiontype, _coopsession.CoopSession) + from ba._coopsession import CoopSession + return issubclass(sessiontype, CoopSession) def __init__(self, settings: Dict[str, Any]): super().__init__(settings) @@ -57,16 +61,14 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): self._warn_beeps_sound = _ba.getsound('warnBeeps') def on_begin(self) -> None: - from ba import _general super().on_begin() # Show achievements remaining. if not _ba.app.kiosk_mode: - _ba.timer(3.8, - _general.WeakCall(self._show_remaining_achievements)) + _ba.timer(3.8, WeakCall(self._show_remaining_achievements)) # Preload achievement images in case we get some. - _ba.timer(2.0, _general.WeakCall(self._preload_achievements)) + _ba.timer(2.0, WeakCall(self._preload_achievements)) # Let's ask the server for a 'time-to-beat' value. levelname = self._get_coop_level_name() @@ -76,7 +78,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): self.settings_raw['name']).get_score_version_string().replace( ' ', '_')) _ba.get_scores_to_beat(levelname, config_str, - _general.WeakCall(self._on_got_scores_to_beat)) + WeakCall(self._on_got_scores_to_beat)) def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None: pass @@ -153,18 +155,18 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def _show_remaining_achievements(self) -> None: # pylint: disable=cyclic-import - from ba import _achievement - from ba import _lang + from ba._achievement import get_achievements_for_coop_level + from ba._lang import Lstr from bastd.actor.text import Text ts_h_offs = 30 v_offs = -200 achievements = [ - a for a in _achievement.get_achievements_for_coop_level( + a for a in get_achievements_for_coop_level( self._get_coop_level_name()) if not a.complete ] vrmode = _ba.app.vr_mode if achievements: - Text(_lang.Lstr(resource='achievementsRemainingText'), + Text(Lstr(resource='achievementsRemainingText'), host_only=True, position=(ts_h_offs - 10 + 40, v_offs - 10), transition=Text.Transition.FADE_IN, @@ -208,12 +210,12 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): Returns True if a banner will be shown; False otherwise """ - from ba import _achievement + from ba._achievement import get_achievement if achievement_name in self._achievements_awarded: return - ach = _achievement.get_achievement(achievement_name) + ach = get_achievement(achievement_name) # If we're in the easy campaign and this achievement is hard-mode-only, # ignore it. @@ -223,8 +225,8 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): if ach.hard_mode_only and campaign.name == 'Easy': return except Exception: - from ba import _error - _error.print_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 @@ -261,10 +263,9 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]): def setup_low_life_warning_sound(self) -> None: """Set up a beeping noise to play when any players are near death.""" - from ba import _general self._life_warning_beep = None self._life_warning_beep_timer = _ba.Timer( - 1.0, _general.WeakCall(self._update_life_warning), repeat=True) + 1.0, WeakCall(self._update_life_warning), repeat=True) def _update_life_warning(self) -> None: # Beep continuously if anyone is close to death. diff --git a/assets/src/ba_data/python/ba/_coopsession.py b/assets/src/ba_data/python/ba/_coopsession.py index be5f5450..9fdd94fa 100644 --- a/assets/src/ba_data/python/ba/_coopsession.py +++ b/assets/src/ba_data/python/ba/_coopsession.py @@ -42,11 +42,21 @@ class CoopSession(Session): These generally consist of 1-4 players against the computer and include functionality such as high score lists. + + Attrs: + + campaign + The ba.Campaign instance this Session represents, or None if + there is no associated Campaign. """ use_teams = True use_team_colors = False allow_mid_activity_joins = False + # Note: even though these are instance vars, we annotate them at the + # class level so that docs generation can access their types. + campaign: Optional[ba.Campaign] + def __init__(self) -> None: """Instantiate a co-op mode session.""" # pylint: disable=cyclic-import @@ -77,16 +87,11 @@ class CoopSession(Session): max_players=max_players) # Tournament-ID if we correspond to a co-op tournament (otherwise None) - self.tournament_id = (app.coop_session_args['tournament_id'] - if 'tournament_id' in app.coop_session_args else - None) + self.tournament_id: Optional[str] = ( + app.coop_session_args.get('tournament_id')) - # FIXME: Could be nice to pass this in as actual args. - self.campaign_state = { - 'campaign': (app.coop_session_args['campaign']), - 'level': app.coop_session_args['level'] - } - self.campaign = get_campaign(self.campaign_state['campaign']) + self.campaign = get_campaign(app.coop_session_args['campaign']) + self.campaign_level_name: str = app.coop_session_args['level'] self._ran_tutorial_activity = False self._tutorial_activity: Optional[ba.Activity] = None @@ -96,7 +101,7 @@ class CoopSession(Session): self.set_activity(_ba.new_activity(CoopJoinActivity)) self._next_game_instance: Optional[ba.GameActivity] = None - self._next_game_name: Optional[str] = None + self._next_game_level_name: Optional[str] = None self._update_on_deck_game_instances() def get_current_game_instance(self) -> ba.GameActivity: @@ -107,12 +112,11 @@ class CoopSession(Session): # pylint: disable=cyclic-import from ba._gameactivity import GameActivity - # Instantiates levels we might be running soon - # so they have time to load. + # Instantiate levels we may be running soon to let them load in the bg. # Build an instance for the current level. assert self.campaign is not None - level = self.campaign.get_level(self.campaign_state['level']) + level = self.campaign.get_level(self.campaign_level_name) gametype = level.gametype settings = level.get_settings() @@ -128,7 +132,7 @@ class CoopSession(Session): # Find the next level and build an instance for it too. levels = self.campaign.get_levels() - level = self.campaign.get_level(self.campaign_state['level']) + level = self.campaign.get_level(self.campaign_level_name) nextlevel: Optional[ba.Level] if level.index < len(levels) - 1: @@ -149,15 +153,15 @@ class CoopSession(Session): newactivity = _ba.new_activity(gametype, settings) assert isinstance(newactivity, GameActivity) self._next_game_instance = newactivity - self._next_game_name = nextlevel.name + self._next_game_level_name = nextlevel.name else: self._next_game_instance = None - self._next_game_name = None + self._next_game_level_name = None # Special case: # If our current level is 'onslaught training', instantiate # our tutorial so its ready to go. (if we haven't run it yet). - if (self.campaign_state['level'] == 'Onslaught Training' + if (self.campaign_level_name == 'Onslaught Training' and self._tutorial_activity is None and not self._ran_tutorial_activity): from bastd.tutorial import TutorialActivity @@ -248,6 +252,7 @@ class CoopSession(Session): from ba._coopgame import CoopGameActivity from ba._gameresults import TeamGameResults from ba._score import ScoreType + from ba._player import PlayerInfo from bastd.tutorial import TutorialActivity from bastd.activity.coopscore import CoopScoreScreen @@ -278,9 +283,9 @@ class CoopSession(Session): if outcome == 'next_level': if self._next_game_instance is None: - raise Exception() - assert self._next_game_name is not None - self.campaign_state['level'] = self._next_game_name + raise RuntimeError() + assert self._next_game_level_name is not None + self.campaign_level_name = self._next_game_level_name next_game = self._next_game_instance else: next_game = self._current_game_instance @@ -289,10 +294,10 @@ class CoopSession(Session): # and will be going into onslaught-training, show the # tutorial first. if (isinstance(activity, JoinActivity) - and self.campaign_state['level'] == 'Onslaught Training' + and self.campaign_level_name == 'Onslaught Training' and not app.kiosk_mode): if self._tutorial_activity is None: - raise RuntimeError('tutorial not preloaded properly') + raise RuntimeError('Tutorial not preloaded properly.') self.set_activity(self._tutorial_activity) self._tutorial_activity = None self._ran_tutorial_activity = True @@ -336,6 +341,8 @@ class CoopSession(Session): self.set_activity(_ba.new_activity(TransitionActivity)) else: + player_info: List[ba.PlayerInfo] + # Generic team games. if isinstance(results, TeamGameResults): player_info = results.get_player_info() @@ -364,8 +371,7 @@ class CoopSession(Session): # Old coop-game-specific results; should migrate away from these. else: - player_info = (results['player_info'] - if 'player_info' in results else None) + player_info = results.get('player_info') score = results['score'] if 'score' in results else None fail_message = (results['fail_message'] if 'fail_message' in results else None) @@ -376,6 +382,11 @@ class CoopSession(Session): assert activity_score_type is not None score_type = activity_score_type + # Validate types. + if player_info is not None: + assert isinstance(player_info, list) + assert (isinstance(i, PlayerInfo) for i in player_info) + # Looks like we were in a round - check the outcome and # go from there. if outcome == 'restart': @@ -393,7 +404,7 @@ class CoopSession(Session): 'score_type': score_type, 'outcome': outcome, 'campaign': self.campaign, - 'level': self.campaign_state['level'] + 'level': self.campaign_level_name })) # No matter what, get the next 2 levels ready to go. diff --git a/assets/src/ba_data/python/ba/_error.py b/assets/src/ba_data/python/ba/_error.py index afb36569..bf1db183 100644 --- a/assets/src/ba_data/python/ba/_error.py +++ b/assets/src/ba_data/python/ba/_error.py @@ -49,6 +49,16 @@ class DependencyError(Exception): return self._deps +class ContextError(Exception): + """Exception raised when a call is made in an invalid context. + + category: Exception Classes + + Examples of this include calling UI functions within an Activity context + or calling scene manipulation functions outside of a game context. + """ + + class NotFoundError(Exception): """Exception raised when a referenced object does not exist. diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index 9d0de5ee..dad009cd 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -30,6 +30,7 @@ from ba._activity import Activity from ba._score import ScoreInfo from ba._lang import Lstr from ba._messages import PlayerDiedMessage +from ba._error import NotFoundError, print_error, print_exception import _ba if TYPE_CHECKING: @@ -273,15 +274,18 @@ class GameActivity(Activity[PlayerType, TeamType]): # Set some defaults. self.allow_pausing = True self.allow_kick_idle_players = True - self._spawn_sound = _ba.getsound('spawn') # Whether to show points for kills. - self._show_kill_points = True + self.show_kill_points = True # If not None, the music type that should play in on_transition_in() # (unless overridden by the map). self.default_music: Optional[ba.MusicType] = None + # Holds some flattened info about the player set at the point + # when on_begin() is called. + self.initial_player_info: Optional[List[ba.PlayerInfo]] = None + # Go ahead and get our map loading. map_name: str if 'map' in settings: @@ -298,13 +302,13 @@ class GameActivity(Activity[PlayerType, TeamType]): _ba.screenmessage(Lstr(resource='noValidMapsErrorText')) raise Exception('No valid maps') map_name = valid_maps[random.randrange(len(valid_maps))] + self._spawn_sound = _ba.getsound('spawn') self._map_type = _map.get_map_class(map_name) self._map_type.preload() self._map: Optional[ba.Map] = None self._powerup_drop_timer: Optional[ba.Timer] = None self._tnt_spawners: Optional[Dict[int, TNTSpawner]] = None self._tnt_drop_timer: Optional[ba.Timer] = None - self.initial_player_info: Optional[List[Dict[str, Any]]] = None self._game_scoreboard_name_text: Optional[ba.Actor] = None self._game_scoreboard_description_text: Optional[ba.Actor] = None self._standard_time_limit_time: Optional[int] = None @@ -333,7 +337,6 @@ class GameActivity(Activity[PlayerType, TeamType]): Raises a ba.NotFoundError if the map does not currently exist. """ if self._map is None: - from ba._error import NotFoundError raise NotFoundError return self._map @@ -355,10 +358,9 @@ class GameActivity(Activity[PlayerType, TeamType]): campaign = self.session.campaign assert campaign is not None return campaign.get_level( - self.session.campaign_state['level']).displayname + self.session.campaign_level_name).displayname except Exception: - from ba import _error - _error.print_error('error getting campaign level name') + print_error('error getting campaign level name') return self.get_instance_display_string() def get_instance_description(self) -> Union[str, Sequence]: @@ -415,19 +417,15 @@ class GameActivity(Activity[PlayerType, TeamType]): return '' def on_transition_in(self) -> None: - super().on_transition_in() # Make our map. self._map = self._map_type() - music = self.default_music - - # give our map a chance to override the music - # (for happy-thoughts and other such themed maps) - override_music = self._map_type.get_music_type() - if override_music is not None: - music = override_music + # Give our map a chance to override the music + # (for happy-thoughts and other such themed maps). + map_music = self._map_type.get_music_type() + music = map_music if map_music is not None else self.default_music if music is not None: from ba import _music @@ -470,9 +468,9 @@ class GameActivity(Activity[PlayerType, TeamType]): 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 import continues - from ba import _gameutils - from ba import _general + from bastd.ui.continues import ContinuesWindow + from ba._gameutils import sharedobj + from ba._general import WeakCall from ba._coopsession import CoopSession from ba._enums import TimeType @@ -490,7 +488,7 @@ class GameActivity(Activity[PlayerType, TeamType]): if isinstance(session, CoopSession): assert session.campaign is not None if session.campaign.sequential: - gnode = _gameutils.sharedobj('globals') + gnode = sharedobj('globals') # Only attempt this if we're not currently paused # and there appears to be no UI. @@ -501,19 +499,18 @@ class GameActivity(Activity[PlayerType, TeamType]): with _ba.Context('ui'): _ba.timer( 0.5, - lambda: continues.ContinuesWindow( + lambda: ContinuesWindow( self, self._continue_cost, - continue_call=_general.WeakCall( + continue_call=WeakCall( self._continue_choice, True), - cancel_call=_general.WeakCall( + cancel_call=WeakCall( self._continue_choice, False)), timetype=TimeType.REAL) return except Exception: - from ba import _error - _error.print_exception('error continuing game') + print_exception('error continuing game') self.end_game() @@ -525,8 +522,8 @@ class GameActivity(Activity[PlayerType, TeamType]): from ba._freeforallsession import FreeForAllSession from ba._coopsession import CoopSession session = self.session - campaign = session.campaign if isinstance(session, CoopSession): + campaign = session.campaign assert campaign is not None _ba.set_analytics_screen( 'Coop Game: ' + campaign.name + ' ' + @@ -576,13 +573,13 @@ class GameActivity(Activity[PlayerType, TeamType]): def on_begin(self) -> None: from ba._general import WeakCall + from ba._player import PlayerInfo super().on_begin() try: self._game_begin_analytics() except Exception: - from ba import _error - _error.print_exception('error in game-begin-analytics') + print_exception('error in game-begin-analytics') # We don't do this in on_transition_in because it may depend on # players/teams which aren't available until now. @@ -591,26 +588,27 @@ class GameActivity(Activity[PlayerType, TeamType]): _ba.timer(2.5, WeakCall(self._show_tip)) # Store some basic info about players present at start time. - self.initial_player_info = [{ - 'name': p.get_name(full=True), - 'character': p.character - } for p in self.players] + self.initial_player_info = [ + PlayerInfo(name=p.get_name(full=True), character=p.character) + for p in self.players + ] # Sort this by name so high score lists/etc will be consistent # regardless of player join order. - self.initial_player_info.sort(key=lambda x: x['name']) + self.initial_player_info.sort(key=lambda x: x.name) # If this is a tournament, query info about it such as how much # time is left. tournament_id = self.session.tournament_id if tournament_id is not None: - _ba.tournament_query(args={ - 'tournamentIDs': [tournament_id], - 'source': 'in-game time remaining query' - }, - callback=WeakCall( - self._on_tournament_query_response)) + _ba.tournament_query( + args={ + 'tournamentIDs': [tournament_id], + 'source': 'in-game time remaining query' + }, + callback=WeakCall(self._on_tournament_query_response), + ) def _on_tournament_query_response(self, data: Optional[Dict[str, Any]]) -> None: @@ -670,7 +668,7 @@ class GameActivity(Activity[PlayerType, TeamType]): kill=True, victim_player=player, importance=importance, - showpoints=self._show_kill_points) + showpoints=self.show_kill_points) def show_scoreboard_info(self) -> None: """Create the game info display. @@ -916,19 +914,19 @@ class GameActivity(Activity[PlayerType, TeamType]): force: bool = False) -> None: from ba._gameresults import TeamGameResults - # if results is a standard team-game-results, associate it with us - # so it can grab our score prefs + # If results is a standard team-game-results, associate it with us + # so it can grab our score prefs. if isinstance(results, TeamGameResults): results.set_game(self) - # if we had a standard time-limit that had not expired, stop it so - # it doesnt tick annoyingly + # If we had a standard time-limit that had not expired, stop it so + # it doesnt tick annoyingly. if (self._standard_time_limit_time is not None and self._standard_time_limit_time > 0): self._standard_time_limit_timer = None self._standard_time_limit_text = None - # ditto with tournament time limits + # Ditto with tournament time limits. if (self._tournament_time_limit is not None and self._tournament_time_limit > 0): self._tournament_time_limit_timer = None diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/assets/src/ba_data/python/ba/_gameresults.py index a4997e37..ff185d56 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/assets/src/ba_data/python/ba/_gameresults.py @@ -57,7 +57,7 @@ class TeamGameResults: self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam], Optional[int]]] = {} self._teams: Optional[List[ReferenceType[ba.SessionTeam]]] = None - self._player_info: Optional[List[Dict[str, Any]]] = None + self._player_info: Optional[List[ba.PlayerInfo]] = None self._lower_is_better: Optional[bool] = None self._score_label: Optional[str] = None self._none_is_winner: Optional[bool] = None @@ -141,7 +141,7 @@ class TeamGameResults: return Lstr(value=str(score[1])) return Lstr(value='-') - def get_player_info(self) -> List[Dict[str, Any]]: + def get_player_info(self) -> List[ba.PlayerInfo]: """Get info about the players represented by the results.""" if not self._game_set: raise RuntimeError("Can't get player-info until game is set.") diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/assets/src/ba_data/python/ba/_gameutils.py index e6cb6adf..491f4140 100644 --- a/assets/src/ba_data/python/ba/_gameutils.py +++ b/assets/src/ba_data/python/ba/_gameutils.py @@ -97,7 +97,7 @@ def sharedobj(name: str) -> Any: # We store these on the current context; whether its an activity or # session. - activity: Optional[ba.Activity] = _ba.getactivity(doraise=False) + activity = _ba.getactivity(doraise=False) if activity is not None: # Grab shared-objs dict. @@ -105,8 +105,8 @@ def sharedobj(name: str) -> Any: # Grab item out of it. try: - return sharedobjs[name] - except Exception: + return sharedobjs[name] # (pylint bug?) pylint: disable=E1136 + except KeyError: pass obj: Any @@ -141,7 +141,7 @@ def sharedobj(name: str) -> Any: "unrecognized shared object (activity context): '" + name + "'") else: - session: Optional[ba.Session] = _ba.getsession(doraise=False) + session = _ba.getsession(doraise=False) if session is not None: # Grab shared-objs dict (creating if necessary). diff --git a/assets/src/ba_data/python/ba/_netutils.py b/assets/src/ba_data/python/ba/_netutils.py index d7a7b371..dc0180c4 100644 --- a/assets/src/ba_data/python/ba/_netutils.py +++ b/assets/src/ba_data/python/ba/_netutils.py @@ -83,7 +83,7 @@ class ServerCallThread(threading.Thread): self._context = _ba.Context('current') # Save and restore the context we were created from. - activity: Optional[ba.Activity] = _ba.getactivity(doraise=False) + activity = _ba.getactivity(doraise=False) self._activity = weakref.ref( activity) if activity is not None else None diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py index bc5c44bb..d4f6a7bf 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/assets/src/ba_data/python/ba/_player.py @@ -22,6 +22,7 @@ from __future__ import annotations +from dataclasses import dataclass from typing import TYPE_CHECKING, TypeVar, Generic import _ba @@ -35,6 +36,16 @@ PlayerType = TypeVar('PlayerType', bound='ba.Player') TeamType = TypeVar('TeamType', bound='ba.Team') +@dataclass +class PlayerInfo: + """Holds basic info about a player. + + Category: Gameplay Classes + """ + name: str + character: str + + class Player(Generic[TeamType]): """A player in a specific ba.Activity. diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index c84e42ea..30b4f8b7 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -70,10 +70,6 @@ class Session: Be aware this value may be None if a Session does not allow any such selection. - campaign - The ba.Campaign instance this Session represents, or None if - there is no associated Campaign. - use_teams Whether this session groups players into an explicit set of teams. If this is off, a unique team is generated for each @@ -95,7 +91,6 @@ class Session: # Note: even though these are instance vars, we annotate them at the # class level so that docs generation can access their types. - campaign: Optional[ba.Campaign] lobby: ba.Lobby max_players: int min_players: int @@ -161,30 +156,28 @@ class Session: # print('Would set host-session asset-reqs to:', # required_asset_packages) - # Stuff in this section should be removed from this class if possible. + # Init our C++ layer data. self._sessiondata = _ba.register_session(self) + + # Stuff in this section should be removed from this class if possible. self.tournament_id: Optional[str] = None self.sharedobjs: Dict[str, Any] = {} self.have_shown_controls_help_overlay = False - self.campaign = None - self.campaign_state: Dict[str, str] = {} self.teams = [] self.players = [] + self.min_players = min_players + self.max_players = max_players + self._in_set_activity = False 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 self._activity_weak = empty_weakref(Activity) - 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 static teams if we're using them. if self.use_teams: diff --git a/assets/src/ba_data/python/bastd/activity/coopjoin.py b/assets/src/ba_data/python/bastd/activity/coopjoin.py index 048dfa68..78addd43 100644 --- a/assets/src/ba_data/python/bastd/activity/coopjoin.py +++ b/assets/src/ba_data/python/bastd/activity/coopjoin.py @@ -35,17 +35,21 @@ if TYPE_CHECKING: class CoopJoinActivity(JoinActivity): """Join-screen for co-op mode.""" + # We can assume our session is a CoopSession. + session: ba.CoopSession + def __init__(self, settings: Dict[str, Any]): super().__init__(settings) - session = ba.getsession() + session = self.session + assert isinstance(session, ba.CoopSession) # Let's show a list of scores-to-beat for 1 player at least. assert session.campaign is not None level_name_full = (session.campaign.name + ':' + - session.campaign_state['level']) - config_str = ( - '1p' + session.campaign.get_level(session.campaign_state['level']). - get_score_version_string().replace(' ', '_')) + session.campaign_level_name) + config_str = ('1p' + session.campaign.get_level( + session.campaign_level_name).get_score_version_string().replace( + ' ', '_')) _ba.get_scores_to_beat(level_name_full, config_str, ba.WeakCall(self._on_got_scores_to_beat)) @@ -53,9 +57,10 @@ class CoopJoinActivity(JoinActivity): from bastd.actor.controlsguide import ControlsGuide from bastd.actor.text import Text super().on_transition_in() + assert isinstance(self.session, ba.CoopSession) assert self.session.campaign Text(self.session.campaign.get_level( - self.session.campaign_state['level']).displayname, + self.session.campaign_level_name).displayname, scale=1.3, h_attach=Text.HAttach.CENTER, h_align=Text.HAlign.CENTER, @@ -163,8 +168,9 @@ class CoopJoinActivity(JoinActivity): # Now list our remaining achievements for this level. assert self.session.campaign is not None + assert isinstance(self.session, ba.CoopSession) levelname = (self.session.campaign.name + ':' + - self.session.campaign_state['level']) + self.session.campaign_level_name) ts_h_offs = 60 if not ba.app.kiosk_mode: diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index 80dfe6e9..4e1bacdb 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -137,7 +137,10 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): self._tournament_time_remaining_text: Optional[Text] = None self._tournament_time_remaining_text_timer: Optional[ba.Timer] = None - self._player_info = settings['player_info'] + self._player_info: List[ba.PlayerInfo] = settings['player_info'] + assert isinstance(self._player_info, list) + assert (isinstance(i, ba.PlayerInfo) for i in self._player_info) + self._score: Optional[int] = settings['score'] assert isinstance(self._score, (int, type(None))) @@ -633,7 +636,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): ba.pushcall(ba.WeakCall(self._show_fail)) self._name_str = name_str = ', '.join( - [p['name'] for p in self._player_info]) + [p.name for p in self._player_info]) if self._show_friend_scores: self._friends_loading_status = Text( @@ -668,7 +671,10 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if self._score is not None: our_score: Optional[list] = [ self._score, { - 'players': self._player_info + 'players': [{ + 'name': p.name, + 'character': p.character + } for p in self._player_info] } ] our_high_scores.append(our_score) diff --git a/assets/src/ba_data/python/bastd/activity/dualteamscore.py b/assets/src/ba_data/python/bastd/activity/dualteamscore.py index 6fcb0305..eb354366 100644 --- a/assets/src/ba_data/python/bastd/activity/dualteamscore.py +++ b/assets/src/ba_data/python/bastd/activity/dualteamscore.py @@ -37,6 +37,8 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): def __init__(self, settings: Dict[str, Any]): super().__init__(settings=settings) + self._winner: ba.SessionTeam = settings['winner'] + assert isinstance(self._winner, ba.SessionTeam) def on_begin(self) -> None: from ba.deprecated import get_resource @@ -80,7 +82,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity): i * 0.2, shift_time - (i * 0.150 + 0.150))) ba.timer(i * 0.150 + 0.5, ba.Call(ba.playsound, self._score_display_sound_small)) - scored = (team is self.settings_raw['winner']) + scored = (team is self._winner) delay = 0.2 if scored: delay = 1.2 diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/assets/src/ba_data/python/bastd/actor/spazbot.py index 984d209d..303beb37 100644 --- a/assets/src/ba_data/python/bastd/actor/spazbot.py +++ b/assets/src/ba_data/python/bastd/actor/spazbot.py @@ -999,7 +999,7 @@ class SpazBotSet: """Immediately clear out any bots in the set.""" # Don't do this if the activity is shutting down or dead. - activity: Optional[ba.Activity] = ba.getactivity(doraise=False) + activity = ba.getactivity(doraise=False) if activity is None or activity.expired: return diff --git a/docs/ba_module.md b/docs/ba_module.md index 7ad463ee..d1e9401b 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-05-27 for Ballistica version 1.5.0 build 20030

+

last updated on 2020-05-28 for Ballistica version 1.5.0 build 20031

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -26,6 +26,7 @@
  • ba.Material
  • ba.Node
  • ba.Player
  • +
  • ba.PlayerInfo
  • ba.PlayerRecord
  • ba.ScoreInfo
  • ba.Session
  • @@ -185,6 +186,7 @@

    Exception Classes