Passed
Push — master ( a89286...fe5519 )
by Steffen
01:36
created

GameServers.parse_64_legacy_response()   A

Complexity

Conditions 3

Size

Total Lines 30
Code Lines 21

Duplication

Lines 30
Ratio 100 %

Code Coverage

Tests 16
CRAP Score 3

Importance

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