Skip to content

Commit 5181de4

Browse files
committed
Add Client.rcon() as well as related exceptions
1 parent 3bec86b commit 5181de4

File tree

2 files changed

+92
-7
lines changed

2 files changed

+92
-7
lines changed

main.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,23 @@
22

33
import trio
44

5-
from samp_query import Client
5+
from samp_query import (
6+
Client,
7+
InvalidRCONPassword,
8+
MissingRCONPassword,
9+
RCONDisabled,
10+
)
611

712

813
async def main(*args: str) -> str | None:
9-
if len(args) != 3:
10-
return f'Usage: {args[0]} ip port'
14+
if not (3 <= len(args) <= 4):
15+
return f'Usage: {args[0]} ip port [rcon_password]'
1116

12-
ip, port = args[1:]
13-
client = Client(ip, int(port))
17+
ip, port = args[1:3]
18+
19+
rcon_password = args[3] if len(args) >= 4 else None
20+
21+
client = Client(ip, int(port), rcon_password)
1422
ping = await client.ping()
1523
print(ping)
1624
print(await client.info())
@@ -23,6 +31,20 @@ async def main(*args: str) -> str | None:
2331

2432
print('Uses open.mp:', await client.is_omp())
2533
print(await client.rules())
34+
35+
try:
36+
print(await client.rcon('varlist'))
37+
print(await client.rcon('players'))
38+
39+
except MissingRCONPassword:
40+
print("You didn't specify a RCON password.")
41+
42+
except RCONDisabled:
43+
print('RCON is disabled.')
44+
45+
except InvalidRCONPassword:
46+
print('Invalid RCON password.')
47+
2648
return None
2749

2850

samp_query/__init__.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
import cchardet as chardet # type: ignore
77
import trio
88

9+
# Assuming latency variance is less than 100%
10+
MAX_LATENCY_VARIANCE = 2
11+
12+
13+
def encode_codepage(string: str) -> bytes:
14+
for codepage in range(1250, 1259):
15+
try:
16+
return string.encode(f'cp{codepage}')
17+
except UnicodeEncodeError:
18+
continue
19+
20+
raise ValueError(f'Unable to encode string "{string}"')
21+
22+
23+
def pack_string(string: str, len_type: str) -> bytes:
24+
format = f'<{len_type}'
25+
return struct.pack(format, len(string)) + encode_codepage(string)
26+
927

1028
def unpack_string(data: bytes, len_type: str) -> tuple[str, bytes]:
1129
format = f'<{len_type}'
@@ -16,6 +34,18 @@ def unpack_string(data: bytes, len_type: str) -> tuple[str, bytes]:
1634
return string.decode(encoding), data
1735

1836

37+
class MissingRCONPassword(Exception):
38+
pass
39+
40+
41+
class InvalidRCONPassword(Exception):
42+
pass
43+
44+
45+
class RCONDisabled(Exception):
46+
pass
47+
48+
1949
@dataclass
2050
class ServerInfo:
2151
name: str
@@ -170,8 +200,7 @@ async def is_omp(self) -> bool:
170200
ping = await self.ping()
171201
payload = random.getrandbits(32).to_bytes(4, 'little')
172202

173-
# Assuming latency variance is less than 100%
174-
with trio.move_on_after(2 * ping):
203+
with trio.move_on_after(MAX_LATENCY_VARIANCE * ping):
175204
await self.send(b'o', payload)
176205
assert self.prefix
177206
data = await self.receive(header=self.prefix + b'o' + payload)
@@ -197,3 +226,37 @@ async def rules(self) -> RuleList:
197226
assert self.prefix
198227
data = await self.receive(header=self.prefix + b'r')
199228
return RuleList.from_data(data)
229+
230+
async def rcon(self, command: str) -> str:
231+
if not self.rcon_password:
232+
raise MissingRCONPassword()
233+
234+
ping = await self.ping()
235+
payload = (
236+
pack_string(self.rcon_password, 'H')
237+
+ pack_string(command, 'H')
238+
)
239+
await self.send(b'x', payload)
240+
assert self.prefix
241+
242+
response = ''
243+
244+
with trio.move_on_after(MAX_LATENCY_VARIANCE * ping) as cancel_scope:
245+
while True:
246+
start_time = trio.current_time()
247+
data = await self.receive(header=self.prefix + b'x')
248+
receive_duration = trio.current_time() - start_time
249+
line_len = struct.unpack_from('<H', data)[0]
250+
data = data[2:] # short, see above
251+
assert len(data) == line_len
252+
encoding: str = chardet.detect(data)['encoding']
253+
response += data.decode(encoding) + '\n'
254+
cancel_scope.deadline += receive_duration
255+
256+
if not response:
257+
raise RCONDisabled()
258+
259+
if response == 'Invalid RCON password.\n':
260+
raise InvalidRCONPassword()
261+
262+
return response[:-1] # Strip trailing newline

0 commit comments

Comments
 (0)