Passed
Push — master ( 6a14d4...f99a48 )
by Steffen
01:38
created

GameServers.parse_extended_response()   A

Complexity

Conditions 2

Size

Total Lines 30
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 22
nop 2
dl 0
loc 30
ccs 17
cts 17
cp 1
crap 2
rs 9.352
c 0
b 0
f 0
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 1
import logging
4 1
import socket
5 1
from collections import deque
6 1
from time import sleep
7 1
from typing import List
8
9 1
from tw_serverinfo import Network
10 1
from tw_serverinfo.models.game_server import GameServer
11
12
13 1
class GameServers(object):
14
    """GameServers Class containing the logic to request and parse responses from the game servers"""
15 1
    SERVER_CHUNK_SIZE = 50
16 1
    SERVER_CHUNK_SLEEP_MS = 1000
17
18 1
    _game_servers = []
19
20 1
    def fill_server_info(self, game_servers: List[GameServer]) -> None:
21
        """Send the SERVERBROWSE_GETINFO and SERVERBROWSE_GETINFO_64_LEGACY packet to each game server in the
22
        passed game_servers list, parse the response and update the GameServer objects in the game_servers argument
23
24
        :type game_servers: list
25
        :return:
26
        """
27 1
        self._game_servers = game_servers
28
        # create an udp protocol socket
29 1
        sock = socket.socket(family=Network.PROTOCOL_FAMILY, type=socket.SOCK_DGRAM)
30
        # set the socket to non blocking to allow parallel requests
31 1
        sock.setblocking(False)
32
33 1
        i = 0
34 1
        for game_server in self._game_servers:  # type: GameServer
35 1
            i += 1
36 1
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETINFO'], extra_data=b'xe',
37
                                server=game_server)
38 1
            Network.send_packet(sock=sock, data=Network.PACKETS['SERVERBROWSE_GETINFO_64_LEGACY'], extra_data=b'xe',
39
                                server=game_server)
40
41 1
            if i % self.SERVER_CHUNK_SIZE == 0 or i >= len(self._game_servers):
42 1
                duration_without_response = Network.CONNECTION_SLEEP_DURATION
43 1
                sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
44
45 1
                while duration_without_response < Network.CONNECTION_TIMEOUT:
46 1
                    if not Network.receive_packet(sock, self._game_servers, self.process_packet):
47
                        # increase the measured duration without a response and sleep for the set duration
48 1
                        duration_without_response += Network.CONNECTION_SLEEP_DURATION
49 1
                        sleep(Network.CONNECTION_SLEEP_DURATION / 1000.0)
50
                    else:
51
                        # if we got a response reset the duration in case we receive multiple packets
52 1
                        duration_without_response = 0
53
54
        # close the socket after checking all servers
55 1
        sock.close()
56
57 1
    def process_packet(self, data: bytes, server: GameServer) -> None:
58
        """Process packet function for
59
         - SERVERBROWSE_COUNT
60
         - SERVERBROWSE_LIST
61
        packets
62
63
        :type data: bytes
64
        :type server: GameServer
65
        :return:
66
        """
67 1
        slots = deque(data[14:].split(b"\x00"))
68 1
        token = int(slots.popleft().decode('utf-8'))
69
        # ToDo: token validation
70
71 1
        server.response = True
72
73 1
        if data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO']:
74
            # vanilla
75 1
            self.parse_vanilla_response(slots, server)
76 1
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_64_LEGACY']:
77
            # 64 legacy
78 1
            self.parse_64_legacy_response(slots, server)
79 1
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_EXTENDED']:
80
            # extended response, current default of DDNet
81 1
            self.parse_extended_response(slots, server)
82 1
        elif data[6:6 + 8] == Network.PACKETS['SERVERBROWSE_INFO_EXTENDED_MORE']:
83 1
            logging.log(logging.DEBUG, 'no idea what to expect here, never got useful data')
84
85 1 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
86 1
    def parse_vanilla_response(slots: deque, server: GameServer) -> None:
