Skip to content

Commit 507d2b0

Browse files
Support Unreal2 Protocol
1 parent 6c8eb36 commit 507d2b0

16 files changed

+521
-33
lines changed

opengsq/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class InvalidPacketException(Exception):
2+
pass
3+
4+
5+
class AuthenticationException(Exception):
6+
pass

opengsq/protocols/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
from opengsq.protocols.quake2 import Quake2
77
from opengsq.protocols.quake3 import Quake3
88
from opengsq.protocols.source import Source
9-
from opengsq.protocols.won import WON
9+
from opengsq.protocols.unreal2 import Unreal2
10+
from opengsq.protocols.won import WON

opengsq/protocols/gamespy1.py

-4
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ class __Request():
1919
STATUS = b'\\status\\xserverquery'
2020
TEAMS = b'\\teams\\'
2121

22-
def __init__(self, address: str, query_port: int, timeout: float = 5.0):
23-
"""GameSpy Query Protocol version 1"""
24-
super().__init__(address, query_port, timeout)
25-
2622
async def get_basic(self) -> dict:
2723
"""This returns basic server information, mainly for recognition."""
2824
return self.__parse_as_key_values(await self.__connect_and_send(self.__Request.BASIC))

opengsq/protocols/gamespy2.py

-4
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ class Request(Flag):
1414
PLAYERS = auto()
1515
TEAMS = auto()
1616

17-
def __init__(self, address: str, query_port: int, timeout: float = 5.0):
18-
"""GameSpy Query Protocol version 2"""
19-
super().__init__(address, query_port, timeout)
20-
2117
async def get_status(self, request: Request = Request.INFO | Request.PLAYERS | Request.TEAMS) -> dict:
2218
"""Retrieves information about the server including, Info, Players, and Teams."""
2319
# Connect to remote host

opengsq/protocols/gamespy3.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22

33
from opengsq.binary_reader import BinaryReader
4+
from opengsq.exceptions import InvalidPacketException
45
from opengsq.protocol_base import ProtocolBase
56
from opengsq.socket_async import SocketAsync
67

@@ -10,10 +11,6 @@ class GameSpy3(ProtocolBase):
1011
full_name = 'GameSpy Query Protocol version 3'
1112
challenge = False
1213

13-
def __init__(self, address: str, query_port: int, timeout: float = 5.0):
14-
"""GameSpy Query Protocol version 3"""
15-
super().__init__(address, query_port, timeout)
16-
1714
async def get_status(self):
1815
"""Retrieves information about the server including, Info, Players, and Teams."""
1916
# Connect to remote host
@@ -137,10 +134,6 @@ async def __read(self, sock) -> bytes:
137134
return response
138135

139136

140-
class InvalidPacketException(Exception):
141-
pass
142-
143-
144137
if __name__ == '__main__':
145138
import asyncio
146139
import json

opengsq/protocols/gamespy4.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ class GameSpy4(GameSpy3):
55
"""GameSpy Query Protocol version 4"""
66
full_name = 'GameSpy Query Protocol version 4'
77
challenge = True
8-
9-
def __init__(self, address: str, query_port: int, timeout: float = 5.0):
10-
"""GameSpy Query Protocol version 4"""
11-
super().__init__(address, query_port, timeout)
8+
129

1310
if __name__ == '__main__':
1411
import asyncio

opengsq/protocols/source.py

+1-12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from enum import Enum
55

66
from opengsq.binary_reader import BinaryReader
7+
from opengsq.exceptions import AuthenticationException, InvalidPacketException
78
from opengsq.protocol_base import ProtocolBase
89
from opengsq.socket_async import SocketAsync
910

@@ -27,10 +28,6 @@ class __ResponseHeader():
2728
S2A_PLAYER = 0x44
2829
S2A_RULES = 0x45
2930
A2A_ACK = 0x6A
30-
31-
def __init__(self, address: str, query_port: int = 27015, timeout: float = 5.0):
32-
"""Source Engine Query Protocol"""
33-
super().__init__(address, query_port, timeout)
3431

