diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 18caaff6..b388fb62 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -1598,6 +1598,7 @@ outpath outputter outval + outvals outvalue ouya overloadsigs diff --git a/CHANGELOG.md b/CHANGELOG.md index 723fde22..1e5b7d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### 1.6.5 (20388) +- Added co-op support to server builds (thanks Dliwk!) ### 1.6.4 (20382) - Some cleanups in the Favorites tab of the gather window. diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py index b19a2e40..4c9190ba 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/assets/src/ba_data/python/ba/_activity.py @@ -112,8 +112,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): # transitions). inherits_tint = False - # Whether players should be allowed to join in the middle of - # activity. + # Whether players should be allowed to join in the middle of this + # activity. Note that Sessions may not allow mid-activity-joins even + # if the activity says its ok. allow_mid_activity_joins: bool = True # If the activity fades or transitions in, it should set the length of diff --git a/assets/src/ba_data/python/ba/_coopsession.py b/assets/src/ba_data/python/ba/_coopsession.py index bb85a4b0..021142a9 100644 --- a/assets/src/ba_data/python/ba/_coopsession.py +++ b/assets/src/ba_data/python/ba/_coopsession.py @@ -166,51 +166,44 @@ class CoopSession(Session): from ba._general import WeakCall super().on_player_leave(sessionplayer) - _ba.timer(2.0, WeakCall(self._check_end_game)) + _ba.timer(2.0, WeakCall(self._handle_empty_activity)) - def _check_end_game(self) -> None: - if not _ba.app.server: - self._end_session_if_empty() + def _handle_empty_activity(self) -> None: + """Handle cases where all players have left the current activity.""" - activity = self.getactivity() - if activity is None: - return # Probably everything is already broken, why do something? - - if [player for player in activity.players if player.is_alive()]: - return - - with _ba.Context(activity): - from ba._gameactivity import GameActivity - - if isinstance(activity, GameActivity): - activity.end_game() - - def _end_session_if_empty(self) -> None: + from ba._gameactivity import GameActivity activity = self.getactivity() if activity is None: return # Hmm what should we do in this case? - # If there's still players in the current activity, we're good. + # If there are still players in the current activity, we're good. if activity.players: return - # If there's *no* players left in the current activity but there *is* - # in the session, restart the activity to pull them into the game - # (or quit if they're just in the lobby). + # If there are *not* players in the current activity but there + # *are* in the session: if not activity.players and self.sessionplayers: - # Special exception for tourney games; don't auto-restart these. - if self.tournament_id is not None: - self.end() - else: - # Don't restart joining activities; this probably means there's - # someone with a chooser up in that case. - if not activity.is_joining_activity: + # 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. lets just end the session. + # 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: - self.end() + if not _ba.app.headless_mode: + self.end() + else: + if isinstance(activity, GameActivity): + with _ba.Context(activity): + activity.end_game() def _on_tournament_restart_menu_press( self, resume_callback: Callable[[], Any]) -> None: @@ -274,9 +267,10 @@ class CoopSession(Session): else: outcome = '' if results is None else results.get('outcome', '') - # If at any point we have no in-game players, quit out of the session - # (this can happen if someone leaves in the tutorial for instance). - if isinstance(activity, TutorialActivity): + # If we're running with a gui and at any point we have no + # in-game players, quit out of the session (this can happen if + # someone leaves in the tutorial for instance). + if not _ba.app.headless_mode: active_players = [p for p in self.sessionplayers if p.in_game] if not active_players: self.end() diff --git a/assets/src/ba_data/python/ba/_level.py b/assets/src/ba_data/python/ba/_level.py index c45f7156..ad764b15 100644 --- a/assets/src/ba_data/python/ba/_level.py +++ b/assets/src/ba_data/python/ba/_level.py @@ -36,6 +36,10 @@ class Level: self._index: Optional[int] = None self._score_version_string: Optional[str] = None + def __repr__(self) -> str: + cls = type(self) + return f"<{cls.__module__}.{cls.__name__} '{self._name}'>" + @property def name(self) -> str: """The unique name for this Level.""" diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py index 0f03d4c5..7f06ddaa 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/assets/src/ba_data/python/ba/_servermode.py @@ -98,8 +98,6 @@ class ServerController: self._playlist_fetch_got_response = False self._playlist_fetch_code = -1 - self._coop_game_name = self._config.coop_game_name - # Now sit around doing any pre-launch prep such as waiting for # account sign-in or fetching playlists; this will kick off the # session once done. @@ -349,11 +347,9 @@ class ServerController: appcfg['Team Tournament Playlist Randomize'] = ( self._config.playlist_shuffle) elif sessiontype is CoopSession: - gamename = self._coop_game_name or 'Default:Onslaught Training' - campaignname, levelname = gamename.split(':') app.coop_session_args = { - 'campaign': campaignname, - 'level': levelname, + 'campaign': self._config.coop_campaign, + 'level': self._config.coop_level, } else: raise RuntimeError(f'Unknown session type {sessiontype}') diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index 618e9d00..c3f0c632 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -206,11 +206,12 @@ class Session: return node def should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool: - """Returned value is used by the Session to determine - whether to allow players to join in the middle of activity. + """Ask ourself if we should allow joins during an Activity. - Activity.allow_mid_activity_joins is also required to allow these - joins.""" + Note that for a join to be allowed, both the Session and Activity + have to be ok with it (via this function and the + Activity.allow_mid_activity_joins property. + """ del activity # Unused. return True diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py index 711ee463..3607ecbd 100644 --- a/assets/src/ba_data/python/bastd/game/runaround.py +++ b/assets/src/ba_data/python/bastd/game/runaround.py @@ -561,7 +561,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): fail_message = None else: score = None - fail_message = 'Reach wave 2 to rank.' + fail_message = ba.Lstr(resource='reachWave2Text') self.end(delay=delay, results={ diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index 36dd41ab..b4e73a88 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -766,6 +766,7 @@ outpath outputter outval + outvals outvalue ouya ovld diff --git a/docs/ba_module.md b/docs/ba_module.md index f1ef308a..4d96634f 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1837,11 +1837,11 @@ is pressed.