87
        """Parse the default response of the vanilla client
88
89
        :type slots: deque
90
        :type server: GameServer
91
        :return:
92
        """
93 1
        server.server_type = 'vanilla'
94 1
        server.version = slots.popleft().decode('utf-8')
95 1
        server.name = slots.popleft().decode('utf-8')
96 1
        server.map_name = slots.popleft().decode('utf-8')
97 1
        server.game_type = slots.popleft().decode('utf-8')
98 1
        server.flags = int(slots.popleft().decode('utf-8'))
99 1
        server.num_players = int(slots.popleft().decode('utf-8'))
100 1
        server.max_players = int(slots.popleft().decode('utf-8'))
101 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
102 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
103
104 1
        while len(slots) >= 5:
105
            # no idea what this is, always empty
106 1
            server.players.append({
107
                'name': slots.popleft(),
108
                'clan': slots.popleft(),
109
                'country': int(slots.popleft().decode('utf-8')),
110
                'score': int(slots.popleft().decode('utf-8')),
111
                'ingame': int(slots.popleft().decode('utf-8'))
112
            })
113
114 1 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
115 1
    def parse_64_legacy_response(slots: deque, server: GameServer) -> None:
116
        """Parse the 64 slot legacy response
117
118
        :type slots: deque
119
        :type server: GameServer
120
        :return:
121
        """
122 1
        server.server_type = '64_legacy'
123 1
        server.version = slots.popleft().decode('utf-8')
124 1
        server.name = slots.popleft().decode('utf-8')
125 1
        server.map_name = slots.popleft().decode('utf-8')
126 1
        server.game_type = slots.popleft().decode('utf-8')
127 1
        server.flags = int(slots.popleft().decode('utf-8'))
128 1
        server.num_players = int(slots.popleft().decode('utf-8'))
129 1
        server.max_players = int(slots.popleft().decode('utf-8'))
130 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
131 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
132
133 1
        if slots[0] == b'':
134
            # no idea what this is, always empty
135 1
            slots.popleft()
136
137 1
        while len(slots) >= 5:
138 1
            server.players.append({
139
                'name': slots.popleft(),
140
                'clan': slots.popleft(),
141
                'country': int(slots.popleft().decode('utf-8')),
142
                'score': int(slots.popleft().decode('utf-8')),
143
                'ingame': int(slots.popleft().decode('utf-8'))
144
            })
145
146 1
    @staticmethod
147 1
    def parse_extended_response(slots: deque, server: GameServer) -> None:
148
        """Parse the extended server info response(default for DDNet)
149
150
        :type slots: deque
151
        :type server: GameServer
152
        :return:
153
        """
154 1
        server.server_type = 'ext'
155 1
        server.version = slots.popleft().decode('utf-8')
156 1
        server.name = slots.popleft().decode('utf-8')
157 1
        server.map_name = slots.popleft().decode('utf-8')
158 1
        server.map_crc = int(slots.popleft().decode('utf-8'))
159 1
        server.map_size = int(slots.popleft().decode('utf-8'))
160 1
        server.game_type = slots.popleft().decode('utf-8')
161 1
        server.flags = int(slots.popleft().decode('utf-8'))
162 1
        server.num_players = int(slots.popleft().decode('utf-8'))
163 1
        server.max_players = int(slots.popleft().decode('utf-8'))
164 1
        server.num_clients = int(slots.popleft().decode('utf-8'))
165 1
        server.max_clients = int(slots.popleft().decode('utf-8'))
166
167 1
        while len(slots) >= 6:
168
            # no idea what this is, always empty
169 1
            slots.popleft()
170 1
            server.players.append({
171
                'name': slots.popleft(),
172
                'clan': slots.popleft(),
173
                'country': int(slots.popleft().decode('utf-8')),
174
                'score': int(slots.popleft().decode('utf-8')),
175
                'ingame': int(slots.popleft().decode('utf-8'))
176
            })
177