diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py index ac3bab8d..7c87af63 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/assets/src/ba_data/python/ba/_servermode.py @@ -16,6 +16,7 @@ import _ba from ba._generated.enums import TimeType from ba._freeforallsession import FreeForAllSession from ba._dualteamsession import DualTeamSession +from ba._coopsession import CoopSession if TYPE_CHECKING: from typing import Optional, Dict, Any, Type @@ -97,6 +98,8 @@ 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. @@ -289,6 +292,8 @@ class ServerController: return FreeForAllSession if self._config.session_type == 'teams': return DualTeamSession + if self._config.session_type == 'coop': + return CoopSession raise RuntimeError( f'Invalid session_type: "{self._config.session_type}"') @@ -311,6 +316,8 @@ class ServerController: ptypename = 'Free-for-All' elif sessiontype is DualTeamSession: ptypename = 'Team Tournament' + elif sessiontype is CoopSession: + ptypename = 'Coop' else: raise RuntimeError(f'Unknown session type {sessiontype}') @@ -340,6 +347,12 @@ class ServerController: appcfg['Team Tournament Playlist Selection'] = self._playlist_name appcfg['Team Tournament Playlist Randomize'] = ( self._config.playlist_shuffle) + elif sessiontype is CoopSession: + campaignname, levelname = self._coop_game_name.split(':') + app.coop_session_args = { + 'campaign': campaignname, + 'level': levelname, + } else: raise RuntimeError(f'Unknown session type {sessiontype}') diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index b8678cd0..a0a9d81f 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -118,6 +118,12 @@ 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 + # Stuff for activity skip by pressing button + self._birth_time = ba.time() + self._min_view_time = 5.0 + self._allow_server_transition = False + self._server_transitioning: Optional[bool] = True + self._playerinfos: List[ba.PlayerInfo] = settings['playerinfos'] assert isinstance(self._playerinfos, list) assert (isinstance(i, ba.PlayerInfo) for i in self._playerinfos) @@ -485,6 +491,51 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): if self._store_button_instance is not None: self._store_button_instance.set_position((pos_x + 100, pos_y)) + def _player_press(self) -> None: + # (Only for headless builds). + + print('HERE PLAYER PRESS') + + # If this activity is a good 'end point', ask server-mode just once if + # it wants to do anything special like switch sessions or kill the app. + if (self._allow_server_transition and _ba.app.server is not None + and self._server_transitioning is None): + self._server_transitioning = _ba.app.server.handle_transition() + assert isinstance(self._server_transitioning, bool) + + print('HERE IF PASSED') + + # If server-mode is handling this, don't do anything ourself. + if self._server_transitioning is True: + return + + print('HERE RESTARTING COOP') + # Otherwise restart current level. + print('HERE ui_restart() only for testing; replace after that') + self._ui_restart() + + def _safe_assign(self, player: EmptyPlayer) -> None: + # (Only for headless builds). + + # Just to be extra careful, don't assign if we're transitioning out. + # (though theoretically that should be ok). + if not self.is_transitioning_out() and player: + player.assigninput((ba.InputType.JUMP_PRESS, ba.InputType.PUNCH_PRESS, + ba.InputType.BOMB_PRESS, ba.InputType.PICK_UP_PRESS), + self._player_press) + + def on_player_join(self, player: PlayerType) -> None: + print('HERE ON PLAYER JOIN') + super().on_player_join(player) + + if ba.app.server is not None: + print('HERE INPUT ASSIGNED') + # Host can't press retry button, so anyone can do it instead. + time_till_assign = max( + 0, self._birth_time + self._min_view_time - _ba.time()) + + ba.timer(time_till_assign, ba.WeakCall(self._safe_assign, player)) + def on_begin(self) -> None: # FIXME: Clean this up. # pylint: disable=too-many-statements @@ -582,19 +633,35 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]): color=(0.5, 0.7, 0.5, 1), position=(0, 230)).autoretain() - adisp = _ba.get_account_display_string() - txt = Text(ba.Lstr(resource='waitingForHostText', - subs=[('${HOST}', adisp)]), - maxwidth=300, - transition=Text.Transition.FADE_IN, - transition_delay=8.0, - scale=0.85, - h_align=Text.HAlign.CENTER, - v_align=Text.VAlign.CENTER, - color=(1, 1, 0, 1), - position=(0, -230)).autoretain() - assert txt.node - txt.node.client_only = True + if ba.app.server is None: + # If we're running in normal non-headless build, show this text + # because only host can continue the game. + adisp = _ba.get_account_display_string() + txt = Text(ba.Lstr(resource='waitingForHostText', + subs=[('${HOST}', adisp)]), + maxwidth=300, + transition=Text.Transition.FADE_IN, + transition_delay=8.0, + scale=0.85, + h_align=Text.HAlign.CENTER, + v_align=Text.VAlign.CENTER, + color=(1, 1, 0, 1), + position=(0, -230)).autoretain() + assert txt.node + txt.node.client_only = True + else: + # In headless build, anyone can continue the game. + sval = ba.Lstr(resource='pressAnyButtonPlayAgainText') + Text(sval, + v_attach=Text.VAttach.BOTTOM, + h_align=Text.HAlign.CENTER, + flash=True, + vr_depth=50, + position=(0, 60), + scale=0.8, + color=(0.5, 0.7, 0.5, 0.5), + transition=Text.Transition.IN_BOTTOM_SLOW, + transition_delay=self._min_view_time).autoretain() if self._score is not None: ba.timer(0.35, diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index c4a22464..128d5090 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -51,10 +51,14 @@ class ServerConfig: # exposed but I'll try to add that soon. max_party_size: int = 6 - # Options here are 'ffa' (free-for-all) and 'teams' + # Options here are 'ffa' (free-for-all), 'teams' and 'coop' (cooperative) # 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 + # 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