def _setup_exercise(game_interface: GameInterface, ex: Exercise, seed: int) -> Optional[Result]: """ Set the game state. Only returns a result if there was an error in ex.setup() """ rng = random.Random() rng.seed(seed) try: game_state = ex.setup(rng) except Exception as e: return Result(ex, seed, FailDueToExerciseException(e, traceback.format_exc())) game_interface.set_game_state(game_state)
def arrange_in_ground_circle(drones: List[Drone], game_interface: GameInterface, radius: float, radian_offset: float): if len(drones) == 0: return car_states = {} radian_spacing = 2 * math.pi / len(drones) for index, drone in enumerate(drones): progress = index * radian_spacing + radian_offset target = Vec3(radius * math.sin(progress), radius * math.cos(progress), 1000) car_states[drone.index] = CarState( Physics(location=Vector3(target.x, target.y, 50), velocity=Vector3(0, 0, 0), rotation=Rotator(0, -progress, 0))) game_interface.set_game_state(GameState(cars=car_states))
def run_state_setting(my_queue: Queue): """Controls state setting for tests.""" message = my_queue.get() if message != Message.READY: raise Exception(f"Got {message} instead of READY") logger = get_logger("UTSS") # Unit Test State Setting game_interface = GameInterface(logger) game_interface.load_interface() game_interface.wait_until_loaded() logger.info("Running!") # Get the first GameState. game_state, message = my_queue.get() game_interface.set_game_state(game_state) while True: while my_queue.qsize() == 0: # Sleep to prevent reset-spamming. time.sleep(0.1) if not keyboard.is_pressed(RESET_KEY): continue # Reset the GameState. logger.info("Resetting test.") game_interface.set_game_state(game_state) # Receive GameState. logger.info("Setting new test.") game_state, message = my_queue.get() if message == Message.DONE: print('Thread 2 closing.') exit() game_interface.set_game_state(game_state)
class PythonHivemind(BotHelperProcess): # Terminology: # hivemind - main process controlling the drones. # drone - a bot under the hivemind's control. # Path to the executable. NOT USED BY PYTHON HIVEMINDS! exec_path = None def __init__(self, agent_metadata_queue, quit_event, options): super().__init__(agent_metadata_queue, quit_event, options) # Sets up the logger. The string is the name of your hivemind. # This is configured inside your config file under hivemind_name. self.logger = get_logger(options['name']) # Give a warning if exec_path was given. if self.exec_path is not None: self.logger.warning( "An exec_path was given, but this is a PythonHivemind, and so it is ignored." ) self.logger.warning( " Try subclassing SubprocessHivemind if you want to use an executable." ) # The game interface is how you get access to things # like ball prediction, the game tick packet, or rendering. self.game_interface = GameInterface(self.logger) # drone_indices is a set of bot indices # which requested this hivemind with the same key. self.drone_indices = set() def try_receive_agent_metadata(self): """Adds all drones with the correct key to our set of running indices.""" while not self.metadata_queue.empty(): # Tries to get the next agent from the queue. single_agent_metadata: AgentMetadata = self.metadata_queue.get( timeout=1.0) # Adds the index to the drone_indices. self.drone_indices.add(single_agent_metadata.index) def start(self): """Starts the BotHelperProcess - Hivemind.""" # Prints an activation message into the console. # This lets you know that the process is up and running. self.logger.debug("Hello, world!") # Loads game interface. self.game_interface.load_interface() # Collect drone indices that requested a helper process with our key. self.logger.info("Collecting drones; give me a moment.") self.try_receive_agent_metadata() self.logger.info("Ready to go!") # Runs the game loop where the hivemind will spend the rest of its time. self.__game_loop() def __game_loop(self): """ The bot hivemind will stay in this loop for the whole game. This is where the initialize_hive and get_outputs functions are called. """ # Creating ball prediction and field info objects to later update in wrapper methods. self._ball_prediction = BallPrediction() self._field_info = FieldInfoPacket() self.game_interface.update_field_info_packet(self._field_info) # Wrapper for renderer. self.renderer: RenderingManager = self.game_interface.renderer # Create packet object. packet = GameTickPacket() # Uses one of the drone indices as a key. key = next(iter(self.drone_indices)) self.game_interface.fresh_live_data_packet(packet, 20, key) # Initialization step for your hivemind. self.initialize_hive(packet) while not self.quit_event.is_set(): try: # Updating the packet. self.game_interface.fresh_live_data_packet(packet, 20, key) # Get outputs from hivemind for each bot. # Outputs are expected to be a Dict[int, PlayerInput] outputs = self.get_outputs(packet) if outputs is None: self.logger.error("No outputs were returned.") self.logger.error( " Try putting `return {i: PlayerInput() for i in self.drone_indices}`" ) self.logger.error(" in `get_outputs()` to troubleshoot.") continue if len(outputs) < len(self.drone_indices): self.logger.error("Not enough outputs were given.") elif len(outputs) > len(self.drone_indices): self.logger.error("Too many outputs were given.") # Send the drone inputs to the drones. for index in outputs: if index not in self.drone_indices: self.logger.error( "Tried to send output to bot index not in drone_indices." ) continue output = outputs[index] self.game_interface.update_player_input(output, index) except Exception: traceback.print_exc() # Override these methods: def initialize_hive(self, packet: GameTickPacket) -> None: """ Override this method if you want to have an initialization step for your hivemind. """ pass def get_outputs(self, packet: GameTickPacket) -> Dict[int, PlayerInput]: """Where all the logic of your hivemind gets its input and returns its outputs for each drone. Use self.drone_indices to access the set of bot indices your hivemind controls. Arguments: packet {GameTickPacket} -- see https://github.com/RLBot/RLBot/wiki/Input-and-Output-Data-(current) Returns: Dict[int, PlayerInput] -- A dictionary with drone indices as keys and corresponding PlayerInputs as values. """ return {index: PlayerInput() for index in self.drone_indices} # Wrapper methods to make them the same as if you were making a normal python bot: def get_ball_prediction_struct(self) -> BallPrediction: self.game_interface.update_ball_prediction(self._ball_prediction) return self._ball_prediction def get_field_info(self) -> FieldInfoPacket: # Field info does not need to be updated. return self._field_info def get_match_settings(self) -> MatchSettings: return self.game_interface.get_match_settings() def set_game_state(self, game_state: GameState) -> None: self.game_interface.set_game_state(game_state)
class BotManager: def __init__(self, terminate_request_event, termination_complete_event, reload_request_event, bot_configuration, name, team, index, agent_class_wrapper, agent_metadata_queue, match_config: MatchConfig, matchcomms_root: URL): """ :param terminate_request_event: an Event (multiprocessing) which will be set from the outside when the program is trying to terminate :param termination_complete_event: an Event (multiprocessing) which should be set from inside this class when termination has completed successfully :param reload_request_event: an Event (multiprocessing) which will be set from the outside to force a reload of the agent :param reload_complete_event: an Event (multiprocessing) which should be set from inside this class when reloading has completed successfully :param bot_configuration: parameters which will be passed to the bot's constructor :param name: name which will be passed to the bot's constructor. Will probably be displayed in-game. :param team: 0 for blue team or 1 for orange team. Will be passed to the bot's constructor. :param index: The player index, i.e. "this is player number <index>". Will be passed to the bot's constructor. Can be used to pull the correct data corresponding to the bot's car out of the game tick packet. :param agent_class_wrapper: The ExternalClassWrapper object that can be used to load and reload the bot :param agent_metadata_queue: a Queue (multiprocessing) which expects to receive AgentMetadata once available. :param match_config: Describes the match that is being played. :param matchcomms_root: The server to connect to if you want to communicate to other participants in the match. """ self.terminate_request_event = terminate_request_event self.termination_complete_event = termination_complete_event self.reload_request_event = reload_request_event self.bot_configuration = bot_configuration self.name = name self.team = team self.index = index self.agent_class_wrapper = agent_class_wrapper self.agent_metadata_queue = agent_metadata_queue self.logger = get_logger('bot' + str(self.index)) self.game_interface = GameInterface(self.logger) self.last_chat_time = time.time() self.chat_counter = 0 self.reset_chat_time = True self.game_tick_packet = None self.bot_input = None self.ball_prediction = None self.rigid_body_tick = None self.match_config = match_config self.matchcomms_root = matchcomms_root self.last_message_index = 0 self.agent = None self.agent_class_file = None self.last_module_modification_time = 0 def send_quick_chat_from_agent(self, team_only, quick_chat): """ Passes the agents quick chats to the game, and also to other python bots. This does perform limiting. You are limited to 5 quick chats in a 2 second period starting from the first chat. This means you can spread your chats out to be even within that 2 second period. You could spam them in the first little bit but then will be throttled. """ # Send the quick chat to the game rlbot_status = send_quick_chat_flat(self.game_interface, self.index, self.team, team_only, quick_chat) if rlbot_status == RLBotCoreStatus.QuickChatRateExceeded: self.logger.debug('quick chat disabled') def load_agent(self): """ Loads and initializes an agent using instance variables, registers for quick chat and sets render functions. :return: An instance of an agent, and the agent class file. """ agent_class = self.agent_class_wrapper.get_loaded_class() self.agent = agent_class(self.name, self.team, self.index) self.agent.init_match_config(self.match_config) self.agent.load_config( self.bot_configuration.get_header("Bot Parameters")) self.update_metadata_queue() self.set_render_manager() self.agent_class_file = self.agent_class_wrapper.python_file self.agent._register_quick_chat(self.send_quick_chat_from_agent) self.agent._register_field_info(self.get_field_info) self.agent._register_set_game_state(self.set_game_state) self.agent._register_ball_prediction(self.get_ball_prediction) self.agent._register_ball_prediction_struct( self.get_ball_prediction_struct) self.agent._register_get_rigid_body_tick(self.get_rigid_body_tick) self.agent._register_match_settings_func(self.get_match_settings) self.agent.matchcomms_root = self.matchcomms_root while not self.is_valid_field_info(): time.sleep(0.1) # Once all engine setup is done, do the agent-specific initialization, if any: self.agent.initialize_agent() def set_render_manager(self): """ Sets the render manager for the agent. :param agent: An instance of an agent. """ rendering_manager = self.game_interface.renderer.get_rendering_manager( self.index, self.team) self.agent._set_renderer(rendering_manager) def update_metadata_queue(self): """ Adds a new instance of AgentMetadata into the `agent_metadata_queue` using `agent` data. :param agent: An instance of an agent. """ pids = {os.getpid(), *self.agent.get_extra_pids()} helper_process_request = self.agent.get_helper_process_request() self.agent_metadata_queue.put( AgentMetadata(self.index, self.name, self.team, pids, helper_process_request)) def reload_agent(self): """ Reloads the agent. Can throw exceptions. External classes should use reload_event.set() instead. """ self.logger.info('Reloading Agent: ' + self.agent.name) self.agent_class_wrapper.reload() old_agent = self.agent self.load_agent() self.retire_agent( old_agent) # We do this after load_agent as load_agent might fail. def run(self): """ Loads interface for RLBot, prepares environment and agent, and calls the update for the agent. """ self.logger.debug('initializing agent') self.game_interface.load_interface() self.prepare_for_run() # Create Ratelimiter rate_limit = rate_limiter.RateLimiter( GAME_TICK_PACKET_POLLS_PER_SECOND) last_tick_game_time = None # What the tick time of the last observed tick was last_call_real_time = datetime.now() # When we last called the Agent # Get bot module self.load_agent() self.last_module_modification_time = self.check_modification_time( os.path.dirname(self.agent_class_file)) # Run until main process tells to stop, or we detect Ctrl+C try: while not self.terminate_request_event.is_set(): self.pull_data_from_game() # game_tick_packet = self.game_interface.get # Read from game data shared memory # Run the Agent only if the game_info has updated. tick_game_time = self.get_game_time() should_call_while_paused = datetime.now( ) - last_call_real_time >= MAX_AGENT_CALL_PERIOD if tick_game_time != last_tick_game_time or should_call_while_paused: last_tick_game_time = tick_game_time last_call_real_time = datetime.now() # Reload the Agent if it has been modified or if reload is requested from outside. if self.agent.is_hot_reload_enabled(): self.hot_reload_if_necessary() try: chat_messages = self.game_interface.receive_chat( self.index, self.team, self.last_message_index) for i in range(0, chat_messages.MessagesLength()): message = chat_messages.Messages(i) if len(self.match_config.player_configs ) > message.PlayerIndex(): self.agent.handle_quick_chat( index=message.PlayerIndex(), team=self.match_config.player_configs[ message.PlayerIndex()].team, quick_chat=message.QuickChatSelection()) else: self.logger.debug( f"Skipping quick chat delivery for {message.MessageIndex()} because " "we don't recognize the player index. Probably stale." ) self.last_message_index = message.MessageIndex() except EmptyDllResponse: self.logger.debug("Empty response when reading chat!") # Call agent try: self.call_agent( self.agent, self.agent_class_wrapper.get_loaded_class()) except Exception as e: self.logger.error("Call to agent failed:\n" + traceback.format_exc()) # Ratelimit here rate_limit.acquire() except KeyboardInterrupt: self.terminate_request_event.set() self.retire_agent(self.agent) # If terminated, send callback self.termination_complete_event.set() def hot_reload_if_necessary(self): try: new_module_modification_time = self.check_modification_time( os.path.dirname(self.agent_class_file)) if new_module_modification_time != self.last_module_modification_time or self.reload_request_event.is_set( ): self.reload_request_event.clear() self.last_module_modification_time = new_module_modification_time # Clear the render queue on reload. if hasattr(self.agent, 'renderer') and isinstance( self.agent.renderer, RenderingManager): self.agent.renderer.clear_all_touched_render_groups() self.reload_agent() except FileNotFoundError: self.logger.error( f"Agent file {self.agent_class_file} was not found. Will try again." ) time.sleep(0.5) except Exception: self.logger.error("Reloading the agent failed:\n" + traceback.format_exc()) time.sleep( 5 ) # Avoid burning CPU, and give the user a moment to read the log def retire_agent(self, agent): # Shut down the bot by calling cleanup functions. if hasattr(agent, 'retire'): try: agent.retire() except Exception as e: self.logger.error("Retiring the agent failed:\n" + traceback.format_exc()) if hasattr(agent, 'renderer') and isinstance(agent.renderer, RenderingManager): agent.renderer.clear_all_touched_render_groups() # Zero out the inputs, so it's more obvious that the bot has stopped. self.game_interface.update_player_input(PlayerInput(), self.index) # Don't trust the agent to shut down its own client in retire(). if agent._matchcomms is not None: agent._matchcomms.close() @staticmethod def check_modification_time(directory): files = [f for f in glob.glob(directory + "/**/*.py", recursive=True)] max_modification_time = 0 for file in files: mtime = os.stat(file).st_mtime if mtime > max_modification_time: max_modification_time = mtime return max_modification_time def get_field_info(self): return self.game_interface.get_field_info() def get_rigid_body_tick(self): """Get the most recent state of the physics engine.""" return self.game_interface.update_rigid_body_tick(self.rigid_body_tick) def set_game_state(self, game_state: GameState) -> None: self.game_interface.set_game_state(game_state) def get_ball_prediction(self): return self.game_interface.get_ball_prediction() def get_match_settings(self) -> MatchSettings: return self.game_interface.get_match_settings() def get_ball_prediction_struct(self): raise NotImplementedError def prepare_for_run(self): raise NotImplementedError def call_agent(self, agent: BaseAgent, agent_class): raise NotImplementedError def get_game_time(self): raise NotImplementedError def pull_data_from_game(self): raise NotImplementedError def is_valid_field_info(self) -> bool: """Checks if the contents of field info are valid.""" raise NotImplementedError
class Observer(): def __init__(self): self.game_interface = GameInterface(get_logger("observer")) self.game_interface.load_interface() self.game_interface.wait_until_loaded() self.game_interface.set_game_state(GameState(console_commands=[f'Set WorldInfo WorldGravityZ {WORLD_GRAVITY}'])) self.main() def main(self): # Create packet packet = GameTickPacket() last_game_time = 0.0 while True: # Update packet self.game_interface.update_live_data_packet(packet) game_time = packet.game_info.seconds_elapsed # Sleep until a new packet is received. if last_game_time == game_time: time.sleep(0.001) else: if packet.game_info.is_round_active: # Renders ball prediction. ball_prediction = BallPrediction() self.game_interface.update_ball_prediction(ball_prediction) self.game_interface.renderer.begin_rendering() self.game_interface.renderer.draw_polyline_3d([step.physics.location for step in ball_prediction.slices[::10]], self.game_interface.renderer.cyan()) self.game_interface.renderer.end_rendering() car_states = {} for i in range(packet.num_cars): car = packet.game_cars[i] if STICK != 0 and car.has_wheel_contact: # Makes cars stick by adding a velocity downwards. pitch = car.physics.rotation.pitch yaw = car.physics.rotation.yaw roll = car.physics.rotation.roll CP = cos(pitch) SP = sin(pitch) CY = cos(yaw) SY = sin(yaw) CR = cos(roll) SR = sin(roll) x = car.physics.velocity.x - STICK*(-CR * CY * SP - SR * SY) y = car.physics.velocity.y - STICK*(-CR * SY * SP + SR * CY) z = car.physics.velocity.z - STICK*(CP * CR) car_states.update({i: CarState(physics=Physics(velocity=Vector3(x,y,z)))}) if packet.game_info.is_kickoff_pause and round(packet.game_ball.physics.location.z) != KICKOFF_BALL_HEIGHT: # Places the ball in the air on kickoff. ball_state = BallState(Physics(location=Vector3(z=KICKOFF_BALL_HEIGHT), velocity=Vector3(0,0,0))) if len(car_states) > 0: game_state = GameState(ball=ball_state, cars=car_states) else: game_state = GameState(ball=ball_state) else: if len(car_states) > 0: game_state = GameState(cars=car_states) else: game_state = GameState() # Uses state setting to set the game state. self.game_interface.set_game_state(game_state)
class BotManager: def __init__(self, terminate_request_event, termination_complete_event, reload_request_event, bot_configuration, name, team, index, agent_class_wrapper, agent_metadata_queue, match_config: MatchConfig, matchcomms_root: URL, spawn_id: int): """ :param terminate_request_event: an Event (multiprocessing) which will be set from the outside when the program is trying to terminate :param termination_complete_event: an Event (multiprocessing) which should be set from inside this class when termination has completed successfully :param reload_request_event: an Event (multiprocessing) which will be set from the outside to force a reload of the agent :param reload_complete_event: an Event (multiprocessing) which should be set from inside this class when reloading has completed successfully :param bot_configuration: parameters which will be passed to the bot's constructor :param name: name which will be passed to the bot's constructor. Will probably be displayed in-game. :param team: 0 for blue team or 1 for orange team. Will be passed to the bot's constructor. :param index: The player index, i.e. "this is player number <index>". Will be passed to the bot's constructor. Can be used to pull the correct data corresponding to the bot's car out of the game tick packet. :param agent_class_wrapper: The ExternalClassWrapper object that can be used to load and reload the bot :param agent_metadata_queue: a Queue (multiprocessing) which expects to receive AgentMetadata once available. :param match_config: Describes the match that is being played. :param matchcomms_root: The server to connect to if you want to communicate to other participants in the match. :param spawn_id: The identifier we expect to see in the game tick packet at our player index. If it does not match, then we will force the agent to retire. Pass None to opt out of this behavior. """ self.terminate_request_event = terminate_request_event self.termination_complete_event = termination_complete_event self.reload_request_event = reload_request_event self.bot_configuration = bot_configuration self.name = name self.team = team self.index = index self.agent_class_wrapper = agent_class_wrapper self.agent_metadata_queue = agent_metadata_queue self.logger = get_logger('bot' + str(self.index)) self.game_interface = GameInterface(self.logger) self.last_chat_time = time.time() self.chat_counter = 0 self.reset_chat_time = True self.game_tick_packet = None self.bot_input = None self.ball_prediction = None self.rigid_body_tick = None self.match_config = match_config self.matchcomms_root = matchcomms_root self.last_message_index = 0 self.agent = None self.agent_class_file = None self.last_module_modification_time = 0 self.scan_last = 0 self.scan_temp = 0 self.file_iterator = None self.maximum_tick_rate_preference = bot_configuration.get( BOT_CONFIG_MODULE_HEADER, MAXIMUM_TICK_RATE_PREFERENCE_KEY) self.spawn_id = spawn_id self.spawn_id_seen = False self.counter = 0 def send_quick_chat_from_agent(self, team_only, quick_chat): """ Passes the agents quick chats to the game, and also to other python bots. This does perform limiting. You are limited to 5 quick chats in a 2 second period starting from the first chat. This means you can spread your chats out to be even within that 2 second period. You could spam them in the first little bit but then will be throttled. """ # Send the quick chat to the game rlbot_status = send_quick_chat_flat(self.game_interface, self.index, self.team, team_only, quick_chat) if rlbot_status == RLBotCoreStatus.QuickChatRateExceeded: self.logger.debug('quick chat disabled') def load_agent(self) -> Tuple[BaseAgent, Path]: """ Loads and initializes an agent using instance variables, registers for quick chat and sets render functions. :return: An instance of an agent, and the agent class file. """ agent_class = self.agent_class_wrapper.get_loaded_class() self.agent = agent_class(self.name, self.team, self.index) self.agent._set_spawn_id(self.spawn_id) self.agent.matchcomms_root = self.matchcomms_root self.agent.init_match_config(self.match_config) self.agent.load_config( self.bot_configuration.get_header("Bot Parameters")) self.update_metadata_queue() self.set_render_manager() self.agent_class_file = self.agent_class_wrapper.python_file self.agent._register_quick_chat(self.send_quick_chat_from_agent) self.agent._register_field_info(self.get_field_info) self.agent._register_set_game_state(self.set_game_state) self.agent._register_ball_prediction(self.get_ball_prediction) self.agent._register_ball_prediction_struct( self.get_ball_prediction_struct) self.agent._register_get_rigid_body_tick(self.get_rigid_body_tick) self.agent._register_match_settings_func(self.get_match_settings) # Once all engine setup is done, do the agent-specific initialization, if any: self.agent.initialize_agent() return self.agent, self.agent_class_file def set_render_manager(self): """ Sets the render manager for the agent. :param agent: An instance of an agent. """ rendering_manager = self.game_interface.renderer.get_rendering_manager( self.index, self.team) self.agent._set_renderer(rendering_manager) def update_metadata_queue(self): """ Adds a new instance of AgentMetadata into the `agent_metadata_queue` using `agent` data. :param agent: An instance of an agent. """ pids = {os.getpid(), *self.agent.get_extra_pids()} helper_process_request = self.agent.get_helper_process_request() self.agent_metadata_queue.put( AgentMetadata(self.index, self.name, self.team, pids, helper_process_request)) def reload_agent(self): """ Reloads the agent. Can throw exceptions. External classes should use reload_event.set() instead. """ self.logger.info('Reloading Agent: ' + self.agent.name) self.agent_class_wrapper.reload() old_agent = self.agent self.load_agent() self.retire_agent( old_agent) # We do this after load_agent as load_agent might fail. def wait_for_full_data(self): for i in range(10): match_settings = self.get_match_settings() if match_settings is not None and self.get_field_info( ).num_goals > 0: return time.sleep(0.1) self.logger.error( "WARNING: failed to get full match data before starting bot!") def run(self): """ Loads interface for RLBot, prepares environment and agent, and calls the update for the agent. """ self.logger.debug('initializing agent') self.game_interface.load_interface() self.wait_for_full_data() self.prepare_for_run() if self.match_config is None: match_settings = self.game_interface.get_match_settings() self.match_config = MatchConfig.from_match_settings_flatbuffer( match_settings) last_tick_game_time = 0 # What the tick time of the last observed tick was last_call_real_time = datetime.now() # When we last called the Agent frame_urgency = 0 # If the bot is getting called more than its preferred max rate, urgency will go negative. # Get bot module self.load_agent() self.last_module_modification_time = self.check_modification_time( os.path.dirname(self.agent_class_file)) # Run until main process tells to stop, or we detect Ctrl+C try: while not self.terminate_request_event.is_set(): self.pull_data_from_game() # Run the Agent only if the game_info has updated. tick_game_time = self.get_game_time() now = datetime.now() should_call_while_paused = now - last_call_real_time >= MAX_AGENT_CALL_PERIOD or self.match_config.enable_lockstep if frame_urgency < 1 / self.maximum_tick_rate_preference: # Urgency increases every frame, but don't let it build up a large backlog frame_urgency += tick_game_time - last_tick_game_time if tick_game_time != last_tick_game_time and frame_urgency >= 0 or should_call_while_paused: last_call_real_time = now # Urgency decreases when a tick is processed. if frame_urgency > 0: frame_urgency -= 1 / self.maximum_tick_rate_preference self.perform_tick() self.counter += 1 last_tick_game_time = tick_game_time if self.spawn_id is not None: packet_spawn_id = self.get_spawn_id() if self.spawn_id_seen: if packet_spawn_id != self.spawn_id: self.logger.warn( f"The bot's spawn id {self.spawn_id} does not match the one in the packet " f"{packet_spawn_id}, retiring!") break elif packet_spawn_id == self.spawn_id and self.game_tick_packet.game_info.is_round_active: self.spawn_id_seen = True except KeyboardInterrupt: self.terminate_request_event.set() self.retire_agent(self.agent) # If terminated, send callback self.termination_complete_event.set() def perform_tick(self): # Reload the Agent if it has been modified or if reload is requested from outside. # But only do that every 20th tick. if self.agent.is_hot_reload_enabled() and self.counter % 20 == 1: self.hot_reload_if_necessary() try: chat_messages = self.game_interface.receive_chat( self.index, self.team, self.last_message_index) for i in range(0, chat_messages.MessagesLength()): message = chat_messages.Messages(i) if len(self.match_config.player_configs) > message.PlayerIndex( ): self.agent.handle_quick_chat( index=message.PlayerIndex(), team=self.match_config.player_configs[ message.PlayerIndex()].team, quick_chat=message.QuickChatSelection()) else: self.logger.debug( f"Skipping quick chat delivery for {message.MessageIndex()} because " "we don't recognize the player index. Probably stale.") self.last_message_index = message.MessageIndex() except EmptyDllResponse: self.logger.debug("Empty response when reading chat!") # Call agent try: self.call_agent(self.agent, self.agent_class_wrapper.get_loaded_class()) except Exception as e: self.logger.error("Call to agent failed:\n" + traceback.format_exc()) def hot_reload_if_necessary(self): try: new_module_modification_time = self.check_modification_time( os.path.dirname(self.agent_class_file)) if new_module_modification_time != self.last_module_modification_time or self.reload_request_event.is_set( ): self.reload_request_event.clear() self.last_module_modification_time = new_module_modification_time # Clear the render queue on reload. if hasattr(self.agent, 'renderer') and isinstance( self.agent.renderer, RenderingManager): self.agent.renderer.clear_all_touched_render_groups() self.reload_agent() except FileNotFoundError: self.logger.error( f"Agent file {self.agent_class_file} was not found. Will try again." ) time.sleep(0.5) except Exception: self.logger.error("Reloading the agent failed:\n" + traceback.format_exc()) time.sleep( 5 ) # Avoid burning CPU, and give the user a moment to read the log def retire_agent(self, agent): # Shut down the bot by calling cleanup functions. if hasattr(agent, 'retire'): try: agent.retire() except Exception as e: self.logger.error("Retiring the agent failed:\n" + traceback.format_exc()) if hasattr(agent, 'renderer') and isinstance(agent.renderer, RenderingManager): agent.renderer.clear_all_touched_render_groups() # Zero out the inputs, so it's more obvious that the bot has stopped. self.game_interface.update_player_input(PlayerInput(), self.index) # Don't trust the agent to shut down its own client in retire(). if agent._matchcomms is not None: agent._matchcomms.close() def check_modification_time(self, directory, timeout_ms=1): if self.scan_last > 0 and timeout_ms is not None: stop_time = time.perf_counter_ns() + timeout_ms * 10**6 else: stop_time = None if self.file_iterator is None: self.file_iterator = glob.iglob(f"{directory}/**/*.py", recursive=True) for f in self.file_iterator: self.scan_temp = max(self.scan_temp, os.stat(f).st_mtime) if stop_time is not None and time.perf_counter_ns() > stop_time: # Timeout exceeded. The scan will pick up from here on the next call. break else: # Scan finished. Update the modification time and restart the scan: self.scan_last, self.scan_temp = self.scan_temp, 0 self.file_iterator = None return self.scan_last def get_field_info(self) -> FieldInfoPacket: field_info = FieldInfoPacket() self.game_interface.update_field_info_packet(field_info) return field_info def get_rigid_body_tick(self): """Get the most recent state of the physics engine.""" return self.game_interface.update_rigid_body_tick(self.rigid_body_tick) def set_game_state(self, game_state: GameState) -> None: self.game_interface.set_game_state(game_state) def get_ball_prediction(self): return self.game_interface.get_ball_prediction() def get_match_settings(self) -> MatchSettings: return self.game_interface.get_match_settings() def get_ball_prediction_struct(self): raise NotImplementedError def prepare_for_run(self): raise NotImplementedError def call_agent(self, agent: BaseAgent, agent_class): raise NotImplementedError def get_game_time(self): raise NotImplementedError def pull_data_from_game(self): raise NotImplementedError def get_spawn_id(self): raise NotImplementedError
class BotManager: def __init__(self, terminate_request_event, termination_complete_event, reload_request_event, bot_configuration, name, team, index, agent_class_wrapper, agent_metadata_queue, quick_chat_queue_holder): """ :param terminate_request_event: an Event (multiprocessing) which will be set from the outside when the program is trying to terminate :param termination_complete_event: an Event (multiprocessing) which should be set from inside this class when termination has completed successfully :param reload_request_event: an Event (multiprocessing) which will be set from the outside to force a reload of the agent :param reload_complete_event: an Event (multiprocessing) which should be set from inside this class when reloading has completed successfully :param bot_configuration: parameters which will be passed to the bot's constructor :param name: name which will be passed to the bot's constructor. Will probably be displayed in-game. :param team: 0 for blue team or 1 for orange team. Will be passed to the bot's constructor. :param index: The player index, i.e. "this is player number <index>". Will be passed to the bot's constructor. Can be used to pull the correct data corresponding to the bot's car out of the game tick packet. :param agent_class_wrapper: The ExternalClassWrapper object that can be used to load and reload the bot :param agent_metadata_queue: a Queue (multiprocessing) which expects to receive certain metadata about the agent once available. :param quick_chat_queue_holder: A data structure which helps the bot send and receive quickchat """ self.terminate_request_event = terminate_request_event self.termination_complete_event = termination_complete_event self.reload_request_event = reload_request_event self.bot_configuration = bot_configuration self.name = name self.team = team self.index = index self.agent_class_wrapper = agent_class_wrapper self.agent_metadata_queue = agent_metadata_queue self.logger = get_logger('bot' + str(self.index)) self.game_interface = GameInterface(self.logger) self.quick_chat_queue_holder = quick_chat_queue_holder self.last_chat_time = time.time() self.chat_counter = 0 self.reset_chat_time = True self.game_tick_packet = None self.bot_input = None def send_quick_chat_from_agent(self, team_only, quick_chat): """ Passes the agents quick chats to the other bots. This does perform limiting. You are limited to 5 quick chats in a 2 second period starting from the first chat. This means you can spread your chats out to be even within that 2 second period. You could spam them in the first little bit but then will be throttled. """ time_since_last_chat = time.time() - self.last_chat_time if not self.reset_chat_time and time_since_last_chat >= MAX_CHAT_RATE: self.reset_chat_time = True if self.reset_chat_time: self.last_chat_time = time.time() self.chat_counter = 0 self.reset_chat_time = False if self.chat_counter < MAX_CHAT_COUNT: send_quick_chat_flat(self.game_interface, self.index, self.team, team_only, quick_chat) #send_quick_chat(self.quick_chat_queue_holder, self.index, self.team, team_only, quick_chat) self.chat_counter += 1 else: self.logger.debug('quick chat disabled for %s', MAX_CHAT_RATE - time_since_last_chat) def load_agent(self): """ Loads and initializes an agent using instance variables, registers for quick chat and sets render functions. :return: An instance of an agent, and the agent class file. """ agent_class = self.agent_class_wrapper.get_loaded_class() agent = agent_class(self.name, self.team, self.index) agent.logger = self.logger agent.load_config(self.bot_configuration.get_header("Bot Parameters")) self.update_metadata_queue(agent) self.set_render_manager(agent) agent_class_file = self.agent_class_wrapper.python_file agent._register_quick_chat(self.send_quick_chat_from_agent) agent._register_field_info(self.get_field_info) agent._register_set_game_state(self.set_game_state) agent._register_ball_prediction(self.get_ball_prediction) register_for_quick_chat(self.quick_chat_queue_holder, agent.handle_quick_chat, self.terminate_request_event) # Once all engine setup is done, do the agent-specific initialization, if any: agent.initialize_agent() return agent, agent_class_file def set_render_manager(self, agent): """ Sets the render manager for the agent. :param agent: An instance of an agent. """ rendering_manager = self.game_interface.renderer.get_rendering_manager(self.index, self.team) agent._set_renderer(rendering_manager) def update_metadata_queue(self, agent): """ Adds a new instance of AgentMetadata into the `agent_metadata_queue` using `agent` data. :param agent: An instance of an agent. """ pids = {os.getpid(), *agent.get_extra_pids()} helper_process_request = agent.get_helper_process_request() self.agent_metadata_queue.put(AgentMetadata(self.index, self.name, self.team, pids, helper_process_request)) def reload_agent(self, agent, agent_class_file): """ Reloads the agent. Can throw exceptions. External classes should use reload_event.set() instead. :param agent: An instance of an agent. :param agent_class_file: The agent's class file. :return: The reloaded instance of the agent, and the agent class file. """ self.logger.info('Reloading Agent: ' + agent_class_file) self.agent_class_wrapper.reload() old_agent = agent agent, agent_class_file = self.load_agent() # Retire after the replacement initialized properly. if hasattr(old_agent, 'retire'): old_agent.retire() return agent, agent_class_file def run(self): """ Loads interface for RLBot, prepares environment and agent, and calls the update for the agent. """ self.logger.debug('initializing agent') self.game_interface.load_interface() self.prepare_for_run() # Create Ratelimiter rate_limit = rate_limiter.RateLimiter(GAME_TICK_PACKET_REFRESHES_PER_SECOND) last_tick_game_time = None # What the tick time of the last observed tick was last_call_real_time = datetime.now() # When we last called the Agent # Get bot module agent, agent_class_file = self.load_agent() last_module_modification_time = os.stat(agent_class_file).st_mtime # Run until main process tells to stop while not self.terminate_request_event.is_set(): before = datetime.now() self.pull_data_from_game() # game_tick_packet = self.game_interface.get # Read from game data shared memory # Run the Agent only if the game_info has updated. tick_game_time = self.get_game_time() should_call_while_paused = datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD if tick_game_time != last_tick_game_time or should_call_while_paused: last_tick_game_time = tick_game_time last_call_real_time = datetime.now() # Reload the Agent if it has been modified or if reload is requested from outside. try: new_module_modification_time = os.stat(agent_class_file).st_mtime if new_module_modification_time != last_module_modification_time or self.reload_request_event.is_set(): self.reload_request_event.clear() last_module_modification_time = new_module_modification_time agent, agent_class_file = self.reload_agent(agent, agent_class_file) except FileNotFoundError: self.logger.error("Agent file {} was not found. Will try again.".format(agent_class_file)) time.sleep(0.5) except Exception: self.logger.error("Reloading the agent failed:\n" + traceback.format_exc()) time.sleep(0.5) # Avoid burning CPU / logs if this starts happening constantly # Call agent try: self.call_agent(agent, self.agent_class_wrapper.get_loaded_class()) except Exception as e: self.logger.error("Call to agent failed:\n" + traceback.format_exc()) # Ratelimit here after = datetime.now() rate_limit.acquire(after - before) if hasattr(agent, 'retire'): agent.retire() # If terminated, send callback self.termination_complete_event.set() def get_field_info(self): return self.game_interface.get_field_info() def set_game_state(self, game_state): return self.game_interface.set_game_state(game_state) def get_ball_prediction(self): return self.game_interface.get_ball_prediction() def prepare_for_run(self): raise NotImplementedError def call_agent(self, agent, agent_class): raise NotImplementedError def get_game_time(self): raise NotImplementedError def pull_data_from_game(self): raise NotImplementedError
class BaseScript(RLBotRunnable): """ A convenience class for building scripts on top of. It is NOT required to use this when configuring a script. """ matchcomms_root: Optional[URL] = None def __init__(self, name): super().__init__(name) self.logger = get_logger(name) self.__key = hash("BaseScript:" + name) self.game_tick_packet = GameTickPacket() self.ball_prediction = BallPrediction() self.game_interface = GameInterface(self.logger) self.game_interface.load_interface() fake_index = random.randint( 100, 10000) # a number unlikely to conflict with bots or other scripts self.renderer = self.game_interface.renderer.get_rendering_manager( bot_index=fake_index, bot_team=2) # Get matchcomms root if provided as a command line argument. try: pos = sys.argv.index("--matchcomms-url") potential_url = urlparse(sys.argv[pos + 1]) except (ValueError, IndexError): # Missing the command line argument. pass else: if potential_url.scheme == "ws" and potential_url.netloc: self.matchcomms_root = potential_url else: raise ValueError("The matchcomms url is invalid") def get_game_tick_packet(self): """Gets the latest game tick packet immediately, without blocking.""" return self.game_interface.update_live_data_packet( self.game_tick_packet) def wait_game_tick_packet(self): """A blocking call which waits for the next new game tick packet and returns as soon as it's available. Will wait for a maximum of 30 milliseconds before giving up and returning the packet the framework already has. This is suitable for low-latency update loops.""" return self.game_interface.fresh_live_data_packet( self.game_tick_packet, 30, self.__key) def get_field_info(self): """Gets the information about the field. This does not change during a match so it only needs to be called once after the everything is loaded.""" return self.game_interface.get_field_info() def set_game_state(self, game_state: GameState): self.game_interface.set_game_state(game_state) def get_ball_prediction_struct(self) -> BallPrediction: """Fetches a prediction of where the ball will go during the next few seconds.""" return self.game_interface.update_ball_prediction(self.ball_prediction) def get_match_settings(self) -> MatchSettings: """Gets the current match settings in flatbuffer format. Useful for determining map, game mode, mutator settings, etc.""" return self.game_interface.get_match_settings() # Information about @classmethod: https://docs.python.org/3/library/functions.html#classmethod @classmethod def base_create_agent_configurations(cls) -> ConfigObject: """ This is used when initializing agent config via builder pattern. It also calls `create_agent_configurations` that can be used by BaseAgent subclasses for custom configs. :return: Returns an instance of a ConfigObject object. """ config = super().base_create_agent_configurations() location_config = config.get_header(LOCATIONS_HEADER) location_config.add_value(SCRIPT_FILE_KEY, str, description="Script's python file.") cls.create_agent_configurations(config) return config # Same as in BaseAgent. _matchcomms: Optional[MatchcommsClient] = None @property def matchcomms(self) -> MatchcommsClient: """ Gets a client to send and recieve messages to other participants in the match (e.g. bots, trainer) """ if self.matchcomms_root is None: raise ValueError( "Your bot tried to access matchcomms but matchcomms_root is None! This " "may be due to manually running a bot in standalone mode without passing the " "--matchcomms-url argument. That's a fine thing to do, and if it's safe to " "ignore matchcomms in your case then go ahead and wrap your matchcomms access " "in a try-except, or do a check first for whether matchcomms_root is None." ) if self._matchcomms is None: self._matchcomms = MatchcommsClient(self.matchcomms_root) return self._matchcomms
class BaseScript(RLBotRunnable): """ A convenience class for building scripts on top of. It is NOT required to use this when configuring a script. """ def __init__(self, name): super().__init__(name) self.logger = get_logger(name) self.__key = hash("BaseScript:" + name) self.game_tick_packet = GameTickPacket() self.ball_prediction = BallPrediction() self.game_interface = GameInterface(self.logger) self.game_interface.load_interface() fake_index = random.randint( 100, 10000) # a number unlikely to conflict with bots or other scripts self.renderer = self.game_interface.renderer.get_rendering_manager( bot_index=fake_index, bot_team=2) def get_game_tick_packet(self): """Gets the latest game tick packet immediately, without blocking.""" return self.game_interface.update_live_data_packet( self.game_tick_packet) def wait_game_tick_packet(self): """A blocking call which waits for the next new game tick packet and returns as soon as it's available. Will wait for a maximum of 30 milliseconds before giving up and returning the packet the framework already has. This is suitable for low-latency update loops.""" return self.game_interface.fresh_live_data_packet( self.game_tick_packet, 30, self.__key) def get_field_info(self): """Gets the information about the field. This does not change during a match so it only needs to be called once after the everything is loaded.""" return self.game_interface.get_field_info() def set_game_state(self, game_state: GameState): self.game_interface.set_game_state(game_state) def get_ball_prediction_struct(self) -> BallPrediction: """Fetches a prediction of where the ball will go during the next few seconds.""" return self.game_interface.update_ball_prediction(self.ball_prediction) def get_match_settings(self) -> MatchSettings: """Gets the current match settings in flatbuffer format. Useful for determining map, game mode, mutator settings, etc.""" return self.game_interface.get_match_settings() # Information about @classmethod: https://docs.python.org/3/library/functions.html#classmethod @classmethod def base_create_agent_configurations(cls) -> ConfigObject: """ This is used when initializing agent config via builder pattern. It also calls `create_agent_configurations` that can be used by BaseAgent subclasses for custom configs. :return: Returns an instance of a ConfigObject object. """ config = super().base_create_agent_configurations() location_config = config.get_header(LOCATIONS_HEADER) location_config.add_value(SCRIPT_FILE_KEY, str, description="Script's python file.") cls.create_agent_configurations(config) return config
class SetupManager: """ This class is responsible for pulling together all bits of the framework to set up a match between agents. A normal order of methods would be: connect_to_game() load_config() launch_ball_prediction() launch_quick_chat_manager() launch_bot_processes() start_match() infinite_loop() # the below two might be from another thread reload_all_agents() shut_down() """ has_started = False num_participants = None names = None teams = None python_files = None parameters = None start_match_configuration = None agent_metadata_queue = None agent_state_queue = None agent_action_queue = None extension = None sub_processes = [] def __init__(self, gym=False): self.logger = get_logger(DEFAULT_LOGGER) self.game_interface = GameInterface(self.logger) self.quick_chat_manager = QuickChatManager(self.game_interface) self.quit_event = mp.Event() self.helper_process_manager = HelperProcessManager(self.quit_event) self.bot_quit_callbacks = [] self.bot_reload_requests = [] self.agent_metadata_map = {} self.ball_prediction_process = None self.match_config: MatchConfig = None self.gym = gym def connect_to_game(self): if self.has_started: return version.print_current_release_notes() if not process_configuration.is_process_running(ROCKET_LEAGUE_PROCESS_INFO['program'], ROCKET_LEAGUE_PROCESS_INFO['program_name']): try: self.logger.info("Launching Rocket League...") webbrowser.open(f"steam://rungameid/{ROCKET_LEAGUE_PROCESS_INFO['gameid']}") except webbrowser.Error: self.logger.info( "Unable to launch Rocket League automatically. Please launch Rocket League manually to continue.") self.game_interface.inject_dll() self.game_interface.load_interface() self.agent_metadata_queue = mp.Queue() self.agent_state_queue = mp.Queue() self.agent_action_queue = mp.Queue() self.has_started = True def load_match_config(self, match_config: MatchConfig, bot_config_overrides={}): """ Loads the match config into internal data structures, which prepares us to later launch bot processes and start the match. This is an alternative to the load_config method; they accomplish the same thing. """ self.num_participants = match_config.num_players self.names = [bot.name for bot in match_config.player_configs] self.teams = [bot.team for bot in match_config.player_configs] bundles = [bot_config_overrides[index] if index in bot_config_overrides else get_bot_config_bundle(bot.config_path) if bot.config_path else None for index, bot in enumerate(match_config.player_configs)] self.python_files = [bundle.python_file if bundle else None for bundle in bundles] self.parameters = [] for index, bot in enumerate(match_config.player_configs): python_config = None if bot.rlbot_controlled: python_config = load_bot_parameters(bundles[index]) self.parameters.append(python_config) if bot.loadout_config is None and bundles[index]: looks_config = bundles[index].get_looks_config() bot.loadout_config = load_bot_appearance(looks_config, bot.team) if match_config.extension_config is not None and match_config.extension_config.python_file_path is not None: self.load_extension(match_config.extension_config.python_file_path) self.match_config = match_config self.start_match_configuration = match_config.create_match_settings() self.game_interface.start_match_configuration = self.start_match_configuration def load_config(self, framework_config: ConfigObject = None, config_location=DEFAULT_RLBOT_CONFIG_LOCATION, bot_configs=None, looks_configs=None): """ Loads the configuration into internal data structures, which prepares us to later launch bot processes and start the match. :param framework_config: A config object that indicates what bots to run. May come from parsing a rlbot.cfg. :param config_location: The location of the rlbot.cfg file, which will be used to resolve relative paths. :param bot_configs: Overrides for bot configurations. :param looks_configs: Overrides for looks configurations. """ self.logger.debug('reading the configs') # Set up RLBot.cfg if framework_config is None: framework_config = create_bot_config_layout() framework_config.parse_file(config_location, max_index=MAX_PLAYERS) if bot_configs is None: bot_configs = {} if looks_configs is None: looks_configs = {} match_config = parse_match_config(framework_config, config_location, bot_configs, looks_configs) self.load_match_config(match_config, bot_configs) def launch_ball_prediction(self): # restart, in case we have changed game mode if self.ball_prediction_process: self.ball_prediction_process.terminate() if self.start_match_configuration.game_mode == 1: # hoops prediction_util.copy_pitch_data_to_temp('hoops') elif self.start_match_configuration.game_mode == 2: # dropshot prediction_util.copy_pitch_data_to_temp('dropshot') else: prediction_util.copy_pitch_data_to_temp('soccar') self.ball_prediction_process = prediction_util.launch() def launch_bot_processes(self): self.logger.debug("Launching bot processes") self.kill_sub_processes() # Launch processes for i in range(self.num_participants): if self.start_match_configuration.player_configuration[i].rlbot_controlled: queue_holder = self.quick_chat_manager.create_queue_for_bot(i, self.teams[i]) reload_request = mp.Event() quit_callback = mp.Event() self.bot_reload_requests.append(reload_request) self.bot_quit_callbacks.append(quit_callback) if self.gym: process = mp.Process(target=SetupManager.run_agent, args=(self.quit_event, quit_callback, reload_request, self.parameters[0], str(self.start_match_configuration.player_configuration[0].name), self.teams[0], 0, self.python_files[0], self.agent_metadata_queue, (self.agent_state_queue, self.agent_action_queue), queue_holder, self.match_config)) else: process = mp.Process(target=SetupManager.run_agent, args=(self.quit_event, quit_callback, reload_request, self.parameters[i], str(self.start_match_configuration.player_configuration[i].name), self.teams[i], i, self.python_files[i], self.agent_metadata_queue, queue_holder, self.match_config)) process.start() self.sub_processes.append(process) self.logger.debug("Successfully started bot processes") def launch_quick_chat_manager(self): self.quick_chat_manager.start_manager(self.quit_event) self.logger.debug("Successfully started quick chat manager") def start_match(self): self.game_interface.start_match() self.logger.info("Match has started") def reset_game(self): car_state = CarState( Physics( location=Vector3(0,-1000, 20), velocity=Vector3(0, 0,0), rotation=Rotator(0, 0.5 * np.pi, 0), angular_velocity=Vector3(0, 0, 0)), jumped=False, double_jumped=False, boost_amount=100 ) ball_state = BallState( Physics( location=Vector3(0, 0, 20), velocity=Vector3(0, 0,0), rotation=Rotator(0, 0.5 * np.pi, 0), angular_velocity=Vector3(0, 0, 0) ) ) game_state = GameState(cars={0: car_state}, ball=ball_state) # game_state = GameState() self.game_interface.set_game_state(game_state) def infinite_loop(self): instructions = "Press 'r' to reload all agents, or 'q' to exit" self.logger.info(instructions) while not self.quit_event.is_set(): # Handle commands if msvcrt.kbhit(): command = msvcrt.getwch() if command.lower() == 'r': # r: reload self.reload_all_agents() elif command.lower() == 'q' or command == '\u001b': # q or ESC: quit self.shut_down() break # Print instructions again if a alphabet character was pressed but no command was found elif command.isalpha(): self.logger.info(instructions) self.try_recieve_agent_metadata() def try_recieve_agent_metadata(self): """ Checks whether any of the started bots have posted their AgentMetadata yet. If so, we put them on the agent_metadata_map such that we can kill their process later when we shut_down(kill_agent_process_ids=True) Returns how from how many bots we recieved metadata from. """ num_recieved = 0 while True: # will exit on queue.Empty try: single_agent_metadata = self.agent_metadata_queue.get(timeout=0.1) num_recieved += 1 self.helper_process_manager.start_or_update_helper_process(single_agent_metadata) self.agent_metadata_map[single_agent_metadata.index] = single_agent_metadata process_configuration.configure_processes(self.agent_metadata_map, self.logger) except queue.Empty: return num_recieved except Exception as ex: self.logger.error(ex) return num_recieved return num_recieved def reload_all_agents(self, quiet=False): if not quiet: self.logger.info("Reloading all agents...") for rr in self.bot_reload_requests: rr.set() def shut_down(self, time_limit=5, kill_all_pids=False, quiet=False): if not quiet: self.logger.info("Shutting Down") self.quit_event.set() end_time = datetime.now() + timedelta(seconds=time_limit) if self.ball_prediction_process: self.ball_prediction_process.terminate() # Wait for all processes to terminate before terminating main process terminated = False while not terminated: terminated = True for callback in self.bot_quit_callbacks: if not callback.is_set(): terminated = False time.sleep(0.1) if datetime.now() > end_time: self.logger.info("Taking too long to quit, trying harder...") self.kill_sub_processes() break if kill_all_pids: self.kill_agent_process_ids() # The quit event can only be set once. Let's reset to our initial state self.quit_event = mp.Event() self.helper_process_manager = HelperProcessManager(self.quit_event) if not quiet: self.logger.info("Shut down complete!") def load_extension(self, extension_filename): try: extension_class = import_class_with_base(extension_filename, BaseExtension).get_loaded_class() self.extension = extension_class(self) self.game_interface.set_extension(self.extension) except FileNotFoundError as e: print(f'Failed to load extension: {e}') @staticmethod def run_agent(terminate_event, callback_event, reload_request, config_file, name, team, index, python_file, agent_telemetry_queue, queue_holder, match_config: MatchConfig): agent_class_wrapper = import_agent(python_file) if hasattr(agent_class_wrapper.get_loaded_class(), "run_independently"): bm = BotManagerIndependent(terminate_event, callback_event, reload_request, config_file, name, team, index, agent_class_wrapper, agent_telemetry_queue, queue_holder, match_config) elif hasattr(agent_class_wrapper.get_loaded_class(), "get_output_flatbuffer"): bm = BotManagerFlatbuffer(terminate_event, callback_event, reload_request, config_file, name, team, index, agent_class_wrapper, agent_telemetry_queue, queue_holder, match_config) else: bm = BotManagerStruct(terminate_event, callback_event, reload_request, config_file, name, team, index, agent_class_wrapper, agent_telemetry_queue, queue_holder, match_config) bm.run() @staticmethod def run_agent(terminate_event, callback_event, reload_request, config_file, name, team, index, python_file, agent_telemetry_queue, agent_state_queue, queue_holder, match_config: MatchConfig): agent_class_wrapper = import_agent(python_file) bm = BotManagerStruct(terminate_event, callback_event, reload_request, config_file, name, team, index, agent_class_wrapper, agent_telemetry_queue, queue_holder, match_config, agent_state_queue) bm.run() def kill_sub_processes(self): for process in self.sub_processes: process.terminate() self.sub_processes = [] def kill_agent_process_ids(self): pids = process_configuration.extract_all_pids(self.agent_metadata_map) for pid in pids: try: parent = psutil.Process(pid) for child in parent.children(recursive=True): # or parent.children() for recursive=False self.logger.info(f"Killing {child.pid} (child of {pid})") try: child.kill() except psutil._exceptions.NoSuchProcess: self.logger.info("Already dead.") self.logger.info(f"Killing {pid}") try: parent.kill() except psutil._exceptions.NoSuchProcess: self.logger.info("Already dead.") except psutil.NoSuchProcess: self.logger.info("Can't fetch parent process, already dead.")
class BotManager: def __init__(self, terminate_request_event, termination_complete_event, reload_request_event, bot_configuration, name, team, index, agent_class_wrapper, agent_metadata_queue, quick_chat_queue_holder, match_config): """ :param terminate_request_event: an Event (multiprocessing) which will be set from the outside when the program is trying to terminate :param termination_complete_event: an Event (multiprocessing) which should be set from inside this class when termination has completed successfully :param reload_request_event: an Event (multiprocessing) which will be set from the outside to force a reload of the agent :param reload_complete_event: an Event (multiprocessing) which should be set from inside this class when reloading has completed successfully :param bot_configuration: parameters which will be passed to the bot's constructor :param name: name which will be passed to the bot's constructor. Will probably be displayed in-game. :param team: 0 for blue team or 1 for orange team. Will be passed to the bot's constructor. :param index: The player index, i.e. "this is player number <index>". Will be passed to the bot's constructor. Can be used to pull the correct data corresponding to the bot's car out of the game tick packet. :param agent_class_wrapper: The ExternalClassWrapper object that can be used to load and reload the bot :param agent_metadata_queue: a Queue (multiprocessing) which expects to receive AgentMetadata once available. :param quick_chat_queue_holder: A data structure which helps the bot send and receive quickchat """ self.terminate_request_event = terminate_request_event self.termination_complete_event = termination_complete_event self.reload_request_event = reload_request_event self.bot_configuration = bot_configuration self.name = name self.team = team self.index = index self.agent_class_wrapper = agent_class_wrapper self.agent_metadata_queue = agent_metadata_queue self.logger = get_logger('bot' + str(self.index)) self.game_interface = GameInterface(self.logger) self.quick_chat_queue_holder = quick_chat_queue_holder self.last_chat_time = time.time() self.chat_counter = 0 self.reset_chat_time = True self.game_tick_packet = None self.bot_input = None self.ball_prediction = None self.rigid_body_tick = None self.match_config = match_config def send_quick_chat_from_agent(self, team_only, quick_chat): """ Passes the agents quick chats to the game, and also to other python bots. This does perform limiting. You are limited to 5 quick chats in a 2 second period starting from the first chat. This means you can spread your chats out to be even within that 2 second period. You could spam them in the first little bit but then will be throttled. """ # Send the quick chat to the game rlbot_status = send_quick_chat_flat(self.game_interface, self.index, self.team, team_only, quick_chat) if rlbot_status == RLBotCoreStatus.QuickChatRateExceeded: self.logger.debug('quick chat disabled') else: # Make the quick chat visible to other python bots. Unfortunately other languages can't see it. send_quick_chat(self.quick_chat_queue_holder, self.index, self.team, team_only, quick_chat) def load_agent(self): """ Loads and initializes an agent using instance variables, registers for quick chat and sets render functions. :return: An instance of an agent, and the agent class file. """ agent_class = self.agent_class_wrapper.get_loaded_class() agent = agent_class(self.name, self.team, self.index) agent.init_match_config(self.match_config) agent.load_config(self.bot_configuration.get_header("Bot Parameters")) self.update_metadata_queue(agent) self.set_render_manager(agent) agent_class_file = self.agent_class_wrapper.python_file agent._register_quick_chat(self.send_quick_chat_from_agent) agent._register_field_info(self.get_field_info) agent._register_set_game_state(self.set_game_state) agent._register_ball_prediction(self.get_ball_prediction) agent._register_ball_prediction_struct(self.get_ball_prediction_struct) agent._register_get_rigid_body_tick(self.get_rigid_body_tick) register_for_quick_chat(self.quick_chat_queue_holder, agent.handle_quick_chat, self.terminate_request_event) while not self.is_valid_field_info(): time.sleep(0.1) # Once all engine setup is done, do the agent-specific initialization, if any: agent.initialize_agent() return agent, agent_class_file def set_render_manager(self, agent: BaseAgent): """ Sets the render manager for the agent. :param agent: An instance of an agent. """ rendering_manager = self.game_interface.renderer.get_rendering_manager( self.index, self.team) agent._set_renderer(rendering_manager) def update_metadata_queue(self, agent: BaseAgent): """ Adds a new instance of AgentMetadata into the `agent_metadata_queue` using `agent` data. :param agent: An instance of an agent. """ pids = {os.getpid(), *agent.get_extra_pids()} helper_process_request = agent.get_helper_process_request() self.agent_metadata_queue.put( AgentMetadata(self.index, self.name, self.team, pids, helper_process_request)) def reload_agent(self, agent: BaseAgent, agent_class_file): """ Reloads the agent. Can throw exceptions. External classes should use reload_event.set() instead. :param agent: An instance of an agent. :param agent_class_file: The agent's class file. TODO: Remove this argument, it only affects logging and may be misleading. :return: The reloaded instance of the agent, and the agent class file. """ self.logger.debug('Reloading Agent: ' + agent_class_file) self.agent_class_wrapper.reload() old_agent = agent agent, agent_class_file = self.load_agent() # Retire after the replacement initialized properly. if hasattr(old_agent, 'retire'): old_agent.retire() return agent, agent_class_file def run(self): """ Loads interface for RLBot, prepares environment and agent, and calls the update for the agent. """ self.logger.debug('initializing agent') self.game_interface.load_interface() self.prepare_for_run() # Create Ratelimiter rate_limit = rate_limiter.RateLimiter( GAME_TICK_PACKET_POLLS_PER_SECOND) last_tick_game_time = None # What the tick time of the last observed tick was last_call_real_time = datetime.now() # When we last called the Agent # Get bot module agent, agent_class_file = self.load_agent() last_module_modification_time = os.stat(agent_class_file).st_mtime # Run until main process tells to stop, or we detect Ctrl+C try: while not self.terminate_request_event.is_set(): self.pull_data_from_game() # game_tick_packet = self.game_interface.get # Read from game data shared memory # Run the Agent only if the game_info has updated. tick_game_time = self.get_game_time() should_call_while_paused = datetime.now( ) - last_call_real_time >= MAX_AGENT_CALL_PERIOD if tick_game_time != last_tick_game_time or should_call_while_paused: last_tick_game_time = tick_game_time last_call_real_time = datetime.now() # Reload the Agent if it has been modified or if reload is requested from outside. try: new_module_modification_time = os.stat( agent_class_file).st_mtime if new_module_modification_time != last_module_modification_time or self.reload_request_event.is_set( ): self.reload_request_event.clear() last_module_modification_time = new_module_modification_time agent, agent_class_file = self.reload_agent( agent, agent_class_file) except FileNotFoundError: self.logger.error( f"Agent file {agent_class_file} was not found. Will try again." ) time.sleep(0.5) except Exception: self.logger.error("Reloading the agent failed:\n" + traceback.format_exc()) time.sleep( 0.5 ) # Avoid burning CPU / logs if this starts happening constantly # Call agent try: self.call_agent( agent, self.agent_class_wrapper.get_loaded_class()) except Exception as e: self.logger.error("Call to agent failed:\n" + traceback.format_exc()) # Ratelimit here rate_limit.acquire() except KeyboardInterrupt: self.terminate_request_event.set() # Shut down the bot by calling cleanup functions. if hasattr(agent, 'retire'): try: agent.retire() except Exception as e: self.logger.error("Retiring the agent failed:\n" + traceback.format_exc()) if hasattr(agent, 'renderer') and isinstance(agent.renderer, RenderingManager): agent.renderer.clear_all_touched_render_groups() # Zero out the inputs, so it's more obvious that the bot has stopped. self.game_interface.update_player_input(PlayerInput(), self.index) # If terminated, send callback self.termination_complete_event.set() def get_field_info(self): return self.game_interface.get_field_info() def get_rigid_body_tick(self): """Get the most recent state of the physics engine.""" return self.game_interface.update_rigid_body_tick(self.rigid_body_tick) def set_game_state(self, game_state: GameState) -> None: self.game_interface.set_game_state(game_state) def get_ball_prediction(self): return self.game_interface.get_ball_prediction() def get_ball_prediction_struct(self): raise NotImplementedError def prepare_for_run(self): raise NotImplementedError def call_agent(self, agent: BaseAgent, agent_class): raise NotImplementedError def get_game_time(self): raise NotImplementedError def pull_data_from_game(self): raise NotImplementedError def is_valid_field_info(self) -> bool: """Checks if the contents of field info are valid.""" raise NotImplementedError