should_allow_mid_activity_joins()

should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool

-

Returned value is used by the Session to determine -whether to allow players to join in the middle of activity.

+

Ask ourself if we should allow joins during an Activity.

-

Activity.allow_mid_activity_joins is also required to allow these -joins.

+

Note that for a join to be allowed, both the Session and Activity +have to be ok with it (via this function and the +Activity.allow_mid_activity_joins property.

@@ -5263,11 +5263,11 @@ session.setactivity(foo) and then ba.newnode(

should_allow_mid_activity_joins()

should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool

-

Returned value is used by the Session to determine -whether to allow players to join in the middle of activity.

+

Ask ourself if we should allow joins during an Activity.

-

Activity.allow_mid_activity_joins is also required to allow these -joins.

+

Note that for a join to be allowed, both the Session and Activity +have to be ok with it (via this function and the +Activity.allow_mid_activity_joins property.

diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index 128d5090..0ae3005c 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -55,10 +55,7 @@ class ServerConfig: # This value is ignored if you supply a playlist_code (see below). session_type: str = 'ffa' - # There are unavailable co-op playlists now, so if you want to host a co-op - # game, pass level name here. - coop_game_name: Optional[str] = None - + # Playlist-code for teams or free-for-all mode sessions. # To host your own custom playlists, use the 'share' functionality in the # playlist editor in the regular version of the game. # This will give you a numeric code you can enter here to host that @@ -76,6 +73,15 @@ class ServerConfig: # (teams mode only). auto_balance_teams: bool = True + # The campaign used when in co-op session mode. + # Do print(ba.app.campaigns) to see available campaign names. + coop_campaign: str = 'Easy' + + # The level name within the campaign used in co-op session mode. + # For campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see + # available level names. + coop_level: str = 'Onslaught Training' + # Whether to enable telnet access. # IMPORTANT: This option is no longer available, as it was being used # for exploits. Live access to the running server is still possible through diff --git a/tools/batools/build.py b/tools/batools/build.py index 18b3be97..7c914573 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -742,10 +742,14 @@ def _get_server_config_template_yaml(projroot: str) -> str: continue if line != '' and not line.startswith('#'): - vname, _vtype, veq, vval_raw = line.split() + before_equal_sign, vval_raw = line.split('=', 1) + before_equal_sign = before_equal_sign.strip() + vval_raw = vval_raw.strip() + # vname, _vtype, veq, vval_raw = line.split() + vname, _vtype = before_equal_sign.split() assert vname.endswith(':') vname = vname[:-1] - assert veq == '=' + # assert veq == '=' vval: Any if vval_raw == 'field(default_factory=list)': vval = []