Exemplo n.º 1
0
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
Exemplo n.º 2
0
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))
Exemplo n.º 3
0
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)