Skip to content

Commit fbe42eb

Browse files
committed
Remove threading from bots
1 parent 8644394 commit fbe42eb

File tree

3 files changed

+124
-83
lines changed

3 files changed

+124
-83
lines changed

rlbot/interface.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -155,18 +155,18 @@ def run_after_connect(
155155
"Took too long to connect to the RLBot executable!"
156156
) from e
157157

158-
def connect_and_run(
158+
def connect(
159159
self,
160160
wants_match_communcations: bool,
161161
wants_ball_predictions: bool,
162162
close_after_match: bool = True,
163-
only_wait_for_ready: bool = False,
164163
rlbot_server_port: int = RLBOT_SERVER_PORT,
165164
):
166165
"""
167-
Connects to the socket and begins a loop that reads messages and calls any handlers
168-
that have been registered. Connect and run are combined into a single method because
169-
currently bad things happen if the buffer is allowed to fill up.
166+
Connects to the socket and sends the connection settings.
167+
168+
NOTE: Bad things happen if the buffer is allowed to fill up. Ensure
169+
`handle_incoming_messages` is called frequently enough to prevent this.
170170
"""
171171
self.socket.settimeout(self.connection_timeout)
172172
for _ in range(int(self.connection_timeout * 10)):
@@ -196,6 +196,26 @@ def connect_and_run(
196196
).pack()
197197
self.send_bytes(flatbuffer, SocketDataType.CONNECTION_SETTINGS)
198198

199+
def connect_and_run(
200+
self,
201+
wants_match_communcations: bool,
202+
wants_ball_predictions: bool,
203+
close_after_match: bool = True,
204+
only_wait_for_ready: bool = False,
205+
rlbot_server_port: int = RLBOT_SERVER_PORT,
206+
):
207+
"""
208+
Connects to the socket and begins a loop that reads messages and calls any handlers
209+
that have been registered. Connect and run are combined into a single method because
210+
currently bad things happen if the buffer is allowed to fill up.
211+
"""
212+
self.connect(
213+
wants_match_communcations,
214+
wants_ball_predictions,
215+
close_after_match,
216+
rlbot_server_port,
217+
)
218+
199219
incoming_message = read_from_socket(self.socket)
200220
self.handle_incoming_message(incoming_message)
201221

@@ -204,11 +224,14 @@ def connect_and_run(
204224
else:
205225
self.handle_incoming_messages()
206226

207-
def handle_incoming_messages(self):
227+
def handle_incoming_messages(self, set_nonblocking_after_recv: bool = False):
208228
try:
209229
while self._should_continue:
210230
incoming_message = read_from_socket(self.socket)
211231

232+
if set_nonblocking_after_recv:
233+
self.socket.setblocking(False)
234+
212235
try:
213236
self.handle_incoming_message(incoming_message)
214237
except flat.InvalidFlatbuffer as e:
@@ -224,6 +247,8 @@ def handle_incoming_messages(self):
224247
incoming_message.type.name,
225248
e,
226249
)
250+
except BlockingIOError:
251+
raise BlockingIOError
227252
except:
228253
self.logger.error("Socket manager disconnected unexpectedly!")
229254

rlbot/managers/bot.py

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
from threading import Thread, Event
32
from traceback import print_exc
43
from typing import Optional
54

@@ -29,11 +28,8 @@ class Bot:
2928
_has_match_settings = False
3029
_has_field_info = False
3130

32-
_latest_packet = flat.GameTickPacket()
31+
_latest_packet: Optional[flat.GameTickPacket] = None
3332
_lastest_prediction = flat.BallPrediction()
34-
_packet_event = Event()
35-
_packet_thread = None
36-
_run_packet_thread = True
3733

3834
def __init__(self):
3935
spawn_id = os.environ.get("RLBOT_SPAWN_IDS")
@@ -55,9 +51,6 @@ def __init__(self):
5551
)
5652
self._game_interface.packet_handlers.append(self._handle_packet)
5753

58-
self._packet_thread = Thread(target=self._packet_processor, daemon=True)
59-
self._packet_thread.start()
60-
6154
self.renderer = Renderer(self._game_interface)
6255

6356
def _initialize_agent(self):
@@ -100,9 +93,8 @@ def _handle_ball_prediction(self, ball_prediction: flat.BallPrediction):
10093

