77import trio
88
99
10- def unpack_string (data : bytes , len_type : str = 'I' ) -> tuple [str , bytes ]:
10+ def unpack_string (data : bytes , len_type : str ) -> tuple [str , bytes ]:
1111 format = f'<{ len_type } '
1212 size = struct .calcsize (format )
1313 str_len , data = * struct .unpack_from (format , data ), data [size :]
@@ -29,9 +29,9 @@ class ServerInfo:
2929 def from_data (cls , data : bytes ) -> ServerInfo :
3030 password , players , max_players = struct .unpack_from ('<?HH' , data )
3131 data = data [5 :] # _Bool + short + short, see above
32- name , data = unpack_string (data )
33- gamemode , data = unpack_string (data )
34- language , data = unpack_string (data )
32+ name , data = unpack_string (data , 'I' )
33+ gamemode , data = unpack_string (data , 'I' )
34+ language , data = unpack_string (data , 'I' )
3535
3636 assert not data # We consumed all the buffer
3737
@@ -81,14 +81,49 @@ def from_data(cls, data: bytes) -> PlayerList:
8181 return cls (players = players )
8282
8383
84+ @dataclass
85+ class Rule :
86+ name : str
87+ value : str
88+
89+ @classmethod
90+ def from_data (cls , data : bytes ) -> tuple [Rule , bytes ]:
91+ name , data = unpack_string (data , 'B' )
92+ value , data = unpack_string (data , 'B' )
93+
94+ return cls (
95+ name = name ,
96+ value = value ,
97+ ), data
98+
99+
100+ @dataclass
101+ class RuleList :
102+ rules : list [Rule ]
103+
104+ @classmethod
105+ def from_data (cls , data : bytes ) -> RuleList :
106+ rule_count = struct .unpack_from ('<H' , data )[0 ]
107+ data = data [2 :] # short, see above
108+ rules = []
109+
110+ for _ in range (rule_count ):
111+ rule , data = Rule .from_data (data )
112+ rules .append (rule )
113+
114+ assert not data # We consumed all the buffer
115+
116+ return cls (rules = rules )
117+
118+
84119@dataclass
85120class Client :
86121 ip : str
87122 port : int
88123 rcon_password : str | None = field (default = None , repr = False )
89124
125+ prefix : bytes | None = field (default = None , repr = False )
90126 _socket : trio .socket .SocketType | None = field (default = None , repr = False )
91- _prefix : bytes | None = field (default = None , repr = False )
92127
93128 async def connect (self ) -> None :
94129 family , type , proto , _ , (ip , * _ ) = (await trio .socket .getaddrinfo (
@@ -100,7 +135,7 @@ async def connect(self) -> None:
100135 self .ip = ip
101136 self ._socket = socket = trio .socket .socket (family , type , proto )
102137 await socket .connect ((self .ip , self .port ))
103- self ._prefix = (
138+ self .prefix = (
104139 b'SAMP'
105140 + trio .socket .inet_aton (self .ip )
106141 + self .port .to_bytes (2 , 'little' )
@@ -110,8 +145,8 @@ async def send(self, opcode: bytes, payload: bytes = b'') -> None:
110145 if not self ._socket :
111146 await self .connect ()
112147
113- assert self ._socket and self ._prefix
114- await self ._socket .send (self ._prefix + opcode + payload )
148+ assert self ._socket and self .prefix
149+ await self ._socket .send (self .prefix + opcode + payload )
115150
116151 async def receive (self , header : bytes = b'' ) -> bytes :
117152 assert self ._socket
@@ -126,8 +161,8 @@ async def ping(self) -> float:
126161 payload = random .getrandbits (32 ).to_bytes (4 , 'little' )
127162 start_time = trio .current_time ()
128163 await self .send (b'p' , payload )
129- assert self ._prefix
130- data = await self .receive (header = self ._prefix + b'p' + payload )
164+ assert self .prefix
165+ data = await self .receive (header = self .prefix + b'p' + payload )
131166 assert not data # No data beyond expected header
132167 return trio .current_time () - start_time
133168
@@ -138,20 +173,26 @@ async def is_omp(self) -> bool:
138173 # Assuming latency variance is less than 100%
139174 with trio .move_on_after (2 * ping ):
140175 await self .send (b'o' , payload )
141- assert self ._prefix
142- await self .receive (header = self ._prefix + b'o' + payload )
176+ assert self .prefix
177+ await self .receive (header = self .prefix + b'o' + payload )
143178 return True
144179
145180 return False
146181
147182 async def info (self ) -> ServerInfo :
148183 await self .send (b'i' )
149- assert self ._prefix
150- data = await self .receive (header = self ._prefix + b'i' )
184+ assert self .prefix
185+ data = await self .receive (header = self .prefix + b'i' )
151186 return ServerInfo .from_data (data )
152187
153188 async def players (self ) -> PlayerList :
154189 await self .send (b'c' )
155- assert self ._prefix
156- data = await self .receive (header = self ._prefix + b'c' )
190+ assert self .prefix
191+ data = await self .receive (header = self .prefix + b'c' )
157192 return PlayerList .from_data (data )
193+
194+ async def rules (self ) -> RuleList :
195+ await self .send (b'r' )
196+ assert self .prefix
197+ data = await self .receive (header = self .prefix + b'r' )
198+ return RuleList .from_data (data )
0 commit comments