1+ from __future__ import annotations
2+
3+ import asyncio
4+ import struct
5+ import xmlrpc .client as xmlrpclib
6+ from typing import Any , Optional
7+
8+ from opengsq .exceptions import InvalidPacketException
9+ from opengsq .protocol_base import ProtocolBase
10+ from opengsq .responses .nadeo import Status
11+
12+
13+ class Nadeo (ProtocolBase ):
14+ full_name = "Nadeo GBXRemote Protocol"
15+ INITIAL_HANDLER = 0x80000000
16+ MAXIMUM_HANDLER = 0xFFFFFFFF
17+
18+ def __init__ (self , host : str , port : int = 5000 , timeout : float = 5.0 ):
19+ super ().__init__ (host , port , timeout )
20+ self .handler = self .MAXIMUM_HANDLER
21+ self ._reader : Optional [asyncio .StreamReader ] = None
22+ self ._writer : Optional [asyncio .StreamWriter ] = None
23+
24+ async def connect (self ) -> None :
25+ self ._reader , self ._writer = await asyncio .open_connection (self ._host , self ._port )
26+
27+ # Read and validate header
28+ data = await self ._reader .read (4 )
29+ header_length = struct .unpack ('<I' , data )[0 ]
30+
31+ data = await self ._reader .read (header_length )
32+ header = data .decode ()
33+
34+ if header != "GBXRemote 2" :
35+ raise InvalidPacketException ('No "GBXRemote 2" header found!' )
36+
37+ async def close (self ) -> None :
38+ if self ._writer :
39+ self ._writer .close ()
40+ await self ._writer .wait_closed ()
41+
42+ async def __aenter__ (self ):
43+ await self .connect ()
44+ return self
45+
46+ async def __aexit__ (self , exc_type , exc_value , traceback ):
47+ await self .close ()
48+
49+ async def _execute (self , method : str , * args ) -> Any :
50+ if self .handler == self .MAXIMUM_HANDLER :
51+ self .handler = self .INITIAL_HANDLER
52+ else :
53+ self .handler += 1
54+
55+ handler_bytes = self .handler .to_bytes (4 , byteorder = 'little' )
56+ data = xmlrpclib .dumps (args , method ).encode ()
57+ packet_len = len (data )
58+
59+ packet = packet_len .to_bytes (4 , byteorder = 'little' ) + handler_bytes + data
60+
61+ self ._writer .write (packet )
62+ await self ._writer .drain ()
63+
64+ # Read response
65+ header = await self ._reader .read (8 )
66+ size = struct .unpack ('<I' , header [:4 ])[0 ]
67+ handler = struct .unpack ('<I' , header [4 :8 ])[0 ]
68+
69+ if handler != self .handler :
70+ raise InvalidPacketException (f'Handler mismatch: { handler } != { self .handler } ' )
71+
72+ data = await self ._reader .readexactly (size )
73+
74+ try :
75+ response = xmlrpclib .loads (data .decode ())
76+ return response [0 ][0 ] if response else None
77+ except xmlrpclib .Fault as e :
78+ raise InvalidPacketException (f'RPC Fault: { e } ' )
79+
80+ async def authenticate (self , username : str , password : str ) -> bool :
81+ await self .connect ()
82+ result = await self ._execute ('Authenticate' , username , password )
83+ return bool (result )
84+
85+ async def get_status (self ) -> Status :
86+ version = await self ._execute ('GetVersion' )
87+ server_info = await self ._execute ('GetServerOptions' )
88+ player_list = await self ._execute ('GetPlayerList' , 100 , 0 )
89+ current_map = await self ._execute ('GetCurrentChallengeInfo' )
90+
91+ return Status .from_raw_data (
92+ version_data = version ,
93+ server_data = server_info ,
94+ players_data = player_list ,
95+ map_data = current_map
96+ )
0 commit comments