3532
async def get_info(self) -> dict:
3633
"""
@@ -448,14 +445,6 @@ def __init__(self, *args):
448445
def get_bytes(self):
449446
packet_bytes = self.id.to_bytes(4, byteorder = 'little') + self.type.to_bytes(4, byteorder = 'little') + str.encode(self.body + '\0')
450447
return len(packet_bytes).to_bytes(4, byteorder = 'little') + packet_bytes
451-
452-
453-
class InvalidPacketException(Exception):
454-
pass
455-
456-
457-
class AuthenticationException(Exception):
458-
pass
459448

460449

461450
if __name__ == '__main__':

opengsq/protocols/unreal2.py

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import re
2+
3+
from opengsq.binary_reader import BinaryReader
4+
from opengsq.exceptions import InvalidPacketException
5+
from opengsq.protocol_base import ProtocolBase
6+
from opengsq.socket_async import SocketAsync
7+
8+
9+
class Unreal2(ProtocolBase):
10+
"""Unreal 2 Protocol"""
11+
full_name = 'Unreal 2 Protocol'
12+
13+
_DETAILS = 0x00
14+
_RULES = 0x01
15+
_PLAYERS = 0x02
16+
17+
async def get_details(self):
18+
with SocketAsync() as sock:
19+
sock.settimeout(self._timeout)
20+
await sock.connect((self._address, self._query_port))
21+
22+
# Send Request
23+
sock.send(b'\x79\x00\x00\x00' + bytes([self._DETAILS]))
24+
25+
# Server response
26+
response = await sock.recv()
27+
28+
# Remove the first 4 bytes \x80\x00\x00\x00
29+
br = BinaryReader(response[4:])
30+
header = br.read_byte()
31+
32+
if header != self._DETAILS:
33+
raise InvalidPacketException(
34+
'Packet header mismatch. Received: {}. Expected: {}.'
35+
.format(chr(header), chr(self._DETAILS))
36+
)
37+
38+
details = {}
39+
details['ServerId'] = br.read_long() # 0
40+
details['ServerIP'] = br.read_string() # empty
41+
details['GamePort'] = br.read_long()
42+
details['QueryPort'] = br.read_long() # 0
43+
details['ServerName'] = self.__read_string(br)
44+
details['MapName'] = self.__read_string(br)
45+
details['GameType'] = self.__read_string(br)
46+
details['NumPlayers'] = br.read_long()
47+
details['MaxPlayers'] = br.read_long()
48+
49+
if br.length() > 12:
50+
try:
51+
# Killing Floor
52+
stream_position = br.stream_position
53+
details['WaveCurrent'] = br.read_long()
54+
details['WaveTotal'] = br.read_long()
55+
details['Ping'] = br.read_long()
56+
details['Flags'] = br.read_long()
57+
details['Skill'] = self.__read_string(br)
58+
except:
59+
br.stream_position = stream_position
60+
details['Ping'] = br.read_long()
61+
details['Flags'] = br.read_long()
62+
details['Skill'] = self.__read_string(br)
63+
64+
return details
65+
66+
async def get_rules(self):
67+
with SocketAsync() as sock:
68+
sock.settimeout(self._timeout)
69+
await sock.connect((self._address, self._query_port))
70+
71+
# Send Request
72+
sock.send(b'\x79\x00\x00\x00' + bytes([self._RULES]))
73+
74+
# Server response
75+
response = await sock.recv()
76+
77+
# Remove the first 4 bytes \x80\x00\x00\x00
78+
br = BinaryReader(response[4:])
79+
header = br.read_byte()
80+
81+
if header != self._RULES:
82+
83+
raise InvalidPacketException(
84+
'Packet header mismatch. Received: {}. Expected: {}.'
85+
.format(chr(header), chr(self._RULES))
86+
)
87+
88+
rules = {}
89+
rules['Mutators'] = []
90+
91+
while not br.is_end():
92+
key = self.__read_string(br)
93+
val = self.__read_string(br)
94+
95+
if key.lower() == 'mutator':
96+
rules['Mutators'].append(val)
97+
else:
98+
rules[key] = val
99+
100+
return rules
101+
102+
async def get_players(self):
103+
with SocketAsync() as sock:
104+
sock.settimeout(self._timeout)
105+
await sock.connect((self._address, self._query_port))
106+
107+
# Send Request
108+
sock.send(b'\x79\x00\x00\x00' + bytes([self._PLAYERS]))
109+
110+
# Server response
111+
response = await sock.recv()
112+
113+
# Remove the first 4 bytes \x80\x00\x00\x00
114+
br = BinaryReader(response[4:])
115+
header = br.read_byte()
116+
117+
if header != self._PLAYERS:
118+
raise InvalidPacketException(
119+
'Packet header mismatch. Received: {}. Expected: {}.'
120+
.format(chr(header), chr(self._PLAYERS))
121+
)
122+
123+
players = []
124+
125+
while not br.is_end():
126+
player = {}
127+
player['Id'] = br.read_long()
128+
player['Name'] = self.__read_string(br)
129+
player['Ping'] = br.read_long()
130+
player['Score'] = br.read_long()
131+
player['StatsId'] = br.read_long()
132+
players.append(player)
133+
134+
return players
135+
136+
@staticmethod
137+
def strip_colors(text: bytes):
138+
"""Strip color codes"""
139+
return re.compile(b'\x7f|[\x00-\x1a]|[\x1c-\x1f]').sub(b'', text).replace(b'\x1b@@', b'').replace(b'\x1b@', b'').replace(b'\x1b', b'')
140+
141+
def __read_string(self, br: BinaryReader):
142+
length = br.read_byte()
143+
string = br.read_string()
144+
145+
if length == len(string) + 1:
146+
b = bytes(string, encoding='utf-8')
147+
else:
148+
b = bytes(string, encoding='utf-16')
149+
150+
b = Unreal2.strip_colors(b)
151+
152+
return b.decode('utf-8', 'ignore')
153+
154+
155+
if __name__ == '__main__':
156+
import asyncio
157+
import json
158+
159+
async def main_async():
160+
# ut2004
161+
unreal2 = Unreal2(address='109.230.224.189', query_port=6970, timeout=10.0)
162+
details = await unreal2.get_details()
163+
print(json.dumps(details, indent=None) + '\n')
164+
rules = await unreal2.get_rules()
165+
print(json.dumps(rules, indent=None) + '\n')
166+
players = await unreal2.get_players()
167+
print(json.dumps(players, indent=None) + '\n')
168+
169+
asyncio.run(main_async())

tests/protocols/test_unreal2.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
from opengsq.protocols.unreal2 import Unreal2
3+
from .result_handler import ResultHandler
4+
5+
6+
handler = ResultHandler('test_unreal2')
7+
# handler.enable_save = True
8+
9+
# Killing Floor
10+
test = Unreal2(address='51.195.117.236', query_port=9981)
11+
12+
@pytest.mark.asyncio
13+
async def test_get_details():
14+
result = await test.get_details()
15+
await handler.save_result('test_get_details', result)
16+
17+
@pytest.mark.asyncio
18+
async def test_get_rules():
19+
result = await test.get_rules()
20+
await handler.save_result('test_get_rules', result)
21+
22+
@pytest.mark.asyncio
23+
async def test_get_players():
24+
result = await test.get_players()
25+
await handler.save_result('test_get_players', result)

tests/protocols/test_won.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
from opengsq.protocols.won import WON
3+
from .result_handler import ResultHandler
4+
5+
6+
handler = ResultHandler('test_won')
7+
# handler.enable_save = True
8+
9+
# Counter-Strike 1.5
10+
won = WON('212.227.190.150', 27020)
11+
12+
@pytest.mark.asyncio
13+
async def test_get_info():
14+
result = await won.get_info()
15+
await handler.save_result('test_get_info', result)
16+
17+
@pytest.mark.asyncio
18+
async def test_get_players():
19+
result = await won.get_players()
20+
await handler.save_result('test_get_players', result)
21+
22+
@pytest.mark.asyncio
23+
async def test_get_rules():
24+
result = await won.get_rules()
25+
await handler.save_result('test_get_rules', result)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"ServerId": 0,
3+
"ServerIP": "",
4+
"GamePort": 9980,
5+
"QueryPort": 0,
6+
"ServerName": "Uliunai.lt```|[v1065]```|HOE/Suicidal/Hard|150 LvL```|+6 Perks",
7+
"MapName": "KF-Grosse-EDIT",
8+
"GameType": "MCGameType",
9+
"NumPlayers": 13,
10+
"MaxPlayers": 15,
11+
"WaveCurrent": 1,
12+
"WaveTotal": 10,
13+
"Ping": 0,
14+
"Flags": 512,
15+
"Skill": "0"
16+
}

0 commit comments

Comments
 (0)