10194
def _handle_packet(self, packet: flat.GameTickPacket):
10295
self._latest_packet = packet
103-
self._packet_event.set()
10496

105-
def _packet_preprocess(self, packet: flat.GameTickPacket) -> bool:
97+
def _packet_processor(self, packet: flat.GameTickPacket):
10698
if (
10799
self.index == -1
108100
or len(packet.players) <= self.index
@@ -121,7 +113,7 @@ def _packet_preprocess(self, packet: flat.GameTickPacket) -> bool:
121113
self.logger.error(
122114
"Multiple bots in the game, please set RLBOT_SPAWN_IDS"
123115
)
124-
return False
116+
return
125117

126118
player_index = i
127119
self.index = player_index
@@ -132,37 +124,19 @@ def _packet_preprocess(self, packet: flat.GameTickPacket) -> bool:
132124
break
133125

134126
if self.index == -1:
135-
return False
136-
137-
return True
138-
139-
def _packet_processor(self):
140-
while self._run_packet_thread:
141-
self._packet_event.wait()
142-
143-
# if the thread was unblocked,
144-
# but it we're not supposed to be running,
145-
# then exit
146-
if not self._run_packet_thread:
147127
return
148128

149-
self.ball_prediction = self._lastest_prediction
150-
packet: flat.GameTickPacket = self._latest_packet
129+
self.ball_prediction = self._lastest_prediction
151130

152-
self._packet_event.clear()
153-
154-
if not self._packet_preprocess(packet):
155-
continue
156-
157-
try:
158-
controller = self.get_output(packet)
159-
except Exception as e:
160-
self.logger.error("Bot %s returned an error to RLBot: %s", self.name, e)
161-
print_exc()
162-
continue
131+
try:
132+
controller = self.get_output(packet)
133+
except Exception as e:
134+
self.logger.error("Bot %s returned an error to RLBot: %s", self.name, e)
135+
print_exc()
136+
return
163137

164-
player_input = flat.PlayerInput(self.index, controller)
165-
self._game_interface.send_player_input(player_input)
138+
player_input = flat.PlayerInput(self.index, controller)
139+
self._game_interface.send_player_input(player_input)
166140

167141
def run(
168142
self,
@@ -172,15 +146,43 @@ def run(
172146
rlbot_server_port = int(os.environ.get("RLBOT_SERVER_PORT", 23234))
173147

174148
try:
175-
self._game_interface.connect_and_run(
149+
self._game_interface.connect(
176150
wants_match_communcations,
177151
wants_ball_predictions,
178152
rlbot_server_port=rlbot_server_port,
179153
)
180-
finally:
181-
self._run_packet_thread = False
182-
self._packet_event.set()
183154

155+
# custom message handling logic
156+
# this reads all data in the socket until there's no more immediately available
157+
# checks if there was a GameTickPacket in the data, and if so, processes it
158+
# then sets the socket to non-blocking and waits for more data
159+
# if there was no GameTickPacket, it sets to blocking and waits for more data
160+
while True:
161+
try:
162+
self._game_interface.handle_incoming_messages(True)
163+
164+
# a clean exit means that the socket was closed
165+
break
166+
except BlockingIOError:
167+
# the socket was still open,
168+
# but we don't know if data was read
169+
pass
170+
171+
# check data was read that needs to be processed
172+
if self._latest_packet is None:
173+
# there's no data we need to process
174+
# data is coming, but we haven't gotten it yet - wait for it
175+
# after `handle_incoming_messages` gets it's first message,
176+
# it will set the socket back to non-blocking on its own
177+
# that will ensure that `BlockingIOError` gets raised
178+
# when it's done reading the next batch of messages
179+
self._game_interface.socket.setblocking(True)
180+
continue
181+
182+
# process the packet that we got
183+
self._packet_processor(self._latest_packet)
184+
self._latest_packet = None
185+
finally:
184186
self.retire()
185187
del self._game_interface
186188

rlbot/managers/hivemind.py

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from logging import Logger
22
import os
3-
from threading import Event, Thread
43
from traceback import print_exc
54
from typing import Optional
65

@@ -33,9 +32,6 @@ class Hivemind:
3332

3433
_latest_packet = flat.GameTickPacket()
3534
_lastest_prediction = flat.BallPrediction()
36-
_packet_event = Event()
37-
_packet_thread = None
38-
_run_packet_thread = True
3935

4036
def __init__(self):
4137
spawn_ids = os.environ.get("RLBOT_SPAWN_IDS")
@@ -57,9 +53,6 @@ def __init__(self):
5753
)
5854
self._game_interface.packet_handlers.append(self._handle_packet)
5955

60-
self._packet_thread = Thread(target=self._packet_processor, daemon=True)
61-
self._packet_thread.start()
62-
6356
self.renderer = Renderer(self._game_interface)
6457

6558
def _initialize_agent(self):
@@ -103,9 +96,8 @@ def _handle_ball_prediction(self, ball_prediction: flat.BallPrediction):
10396

10497
def _handle_packet(self, packet: flat.GameTickPacket):
10598
self._latest_packet = packet
106-
self._packet_event.set()
10799

108-
def _packet_preprocess(self, packet: flat.GameTickPacket) -> bool:
100+
def _packet_processor(self, packet: flat.GameTickPacket):
109101
if len(self.indicies) != len(self.spawn_ids) or any(
110102
packet.players[i].spawn_id not in self.spawn_ids for i in self.indicies
111103
):
@@ -116,45 +108,67 @@ def _packet_preprocess(self, packet: flat.GameTickPacket) -> bool:
116108
]
117109

118110
if len(self.indicies) != len(self.spawn_ids):
119-
return False
120-
121-
return True
122-
123-
def _packet_processor(self):
124-
while self._run_packet_thread:
125-
self._packet_event.wait()
111+
return
126112

127-
self.ball_prediction = self._lastest_prediction
128-
packet = self._latest_packet
113+
self.ball_prediction = self._lastest_prediction
129114

130-
self._packet_event.clear()
131-
132-
if not self._packet_preprocess(self._latest_packet):
133-
continue
134-
135-
try:
136-
controller = self.get_outputs(packet)
137-
except Exception as e:
138-
self._logger.error(
139-
"Hivemind (with %s) returned an error to RLBot: %s", self.names, e
140-
)
141-
print_exc()
142-
continue
115+
try:
116+
controller = self.get_outputs(packet)
117+
except Exception as e:
118+
self._logger.error(
119+
"Hivemind (with %s) returned an error to RLBot: %s", self.names, e
120+
)
121+
print_exc()
122+
return
143123

144-
for index, controller in controller.items():
145-
player_input = flat.PlayerInput(index, controller)
146-
self._game_interface.send_player_input(player_input)
124+
for index, controller in controller.items():
125+
player_input = flat.PlayerInput(index, controller)
126+
self._game_interface.send_player_input(player_input)
147127

148128
def run(
149129
self,
150130
wants_match_communcations: bool = True,
151131
wants_ball_predictions: bool = True,
152132
):
133+
rlbot_server_port = int(os.environ.get("RLBOT_SERVER_PORT", 23234))
134+
153135
try:
154-
self._game_interface.connect_and_run(
136+
self._game_interface.connect(
155137
wants_match_communcations,
156138
wants_ball_predictions,
139+
rlbot_server_port=rlbot_server_port,
157140
)
141+
142+
# custom message handling logic
143+
# this reads all data in the socket until there's no more immediately available
144+
# checks if there was a GameTickPacket in the data, and if so, processes it
145+
# then sets the socket to non-blocking and waits for more data
146+
# if there was no GameTickPacket, it sets to blocking and waits for more data
147+
while True:
148+
try:
149+
self._game_interface.handle_incoming_messages(True)
150+
151+
# a clean exit means that the socket was closed
152+
break
153+
except BlockingIOError:
154+
# the socket was still open,
155+
# but we don't know if data was read
156+
pass
157+
158+
# check data was read that needs to be processed
159+
if self._latest_packet is None:
160+
# there's no data we need to process
161+
# data is coming, but we haven't gotten it yet - wait for it
162+
# after `handle_incoming_messages` gets it's first message,
163+
# it will set the socket back to non-blocking on its own
164+
# that will ensure that `BlockingIOError` gets raised
165+
# when it's done reading the next batch of messages
166+
self._game_interface.socket.setblocking(True)
167+
continue
168+
169+
# process the packet that we got
170+
self._packet_processor(self._latest_packet)
171+
self._latest_packet = None
158172
finally:
159173
self.retire()
160174
del self._game_interface

0 commit comments

Comments
 (0)