class Launcher: def __init__(self, game_server_config, ports, incoming_queue, server_handler_queue): gevent.getcurrent().name = 'launcher' self.pending_callbacks = PendingCallbacks(incoming_queue) self.logger = logging.getLogger(__name__) self.ports = ports self.firewall = FirewallClient(ports) self.game_server_config = game_server_config self.incoming_queue = incoming_queue self.server_handler_queue = server_handler_queue self.players = TracingDict() self.game_controller = None self.login_server = None self.active_server = GameServerProcess('gameserver1', self.ports, server_handler_queue) self.pending_server = GameServerProcess('gameserver2', self.ports, server_handler_queue) self.min_next_switch_time = None try: with open(map_rotation_state_path, 'rt') as f: self.controller_context = json.load(f) except IOError: self.controller_context = {} self.last_waiting_for_map_message = None self.last_server_info_message = None self.last_map_info_message = None self.last_team_info_message = None self.last_score_info_message = None self.last_match_time_message = None self.last_server_ready_message = None self.last_match_end_message = None self.address_pair, errormsg = IPAddressPair.detect() if not self.address_pair.external_ip: self.logger.warning('Unable to detect public IP address: %s\n' 'This will cause problems if the login server ' 'or any of your players are not on your LAN.' % errormsg) else: self.logger.info('launcher: detected external IP: %s' % self.address_pair.external_ip) if not self.address_pair.internal_ip: self.logger.warning( 'You appear to be running the game server on a machine ' 'directly connected to the internet. This is will cause ' 'problems if the login server or any of your players ' 'are on your LAN.') else: self.logger.info('launcher: detected internal IP: %s' % self.address_pair.internal_ip) self.message_handlers = { PeerConnectedMessage: self.handle_peer_connected, PeerDisconnectedMessage: self.handle_peer_disconnected, Login2LauncherProtocolVersionMessage: self.handle_login_server_protocol_version_message, Login2LauncherNextMapMessage: self.handle_next_map_message, Login2LauncherSetPlayerLoadoutsMessage: self.handle_set_player_loadouts_message, Login2LauncherRemovePlayerLoadoutsMessage: self.handle_remove_player_loadouts_message, Login2LauncherAddPlayer: self.handle_add_player_message, Login2LauncherRemovePlayer: self.handle_remove_player_message, Login2LauncherPings: self.handle_pings_message, Login2LauncherMapVoteResult: self.handle_map_vote_result, Game2LauncherProtocolVersionMessage: self.handle_game_controller_protocol_version_message, Game2LauncherServerInfoMessage: self.handle_server_info_message, Game2LauncherMapInfoMessage: self.handle_map_info_message, Game2LauncherTeamInfoMessage: self.handle_team_info_message, Game2LauncherScoreInfoMessage: self.handle_score_info_message, Game2LauncherMatchTimeMessage: self.handle_match_time_message, Game2LauncherMatchEndMessage: self.handle_match_end_message, Game2LauncherLoadoutRequest: self.handle_loadout_request_message, GameServerTerminatedMessage: self.handle_game_server_terminated_message, ExecuteCallbackMessage: self.handle_execute_callback_message } def run(self): self.firewall.reset_firewall('whitelist') self.pending_server.start() while True: for message in self.incoming_queue: handler = self.message_handlers[type(message)] handler(message) def get_other_server(self, server): for other_server in ['gameserver1', 'gameserver2']: if other_server != server: return other_server assert False def handle_peer_connected(self, msg): if isinstance(msg.peer, GameController): pass elif isinstance(msg.peer, LoginServer): if self.login_server is not None: raise RuntimeError( 'There should only be a connection to one login server at a time' ) self.login_server = msg.peer msg = Launcher2LoginProtocolVersionMessage( str(versions.launcher2loginserver_protocol_version)) self.login_server.send(msg) msg = Launcher2LoginAddressInfoMessage( str(self.address_pair.external_ip) if self.address_pair.external_ip else '', str(self.address_pair.internal_ip) if self.address_pair.internal_ip else '') self.login_server.send(msg) # Send the latest relevant information that was received # while the login server was not connected if self.last_waiting_for_map_message: self.login_server.send(self.last_waiting_for_map_message) self.last_waiting_for_map_message = None if self.last_server_info_message: self.login_server.send(self.last_server_info_message) self.last_server_info_message = None if self.last_map_info_message: self.login_server.send(self.last_map_info_message) self.last_map_info_message = None if self.last_team_info_message: self.login_server.send(self.last_team_info_message) self.last_team_info_message = None if self.last_score_info_message: self.login_server.send(self.last_score_info_message) self.last_score_info_message = None if self.last_match_time_message: self.login_server.send(self.last_match_time_message) self.last_match_time_message = None if self.last_server_ready_message: self.login_server.send(self.last_server_ready_message) self.last_server_ready_message = None if self.last_match_end_message: self.login_server.send(self.last_match_end_message) self.last_match_end_message = None else: assert False, "Invalid connection message received" def hash_server_password(self, password: str) -> List[int]: hash_constants = [0x55, 0x93, 0x55, 0x58, 0xBA, 0x6f, 0xe9, 0xf9] interspersed_constants = [ 0x7a, 0x1e, 0x9f, 0x47, 0xf9, 0x17, 0xb0, 0x03 ] result = [] for idx, c in enumerate(password.encode('latin1')): pattern_idx = idx % 8 result.extend([(c ^ hash_constants[pattern_idx]), interspersed_constants[pattern_idx]]) return result def handle_peer_disconnected(self, msg): if isinstance(msg.peer, GameController): msg.peer.disconnect() elif isinstance(msg.peer, LoginServer): if self.login_server is None: raise RuntimeError( 'How can a login server disconnect if it\'s not there?') self.login_server.disconnect() self.login_server = None else: assert False, "Invalid disconnection message received" def handle_execute_callback_message(self, msg): callback_id = msg.callback_id self.pending_callbacks.execute(callback_id) def handle_login_server_protocol_version_message(self, msg): # The only time we get a message with the login server's protocol version # is when the version that we sent is incompatible with it. raise IncompatibleVersionError( 'The protocol version that this game server launcher supports (%s) is ' 'incompatible with the version supported by the login server at %s:%d (%s)' % (versions.launcher2loginserver_protocol_version, self.login_server.ip, self.login_server.port, StrictVersion(msg.version))) def freeze_active_server_if_empty(self): if len( self.players ) == 0 and self.active_server.ready and not self.active_server.frozen: self.active_server.freeze() def handle_next_map_message(self, msg): self.logger.info( f'launcher: switching to {self.pending_server.name} on port {self.pending_server.port}' ) if self.active_server.running: self.logger.info(f'launcher: stopping {self.active_server.name}') self.active_server.stop() self.active_server, self.pending_server = self.pending_server, self.active_server self.pending_callbacks.add(self, 5, self.freeze_active_server_if_empty) def handle_set_player_loadouts_message(self, msg): self.logger.info('launcher: loadouts changed for player %d' % msg.unique_id) self.players[msg.unique_id] = msg.loadouts def handle_remove_player_loadouts_message(self, msg): self.logger.info('launcher: loadouts removed for player %d' % msg.unique_id) self.players[msg.unique_id] = None def handle_add_player_message(self, msg): if msg.ip: self.logger.info( 'launcher: login server added player %d with ip %s' % (msg.unique_id, msg.ip)) self.firewall.modify_firewall('whitelist', 'add', msg.unique_id, msg.ip) else: self.logger.info('launcher: login server added local player %d' % msg.unique_id) if len(self.players) == 0 and self.active_server.frozen: self.active_server.unfreeze() self.players[msg.unique_id] = None # If the active server is not ready then we are between match end and the switch to the pending server. # It's ok to just drop this message in that case, because when the players are redirected to the pending # server another add_player message will come. if self.active_server.ready: self.game_controller.send( Launcher2GamePlayerInfo(msg.unique_id, msg.rank_xp, msg.eligible_for_first_win)) def handle_remove_player_message(self, msg): if msg.ip: self.logger.info( 'launcher: login server removed player %d with ip %s' % (msg.unique_id, msg.ip)) self.firewall.modify_firewall('whitelist', 'remove', msg.unique_id, msg.ip) else: self.logger.info('launcher: login server removed local player %d' % msg.unique_id) del (self.players[msg.unique_id]) self.freeze_active_server_if_empty() def handle_pings_message(self, msg): if self.game_controller: self.game_controller.send(Launcher2GamePings(msg.player_pings)) def handle_game_controller_protocol_version_message(self, msg): controller_version = StrictVersion(msg.version) my_version = versions.launcher2controller_protocol_version self.logger.info( 'launcher: received protocol version %s from game controller' % controller_version) if controller_version.version[0] != my_version.version[0]: raise IncompatibleVersionError( 'The protocol version of the game controller DLL (%s) is incompatible ' 'with the version supported by this game server launcher (%s)' % (controller_version, my_version)) self.game_controller = msg.peer msg = Launcher2LoginWaitingForMap() if self.login_server: self.login_server.send(msg) else: self.last_waiting_for_map_message = msg def handle_map_vote_result(self, msg): self.logger.info( f'launcher: received map vote result from login server: map = {msg.map_id}' ) if msg.map_id is not None: self.controller_context['next_map_index'] = msg.map_id msg = Launcher2GameInit(self.controller_context) self.game_controller.send(msg) def handle_server_info_message(self, msg): self.logger.info('launcher: received server info from game controller') msg = Launcher2LoginServerInfoMessage(msg.description, msg.motd, msg.game_setting_mode, msg.password_hash) if self.login_server: self.login_server.send(msg) else: self.last_server_info_message = msg def handle_map_info_message(self, msg): self.logger.info('launcher: received map info from game controller') msg = Launcher2LoginMapInfoMessage(msg.map_id) if self.login_server: self.login_server.send(msg) else: self.last_map_info_message = msg def handle_team_info_message(self, msg): self.logger.info('launcher: received team info from game controller') for player_id, team_id in msg.player_to_team_id.items(): if int(player_id) not in self.players: return msg = Launcher2LoginTeamInfoMessage(msg.player_to_team_id) if self.login_server: self.login_server.send(msg) else: self.last_team_info_message = msg def handle_score_info_message(self, msg): self.logger.info('launcher: received score info from game controller') msg = Launcher2LoginScoreInfoMessage(msg.be_score, msg.ds_score) if self.login_server: self.login_server.send(msg) else: self.last_score_info_message = msg def set_server_ready(self): self.pending_server.set_ready(True) self.logger.info( f'launcher: reporting {self.pending_server.name} as ready') msg = Launcher2LoginServerReadyMessage(self.pending_server.port, self.ports['launcherping']) if self.login_server: self.login_server.send(msg) else: self.last_server_ready_message = msg def handle_match_time_message(self, msg): self.logger.info('launcher: received match time from game controller') msg = Launcher2LoginMatchTimeMessage(msg.seconds_remaining, msg.counting) if self.login_server: self.login_server.send(msg) else: self.last_match_time_message = msg if self.pending_server.running and not self.pending_server.ready: if self.min_next_switch_time: time_left = (self.min_next_switch_time - datetime.datetime.utcnow()).total_seconds() else: time_left = 0 if time_left > 0: self.pending_callbacks.add(self, time_left, self.set_server_ready) else: self.set_server_ready() def handle_match_end_message(self, msg): self.logger.info( 'launcher: received match end from game controller (controller context = %s)' % msg.controller_context) self.game_controller = None self.active_server.set_ready(False) self.controller_context = msg.controller_context with open(map_rotation_state_path, 'wt') as f: json.dump(self.controller_context, f) if 'next_map_index' in self.controller_context: next_map_idx = self.controller_context['next_map_index'] else: next_map_idx = 0 msg_to_login = Launcher2LoginMatchEndMessage(next_map_idx, msg.votable_maps, msg.players_time_played) if self.login_server: self.login_server.send(msg_to_login) else: self.last_match_end_message = msg_to_login self.min_next_switch_time = datetime.datetime.utcnow( ) + datetime.timedelta(seconds=msg.next_map_wait_time) self.pending_server.start() def handle_loadout_request_message(self, msg): self.logger.info( 'launcher: received loadout request from game controller') # Class and loadout keys are strings because they came in as json. # There's not much point in converting all keys in the loadouts # dictionary from strings back to ints if we are just going to # send it out as json again later. player_key = msg.player_unique_id class_key = str(msg.class_id) loadout_key = str(msg.loadout_number) if msg.player_unique_id in self.players: try: loadout = self.players[player_key][class_key][loadout_key] except KeyError: # TODO: This is a temporary solution to a bug in tamods-server that causes an incorrect class to be sent ('1686') # We should figure out what's going on and then remove this code again. self.logger.warning( 'launcher: Incorrect params for loadout of player %d [class = %s, loadout = %s]. Sending empty loadout.' % (msg.player_unique_id, class_key, loadout_key)) loadout = {} else: self.logger.warning( 'launcher: Unable to find player %d\'s loadouts. Sending empty loadout.' % msg.player_unique_id) loadout = {} msg = Launcher2GameLoadoutMessage(msg.player_unique_id, msg.class_id, loadout) self.game_controller.send(msg) def handle_game_server_terminated_message(self, msg): terminated_server = self.active_server if self.active_server.name == msg.server else self.pending_server was_already_stopping = terminated_server.stopping terminated_server.terminated() if was_already_stopping: self.logger.info( f'launcher: {terminated_server.name} process terminated.') else: if self.pending_server.running: self.logger.info( f'launcher: {terminated_server.name} process terminated unexpectedly; ' f'{self.pending_server.name} already starting.') else: self.logger.info( f'launcher: {terminated_server.name} process terminated unexpectedly; ' f'starting {self.pending_server.name} to take over.') self.pending_server.start() msg = Launcher2LoginServerReadyMessage(None, None) if self.login_server: self.login_server.send(msg) else: self.last_server_ready_message = msg
class GameServer(Peer): def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = None self.joinable = False self.players = TracingDict(refsonly=True) self.player_kicking = None self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None self.votable_maps = [] self.map_votes = {} self.next_map_idx = None if self.detected_ip.is_global: req = urllib.request.Request( 'https://tools.keycdn.com/geo.json?host=%s' % self.detected_ip, data=None, headers={ 'User-Agent': 'keycdn-tools:https://github.com/Griffon26/taserver/blob/master/README.md' }) response = urllib.request.urlopen(req, cafile=certifi.where()) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE def __repr__(self): return 'server %d (%s %s:%s/%s)' % ( self.server_id, self.game_setting_mode, self.detected_ip, self.port, self.pingport) def disconnect(self, exception=None): for player in list(self.players.values()): player.set_state(AuthenticatedState) super().disconnect(exception) def set_address_info(self, address_pair): self.address_pair = address_pair self.send_pings() def set_info(self, description: str, motd: str, game_setting_mode: str, password_hash: bytes): self.description = description self.motd = motd self.game_setting_mode = game_setting_mode self.password_hash = password_hash def set_match_time(self, seconds_remaining, counting): self.match_time_counting = counting if counting: self.match_end_time_rel_or_abs = int(time.time() + seconds_remaining) else: self.match_end_time_rel_or_abs = seconds_remaining def set_ready(self, port, pingport): if port is not None: self.be_score = 0 self.ds_score = 0 self.port = port self.pingport = pingport self.joinable = True self.start_time = datetime.datetime.utcnow() for unique_id, player in self.players.items(): b4msg = a00b4().set_server(self).set_player(unique_id) b4msg.findbytype(m042a).set(3) b4msg.content.append(m02ff()) player.send(b4msg) self.send(Login2LauncherNextMapMessage()) else: self.joinable = False def get_time_remaining(self): if self.match_end_time_rel_or_abs is not None: if self.match_time_counting: time_remaining = int(self.match_end_time_rel_or_abs - time.time()) else: time_remaining = self.match_end_time_rel_or_abs else: time_remaining = 0 if time_remaining < 0: time_remaining = 0 return time_remaining def add_player(self, player): assert player.unique_id not in self.players self.players[player.unique_id] = player player.vote = None player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherAddPlayer( player.unique_id, str(player_ip) if player_ip is not None else '', player.player_settings.progression.rank_xp, player.player_settings.progression.is_eligible_for_first_win()) self.send(msg) def remove_player(self, player): assert player.unique_id in self.players del self.players[player.unique_id] player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherRemovePlayer( player.unique_id, str(player_ip) if player_ip is not None else '') self.send(msg) def _send_public_message_from_server(self, text): for player in self.players.values(): msg = a0070().set([ m009e().set(MESSAGE_PUBLIC), m02e6().set(text), m034a().set(player.display_name), m0574(), m02fe().set('taserver'), m06de().set('bot') ]) player.send(msg) def send_all_players(self, data): for player in self.players.values(): player.send(data) def send_all_players_on_team(self, data, team): for player in self.players.values(): if player.team == team: player.send(data) def set_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherSetPlayerLoadoutsMessage( player.unique_id, player.get_current_loadouts().get_data()) self.send(msg) def remove_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherRemovePlayerLoadoutsMessage(player.unique_id) self.send(msg) def start_votekick(self, kicker, kickee): if kickee.unique_id in self.players and self.player_being_kicked is None: # Start a new vote reply = a018c() reply.content = [ m02c4().set(self.match_id), m034a().set(kickee.display_name), m0348().set(kickee.unique_id), m02fc().set(STDMSG_VOTE_BY_X_KICK_PLAYER_X_YES_NO), m0442().set_success(True), m0704().set(kicker.unique_id), m0705().set(kicker.display_name) ] self.send_all_players(reply) for player in self.players.values(): player.vote = None self.player_kicking = kicker self.player_being_kicked = kickee self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s"' % (self, kicker.unique_id, kicker.display_name, kickee.unique_id, kickee.display_name)) self.login_server.pending_callbacks.add(self, 35, self.end_votekick) def end_votekick(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s" %s at timeout with %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % (self, self.player_kicking.unique_id, self.player_kicking.display_name, self.player_being_kicked.unique_id, self.player_being_kicked.display_name, 'passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def check_votes(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) # If enough people vote yes or the vote is unanimous, end the vote immediately. # Otherwise wait for the timeout. if (yes_votes >= 8 and vote_passed) or total_votes == eligible_voters: self.logger.info( '%s: votekick started by %d:"%s" against %d:"%s" %s immediately %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % (self, self.player_kicking.unique_id, self.player_kicking.display_name, self.player_being_kicked.unique_id, self.player_being_kicked.display_name, 'passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def _tally_votes(self): if self.player_being_kicked.player_settings.progression.rank_xp > LEVEL_15_XP: required_majority = 0.66 else: required_majority = 0.5 eligible_voters_votes = { p.address_pair.get_address_seen_from( self.login_server.address_pair): p.vote for p in self.players.values() } votes = [v for v in eligible_voters_votes.values() if v is not None] yes_votes = [v for v in votes if v] vote_passed = len(votes) >= 4 and (len(yes_votes) / len(votes)) > required_majority return len(eligible_voters_votes), len(votes), len( yes_votes), vote_passed def _do_kick(self, votekick_passed): player_to_kick = self.player_being_kicked reply = a018c() reply.content = [ m0348().set(player_to_kick.unique_id), m034a().set(player_to_kick.display_name) ] if votekick_passed: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_HAS_BEEN_KICKED), m0442().set_success(True) ]) else: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_WAS_NOT_VOTED_OUT), m0442().set_success(False) ]) self.send_all_players(reply) if votekick_passed: # TODO: figure out if a real votekick also causes an # inconsistency between the menu you see and the one # you're really in for msg in [a00b0(), a0035().setmainmenu(), a006f()]: player_to_kick.send(msg) player_to_kick.set_state(UnauthenticatedState) ip_to_ban_on_login_server = player_to_kick.address_pair.get_address_seen_from( self.login_server.address_pair) self.firewall.modify_firewall('blacklist', 'add', player_to_kick.unique_id, ip_to_ban_on_login_server) def remove_blacklist_rule(): self.firewall.modify_firewall('blacklist', 'remove', player_to_kick.unique_id, ip_to_ban_on_login_server) self.login_server.pending_callbacks.add(self.login_server, 8 * 3600, remove_blacklist_rule) self.player_kicking = None self.player_being_kicked = None def send_pings(self): player_pings = {} for unique_id, player in self.players.items(): player_pings[unique_id] = player.pings[ self.region] if self.region in player.pings else 999 self.send(Login2LauncherPings(player_pings)) self.login_server.pending_callbacks.add(self, PING_UPDATE_TIME, self.send_pings) def initialize_map_vote(self, next_map_idx, votable_maps): self.votable_maps = votable_maps self.map_votes = {} self.next_map_idx = next_map_idx if self.votable_maps: self.logger.info(f'{self}: initiating map vote') self._send_public_message_from_server( 'Please vote for the next map by typing its number in public chat. Only votes from verified players will count.' ) for idx, map in enumerate(votable_maps): suffix = '<-- next in rotation' if idx == next_map_idx else '' self._send_public_message_from_server( f'{idx}. {map} {suffix}') def inspect_message_for_map_vote(self, player, text): if not player.verified: return try: idx = int(text) except ValueError: return if 0 <= idx < len(self.votable_maps): self.map_votes[player.unique_id] = idx def process_map_votes(self): max_votes = 0 map_with_max_votes = self.next_map_idx if self.votable_maps: votes_per_map = Counter(self.map_votes.values()).most_common() max_voted = [ map_id for map_id, nr_of_votes in votes_per_map if nr_of_votes == votes_per_map[0][1] ] if max_voted: map_with_max_votes = random.choice(max_voted) self.logger.info( f'{self}: map with most votes was {map_with_max_votes}') self._send_public_message_from_server( f'Map vote ended. Next map will be {map_with_max_votes}.') self.send(Login2LauncherMapVoteResult(map_with_max_votes))
class GameServer(Peer): def __init__(self, detected_ip: IPv4Address, ports): super().__init__() self.logger = logging.getLogger(__name__) self.firewall = FirewallClient(ports) self.login_server = None self.server_id = None self.match_id = None self.detected_ip = detected_ip self.address_pair = None self.port = None self.pingport = None self.description = None self.motd = None self.password_hash = None self.region = None self.game_setting_mode = 'ootb' self.joinable = False self.players = TracingDict(refsonly=True) self.player_being_kicked = None self.match_end_time_rel_or_abs = None self.match_time_counting = False self.be_score = 0 self.ds_score = 0 self.map_id = 0 self.start_time = None if self.detected_ip.is_global: response = urllib.request.urlopen( 'http://tools.keycdn.com/geo.json?host=%s' % self.detected_ip) result = response.read() json_result = json.loads(result) continent_code_to_region = { 'NA': REGION_NORTH_AMERICA, 'EU': REGION_EUROPE, 'OC': REGION_OCEANIA_AUSTRALIA } try: self.region = continent_code_to_region[ json_result['data']['geo']['continent_code']] except KeyError: self.region = REGION_EUROPE else: self.region = REGION_EUROPE def __str__(self): return 'GameServer(%d)' % self.server_id def disconnect(self, exception=None): for player in list(self.players.values()): player.set_state(AuthenticatedState) super().disconnect(exception) def set_address_info(self, address_pair): self.address_pair = address_pair self.send_pings() def set_info(self, description: str, motd: str, game_setting_mode: str, password_hash: bytes): self.description = description self.motd = motd self.game_setting_mode = game_setting_mode self.password_hash = password_hash def set_match_time(self, seconds_remaining, counting): self.match_time_counting = counting if counting: self.match_end_time_rel_or_abs = int(time.time() + seconds_remaining) else: self.match_end_time_rel_or_abs = seconds_remaining def set_ready(self, port, pingport): if port is not None: self.be_score = 0 self.ds_score = 0 self.port = port self.pingport = pingport self.joinable = True self.start_time = datetime.datetime.utcnow() for unique_id, player in self.players.items(): b4msg = a00b4().set_server(self).set_player(unique_id) b4msg.findbytype(m042a).set(3) b4msg.content.append(m02ff()) player.send(b4msg) self.send(Login2LauncherNextMapMessage()) else: self.joinable = False def get_time_remaining(self): if self.match_end_time_rel_or_abs is not None: if self.match_time_counting: time_remaining = int(self.match_end_time_rel_or_abs - time.time()) else: time_remaining = self.match_end_time_rel_or_abs else: time_remaining = 0 if time_remaining < 0: time_remaining = 0 return time_remaining def add_player(self, player): assert player.unique_id not in self.players self.players[player.unique_id] = player player.vote = None player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherAddPlayer( player.unique_id, str(player_ip) if player_ip is not None else '', player.player_settings.progression.rank_xp, player.player_settings.progression.is_eligible_for_first_win()) self.send(msg) def remove_player(self, player): assert player.unique_id in self.players del self.players[player.unique_id] player_ip = player.address_pair.get_address_seen_from( self.address_pair) msg = Login2LauncherRemovePlayer( player.unique_id, str(player_ip) if player_ip is not None else '') self.send(msg) def send_all_players(self, data): for player in self.players.values(): player.send(data) def send_all_players_on_team(self, data, team): for player in self.players.values(): if player.team == team: player.send(data) def set_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherSetPlayerLoadoutsMessage( player.unique_id, player.get_current_loadouts().get_data()) self.send(msg) def remove_player_loadouts(self, player): assert player.unique_id in self.players msg = Login2LauncherRemovePlayerLoadoutsMessage(player.unique_id) self.send(msg) def start_votekick(self, kicker, kickee): if kickee.unique_id in self.players and self.player_being_kicked is None: # Start a new vote reply = a018c() reply.content = [ m02c4().set(self.match_id), m034a().set(kickee.display_name), m0348().set(kickee.unique_id), m02fc().set(STDMSG_VOTE_BY_X_KICK_PLAYER_X_YES_NO), m0442().set_success(True), m0704().set(kicker.unique_id), m0705().set(kicker.display_name) ] self.send_all_players(reply) for player in self.players.values(): player.vote = None self.player_being_kicked = kickee self.logger.info( 'server: votekick started by %d:"%s" against %d:"%s"' % (kicker.unique_id, kicker.display_name, kickee.unique_id, kickee.display_name)) self.login_server.pending_callbacks.add(self, 35, self.end_votekick) def end_votekick(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) self.logger.info( 'server: votekick %s at timeout with %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % ('passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def check_votes(self): if self.player_being_kicked: eligible_voters, total_votes, yes_votes, vote_passed = self._tally_votes( ) # If enough people vote yes or the vote is unanimous, end the vote immediately. # Otherwise wait for the timeout. if (yes_votes >= 8 and vote_passed) or total_votes == eligible_voters: self.logger.info( 'server: votekick %s immediately %d/%d/%d (yes/no/abstain) with %d eligible voters out of %d players' % ('passed' if vote_passed else 'failed', yes_votes, total_votes - yes_votes, eligible_voters - total_votes, eligible_voters, len(self.players))) self._do_kick(vote_passed) def _tally_votes(self): eligible_voters_votes = { p.address_pair.get_address_seen_from( self.login_server.address_pair): p.vote for p in self.players.values() } votes = [v for v in eligible_voters_votes.values() if v is not None] yes_votes = [v for v in votes if v] vote_passed = len(votes) >= 4 and len(yes_votes) / len(votes) >= 0.5 return len(eligible_voters_votes), len(votes), len( yes_votes), vote_passed def _do_kick(self, votekick_passed): player_to_kick = self.player_being_kicked reply = a018c() reply.content = [ m0348().set(player_to_kick.unique_id), m034a().set(player_to_kick.display_name) ] if votekick_passed: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_HAS_BEEN_KICKED), m0442().set_success(True) ]) else: reply.content.extend([ m02fc().set(STDMSG_PLAYER_X_WAS_NOT_VOTED_OUT), m0442().set_success(False) ]) self.send_all_players(reply) if votekick_passed: # TODO: figure out if a real votekick also causes an # inconsistency between the menu you see and the one # you're really in for msg in [a00b0(), a0035().setmainmenu(), a006f()]: player_to_kick.send(msg) player_to_kick.set_state(UnauthenticatedState) ip_to_ban_on_login_server = player_to_kick.address_pair.get_address_seen_from( self.login_server.address_pair) self.firewall.modify_firewall('blacklist', 'add', player_to_kick.unique_id, ip_to_ban_on_login_server) def remove_blacklist_rule(): self.firewall.modify_firewall('blacklist', 'remove', player_to_kick.unique_id, ip_to_ban_on_login_server) self.login_server.pending_callbacks.add(self.login_server, 8 * 3600, remove_blacklist_rule) self.player_being_kicked = None def send_pings(self): player_pings = {} for unique_id, player in self.players.items(): player_pings[unique_id] = player.pings[ self.region] if self.region in player.pings else 999 self.send(Login2LauncherPings(player_pings)) self.login_server.pending_callbacks.add(self, PING_UPDATE_TIME, self.send_pings)