1
1
import logging
2
+ import time
2
3
from collections .abc import Callable
3
4
from enum import IntEnum
4
5
from pathlib import Path
5
- from socket import IPPROTO_TCP , TCP_NODELAY , socket , timeout
6
+ from socket import IPPROTO_TCP , TCP_NODELAY , socket
6
7
from threading import Thread
7
- from time import sleep
8
8
from typing import Optional
9
9
10
10
from rlbot import flat
16
16
17
17
class SocketDataType (IntEnum ):
18
18
"""
19
- https://wiki.rlbot.org/framework/sockets-specification/#data-types
19
+ See https://github.com/RLBot/core/blob/master/RLBotCS/Types/DataType.cs
20
+ and https://wiki.rlbot.org/framework/sockets-specification/#data-types
20
21
"""
21
22
22
23
NONE = 0
@@ -54,16 +55,25 @@ def __init__(self, type: int, data: bytes):
54
55
self .data = data
55
56
56
57
57
- def read_from_socket (s : socket ) -> SocketMessage :
58
+ def read_message_from_socket (s : socket ) -> SocketMessage :
58
59
type_int = int_from_bytes (s .recv (2 ))
59
60
size = int_from_bytes (s .recv (2 ))
60
61
data = s .recv (size )
61
62
return SocketMessage (type_int , data )
62
63
63
64
64
65
class SocketRelay :
66
+ """
67
+ The SocketRelay provides an abstraction over the direct communication with
68
+ the RLBotServer making it easy to send the various types of messages.
69
+
70
+ Common use patterns are covered by `bot.py`, `script.py`, `hivemind.py`, and `match.py`
71
+ from `rlbot.managers`.
72
+ """
73
+
65
74
is_connected = False
66
- _should_continue = True
75
+ _running = False
76
+ """Indicates whether a messages are being handled by the `run` loop (potentially in a background thread)"""
67
77
68
78
on_connect_handlers : list [Callable [[], None ]] = []
69
79
packet_handlers : list [Callable [[flat .GamePacket ], None ]] = []
@@ -93,10 +103,12 @@ def __del__(self):
93
103
self .socket .close ()
94
104
95
105
def send_bytes (self , data : bytes , data_type : SocketDataType ):
106
+ assert self .is_connected , "Connection has not been established"
107
+
96
108
size = len (data )
97
109
if size > MAX_SIZE_2_BYTES :
98
110
self .logger .error (
99
- "Couldn't send a %s message because it was too big!" , data_type
111
+ "Couldn't send %s message because it was too big!" , data_type . name
100
112
)
101
113
return
102
114
@@ -129,11 +141,9 @@ def stop_match(self, shutdown_server: bool = False):
129
141
flatbuffer = flat .StopCommand (shutdown_server ).pack ()
130
142
self .send_bytes (flatbuffer , SocketDataType .STOP_COMMAND )
131
143
132
- def start_match (
133
- self ,
134
- match_config : Path | flat .MatchSettings ,
135
- rlbot_server_port : int = RLBOT_SERVER_PORT ,
136
- ):
144
+ def start_match (self , match_config : Path | flat .MatchSettings ):
145
+ self .logger .info ("Python interface is attempting to start match..." )
146
+
137
147
match match_config :
138
148
case Path () as path :
139
149
string_path = str (path .absolute ().resolve ())
@@ -142,55 +152,69 @@ def start_match(
142
152
case flat .MatchSettings () as settings :
143
153
flatbuffer = settings .pack ()
144
154
flat_type = SocketDataType .MATCH_SETTINGS
155
+ case _:
156
+ raise ValueError (
157
+ "Expected MatchSettings or path to match settings toml file"
158
+ )
145
159
146
- def connect_handler ():
147
- self .send_bytes (flatbuffer , flat_type )
148
-
149
- self .run_after_connect (connect_handler , rlbot_server_port )
150
-
151
- def run_after_connect (
152
- self ,
153
- handler : Callable [[], None ],
154
- rlbot_server_port : int = RLBOT_SERVER_PORT ,
155
- ):
156
- if self .is_connected :
157
- handler ()
158
- else :
159
- self .on_connect_handlers .append (handler )
160
- try :
161
- self .connect_and_run (False , False , False , True , rlbot_server_port )
162
- except timeout as e :
163
- raise TimeoutError (
164
- "Took too long to connect to the RLBot executable!"
165
- ) from e
160
+ self .send_bytes (flatbuffer , flat_type )
166
161
167
162
def connect (
168
163
self ,
164
+ * ,
169
165
wants_match_communications : bool ,
170
166
wants_ball_predictions : bool ,
171
167
close_after_match : bool = True ,
172
168
rlbot_server_port : int = RLBOT_SERVER_PORT ,
173
169
):
174
170
"""
175
- Connects to the socket and sends the connection settings.
171
+ Connects to the RLBot server specifying the given settings.
172
+
173
+ - wants_match_communications: Whether match communication messages should be sent to this process.
174
+ - wants_ball_predictions: Whether ball prediction messages should be sent to this process.
175
+ - close_after_match: Whether RLBot should close this connection between matches, specifically upon
176
+ `StartMatch` and `StopMatch` messages, since RLBot does not actually detect the ending of matches.
176
177
177
- NOTE: Bad things happen if the buffer is allowed to fill up. Ensure
178
- `handle_incoming_messages` is called frequently enough to prevent this.
178
+ NOTE: Bad things happen if the message buffer fills up. Ensure `handle_incoming_messages` is called
179
+ frequently to prevent this. See `run` for handling messages continuously .
179
180
"""
181
+ assert not self .is_connected , "Connection has already been established"
182
+
180
183
self .socket .settimeout (self .connection_timeout )
181
- for _ in range (int (self .connection_timeout * 10 )):
182
- try :
183
- self .socket .connect (("127.0.0.1" , rlbot_server_port ))
184
- break
185
- except ConnectionRefusedError :
186
- sleep (0.1 )
187
- except ConnectionAbortedError :
188
- sleep (0.1 )
189
-
190
- self .socket .settimeout (None )
191
- self .is_connected = True
184
+ try :
185
+ begin_time = time .time ()
186
+ next_warning = 10
187
+ while time .time () < begin_time + self .connection_timeout :
188
+ try :
189
+ self .socket .connect (("127.0.0.1" , rlbot_server_port ))
190
+ self .is_connected = True
191
+ break
192
+ except ConnectionRefusedError :
193
+ time .sleep (0.1 )
194
+ except ConnectionAbortedError :
195
+ time .sleep (0.1 )
196
+ if time .time () > begin_time + next_warning :
197
+ next_warning *= 2
198
+ self .logger .warning (
199
+ "Connection is being refused/aborted. Trying again ..."
200
+ )
201
+ if not self .is_connected :
202
+ raise ConnectionRefusedError (
203
+ "Connection was refused/aborted repeatedly! "
204
+ "Ensure that Rocket League and the RLBotServer is running. "
205
+ "Try calling `ensure_server_started()` before connecting."
206
+ )
207
+ except TimeoutError as e :
208
+ raise TimeoutError (
209
+ "Took too long to connect to the RLBot! "
210
+ "Ensure that Rocket League and the RLBotServer is running."
211
+ "Try calling `ensure_server_started()` before connecting."
212
+ ) from e
213
+ finally :
214
+ self .socket .settimeout (None )
215
+
192
216
self .logger .info (
193
- "Socket manager connected to port %s from port %s!" ,
217
+ "SocketRelay connected to port %s from port %s!" ,
194
218
rlbot_server_port ,
195
219
self .socket .getsockname ()[1 ],
196
220
)
@@ -199,76 +223,75 @@ def connect(
199
223
handler ()
200
224
201
225
flatbuffer = flat .ConnectionSettings (
202
- self .agent_id ,
203
- wants_ball_predictions ,
204
- wants_match_communications ,
205
- close_after_match ,
226
+ agent_id = self .agent_id ,
227
+ wants_ball_predictions = wants_ball_predictions ,
228
+ wants_comms = wants_match_communications ,
229
+ close_after_match = close_after_match ,
206
230
).pack ()
207
231
self .send_bytes (flatbuffer , SocketDataType .CONNECTION_SETTINGS )
208
232
209
- def connect_and_run (
210
- self ,
211
- wants_match_communications : bool ,
212
- wants_ball_predictions : bool ,
213
- close_after_match : bool = True ,
214
- only_wait_for_ready : bool = False ,
215
- rlbot_server_port : int = RLBOT_SERVER_PORT ,
216
- ):
233
+ def run (self , * , background_thread : bool = False ):
217
234
"""
218
- Connects to the socket and begins a loop that reads messages and calls any handlers
219
- that have been registered. Connect and run are combined into a single method because
220
- currently bad things happen if the buffer is allowed to fill up.
235
+ Handle incoming messages until disconnected.
236
+ If `background_thread` is `True`, a background thread will be started for this.
221
237
"""
222
- self .connect (
223
- wants_match_communications ,
224
- wants_ball_predictions ,
225
- close_after_match ,
226
- rlbot_server_port ,
227
- )
228
-
229
- incoming_message = read_from_socket (self .socket )
230
- self .handle_incoming_message (incoming_message )
231
-
232
- if only_wait_for_ready :
233
- Thread (target = self .handle_incoming_messages ).start ()
238
+ assert self .is_connected , "Connection has not been established"
239
+ assert not self ._running , "Message handling is already running"
240
+ if background_thread :
241
+ Thread (target = self .run ).start ()
234
242
else :
235
- self .handle_incoming_messages ()
243
+ self ._running = True
244
+ while self ._running and self .is_connected :
245
+ self ._running = self .handle_incoming_messages (blocking = True )
246
+ self ._running = False
236
247
237
- def handle_incoming_messages (self , set_nonblocking_after_recv : bool = False ):
248
+ def handle_incoming_messages (self , blocking = False ) -> bool :
249
+ """
250
+ Empties queue of incoming messages (should be called regularly, see `run`).
251
+ Optionally blocking, ensuring that at least one message will be handled.
252
+ Returns true message handling should continue running, and
253
+ false if RLBotServer has asked us to shut down or an error happened.
254
+ """
255
+ assert self .is_connected , "Connection has not been established"
238
256
try :
239
- while self ._should_continue :
240
- incoming_message = read_from_socket (self .socket )
241
-
242
- if set_nonblocking_after_recv :
243
- self .socket .setblocking (False )
244
-
245
- try :
246
- self .handle_incoming_message (incoming_message )
247
- except flat .InvalidFlatbuffer as e :
248
- self .logger .error (
249
- "Error while unpacking message of type %s (%s bytes): %s" ,
250
- incoming_message .type .name ,
251
- len (incoming_message .data ),
252
- e ,
253
- )
254
- except Exception as e :
255
- self .logger .warning (
256
- "Unexpected error while handling message of type %s: %s" ,
257
- incoming_message .type .name ,
258
- e ,
259
- )
257
+ self .socket .setblocking (blocking )
258
+ incoming_message = read_message_from_socket (self .socket )
259
+ try :
260
+ return self .handle_incoming_message (incoming_message )
261
+ except flat .InvalidFlatbuffer as e :
262
+ self .logger .error (
263
+ "Error while unpacking message of type %s (%s bytes): %s" ,
264
+ incoming_message .type .name ,
265
+ len (incoming_message .data ),
266
+ e ,
267
+ )
268
+ return False
269
+ except Exception as e :
270
+ self .logger .error (
271
+ "Unexpected error while handling message of type %s: %s" ,
272
+ incoming_message .type .name ,
273
+ e ,
274
+ )
275
+ return False
260
276
except BlockingIOError :
261
- raise BlockingIOError
277
+ # No incoming messages and blocking==False
278
+ return True
262
279
except :
263
- self .logger .error ("Socket manager disconnected unexpectedly!" )
280
+ self .logger .error ("SocketRelay disconnected unexpectedly!" )
281
+ return False
264
282
265
283
def handle_incoming_message (self , incoming_message : SocketMessage ):
284
+ """
285
+ Handles a messages by passing it to the relevant handlers.
286
+ Returns True if the message was NOT a shutdown request (i.e. NONE).
287
+ """
288
+
266
289
for raw_handler in self .raw_handlers :
267
290
raw_handler (incoming_message )
268
291
269
292
match incoming_message .type :
270
293
case SocketDataType .NONE :
271
- self . _should_continue = False
294
+ return False
272
295
case SocketDataType .GAME_PACKET :
273
296
if len (self .packet_handlers ) > 0 :
274
297
packet = flat .GamePacket .unpack (incoming_message .data )
@@ -302,13 +325,23 @@ def handle_incoming_message(self, incoming_message: SocketMessage):
302
325
for handler in self .controllable_team_info_handlers :
303
326
handler (player_mappings )
304
327
328
+ return True
329
+
305
330
def disconnect (self ):
306
331
if not self .is_connected :
307
332
self .logger .warning ("Asked to disconnect but was already disconnected." )
308
333
return
309
334
310
335
self .send_bytes (bytes ([1 ]), SocketDataType .NONE )
311
- while self ._should_continue :
312
- sleep (0.1 )
313
-
336
+ timeout = 5.0
337
+ while self ._running and timeout > 0 :
338
+ time .sleep (0.1 )
339
+ timeout -= 0.1
340
+ if timeout <= 0 :
341
+ self .logger .critical ("RLBot is not responding to our disconnect request!?" )
342
+ self ._running = False
343
+
344
+ assert (
345
+ not self ._running
346
+ ), "Disconnect request or timeout should have set self._running to False"
314
347
self .is_connected = False
0 commit comments