Exemple #1
0
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_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
    bot_bundles: List[BotConfigBundle] = None
    start_match_configuration = None
    agent_metadata_queue = None
    extension = None
    bot_processes: Dict[int, mp.Process] = {}

    def __init__(self):
        self.logger = get_logger(DEFAULT_LOGGER)
        self.game_interface = GameInterface(self.logger)
        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.match_config: MatchConfig = None
        self.rlbot_gateway_process = None
        self.matchcomms_server: MatchcommsServerThread = None
        self.early_start_seconds = 0
        self.num_metadata_received = 0

    def is_rocket_league_running(self, port) -> bool:
        """
        Returns whether Rocket League is running with the right port.
        """

        try:
            is_rocket_league_running, proc = process_configuration.is_process_running(
                ROCKET_LEAGUE_PROCESS_INFO.PROGRAM,
                ROCKET_LEAGUE_PROCESS_INFO.PROGRAM_NAME,
                ROCKET_LEAGUE_PROCESS_INFO.REQUIRED_ARGS)

            if proc is not None:
                # Check for correct port.
                rocket_league_port = self._read_port_from_rocket_league_args(
                    proc.cmdline())
                if rocket_league_port is not None and rocket_league_port != port:
                    raise Exception(
                        f"Rocket League is already running with port {rocket_league_port} but we wanted "
                        f"{port}! Please close Rocket League and let us start it for you instead!"
                    )
        except WrongProcessArgs:
            raise Exception(
                f"Rocket League is not running with {ROCKET_LEAGUE_PROCESS_INFO.REQUIRED_ARGS}!\n"
                "Please close Rocket League and let us start it for you instead!"
            )

        return is_rocket_league_running

    def connect_to_game(self):
        """
        Connects to the game by initializing self.game_interface.
        """
        version.print_current_release_notes()
        port = self.ensure_rlbot_gateway_started()

        # Prevent loading game interface twice.
        if self.has_started:
            if not self.is_rocket_league_running(port):
                raise Exception(
                    "Rocket League is not running even though we started it once.\n"
                    "Please restart RLBot.")
            return

        # Currently match_config is None when launching from RLBotGUI.
        if self.match_config is not None and self.match_config.networking_role == 'remote_rlbot_client':
            self.logger.info(
                "Will not start Rocket League because this is configured as a client!"
            )
        # Launch the game if it is not running.
        elif not self.is_rocket_league_running(port):
            self.launch_rocket_league(port=port)

        try:
            self.game_interface.load_interface()
        except Exception as e:
            self.logger.error("Terminating rlbot gateway and raising:")
            self.rlbot_gateway_process.terminate()
            raise e
        self.agent_metadata_queue = mp.Queue()
        self.has_started = True

    @staticmethod
    def _read_port_from_rocket_league_args(args):
        for arg in args:
            # The arg will look like RLBot_ControllerURL="127.0.0.1:23233"
            if 'RLBot_ControllerURL' in arg:
                rocket_league_port = int(arg.split(':')[1].replace('"', ''))
                return int(rocket_league_port)
        return None

    def launch_rocket_league(self, port):
        """
        Launches Rocket League but does not connect to it.
        """
        ideal_args = ROCKET_LEAGUE_PROCESS_INFO.get_ideal_args(port)
        self.logger.info(f'Launching Rocket League with args: {ideal_args}')

        # Try launch via Steam.
        steam_exe_path = try_get_steam_executable_path()
        if steam_exe_path:  # Note: This Python 3.8 feature would be useful here https://www.python.org/dev/peps/pep-0572/#abstract
            exe_and_args = [
                str(steam_exe_path),
                '-applaunch',
                str(ROCKET_LEAGUE_PROCESS_INFO.GAMEID),
            ] + ideal_args
            _ = subprocess.Popen(
                exe_and_args)  # This is deliberately an orphan process.
            return

        # TODO: Figure out launching via Epic games

        self.logger.warning('Using fall-back launch method.')
        self.logger.info(
            "You should see a confirmation pop-up, if you don't see it then click on Steam! "
            'https://gfycat.com/AngryQuickFinnishspitz')
        args_string = '%20'.join(ideal_args)

        # Try launch via terminal (Linux)
        if platform.system() == 'Linux':
            linux_args = [
                'steam',
                f'steam://rungameid/{ROCKET_LEAGUE_PROCESS_INFO.GAMEID}//{args_string}'
            ]

            try:
                _ = subprocess.Popen(linux_args)

            except OSError:
                self.logger.warning(
                    'Could not find Steam executable, using browser to open instead.'
                )
            else:
                return

        try:
            webbrowser.open(
                f'steam://rungameid/{ROCKET_LEAGUE_PROCESS_INFO.GAMEID}//{args_string}'
            )
        except webbrowser.Error:
            self.logger.warning(
                'Unable to launch Rocket League. Please launch Rocket League manually using the -rlbot option to continue.'
            )

    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]

        for player in match_config.player_configs:
            if player.bot and not player.rlbot_controlled:
                set_random_psyonix_bot_preset(player)

        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.bot_bundles = []

        for index, bot in enumerate(match_config.player_configs):
            self.bot_bundles.append(bundles[index])
            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 ensure_rlbot_gateway_started(self) -> int:
        """
        Ensures that RLBot.exe is running. Returns the port that it will be listening on for connections from
        Rocket League. Rocket League should be passed a command line argument so that it starts with this same port.
        :return:
        """

        # TODO: Uncomment this when done with local testing of Remote RLBot.
        self.rlbot_gateway_process, port = gateway_util.find_existing_process()
        if self.rlbot_gateway_process is not None:
            self.logger.info(f"Already have RLBot.exe running! Port is {port}")
            return port

        launch_options = LaunchOptions()
        if self.match_config is not None:  # Currently this is None when launching from RLBotGUI.
            networking_role = NetworkingRole[self.match_config.networking_role]
            launch_options = LaunchOptions(
                networking_role=networking_role,
                remote_address=self.match_config.network_address)

        self.rlbot_gateway_process, port = gateway_util.launch(launch_options)
        self.logger.info(
            f"Python started RLBot.exe with process id {self.rlbot_gateway_process.pid} "
            f"and port {port}")
        return port

    def launch_ball_prediction(self):
        # This does nothing now. It's kept here temporarily so that RLBotGUI doesn't break.
        pass

    def has_received_metadata_from_all_bots(self):
        expected_metadata_calls = sum(
            1 for player in self.match_config.player_configs
            if player.rlbot_controlled)
        return self.num_metadata_received >= expected_metadata_calls

    def launch_early_start_bot_processes(self):
        """
        Some bots can start up before the game is ready and not be bothered by missing
        or strange looking values in the game tick packet, etc. Such bots can opt in to the
        early start category and enjoy extra time to load up before the match starts.
        """

        if self.match_config.networking_role == NetworkingRole.remote_rlbot_client:
            return  # The bot indices are liable to change, so don't start anything yet.

        self.logger.debug("Launching early-start bot processes")
        num_started = self.launch_bot_process_helper(early_starters_only=True)
        self.try_recieve_agent_metadata()
        if num_started > 0 and self.early_start_seconds > 0:
            self.logger.info(
                f"Waiting for {self.early_start_seconds} seconds to let early-start bots load."
            )
            end_time = datetime.now() + timedelta(
                seconds=self.early_start_seconds)
            while datetime.now() < end_time:
                self.try_recieve_agent_metadata()
                time.sleep(0.1)

    def launch_bot_processes(self):
        self.logger.debug("Launching bot processes")
        self.launch_bot_process_helper(early_starters_only=False)

    def launch_bot_process_helper(self, early_starters_only=False):
        # Start matchcomms here as it's only required for the bots.
        self.kill_matchcomms_server()
        self.matchcomms_server = launch_matchcomms_server()

        num_started = 0

        # Launch processes
        # TODO: this might be the right moment to fix the player indices based on a game tick packet.
        packet = game_data_struct.GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        # TODO: root through the packet and find discrepancies in the player index mapping.
        for i in range(self.num_participants):
            if not self.start_match_configuration.player_configuration[
                    i].rlbot_controlled:
                continue
            if early_starters_only and not self.bot_bundles[
                    i].supports_early_start:
                continue

            bot_manager_spawn_id = None

            if early_starters_only:
                # Don't use a spawn id stuff for the early start system. The bots will be starting up before
                # the car spawns, and we don't want the bot manager to panic.
                participant_index = i
            else:
                participant_index = None
                spawn_id = self.game_interface.start_match_configuration.player_configuration[
                    i].spawn_id
                self.logger.info(
                    f'Player in slot {i} was sent with spawn id {spawn_id}, will search in the packet.'
                )
                for n in range(0, packet.num_cars):
                    packet_spawn_id = packet.game_cars[n].spawn_id
                    self.logger.info(
                        f'Packet index {n} has spawn id {packet_spawn_id}')
                    if spawn_id == packet_spawn_id:
                        self.logger.info(
                            f'Looks good, considering participant index to be {n}'
                        )
                        participant_index = n
                        bot_manager_spawn_id = spawn_id
                if participant_index is None:
                    raise Exception("Unable to determine the bot index!")

            if participant_index not in self.bot_processes:
                reload_request = mp.Event()
                quit_callback = mp.Event()
                self.bot_reload_requests.append(reload_request)
                self.bot_quit_callbacks.append(quit_callback)
                process = mp.Process(
                    target=SetupManager.run_agent,
                    args=(self.quit_event, quit_callback, reload_request,
                          self.bot_bundles[i],
                          str(self.start_match_configuration.
                              player_configuration[i].name), self.teams[i],
                          participant_index, self.python_files[i],
                          self.agent_metadata_queue, self.match_config,
                          self.matchcomms_server.root_url,
                          bot_manager_spawn_id))
                process.start()
                self.bot_processes[i] = process
                num_started += 1

        self.logger.debug(f"Successfully started {num_started} bot processes")
        return num_started

    def launch_quick_chat_manager(self):
        # Quick chat manager is gone since we're using RLBot.exe now.
        # Keeping this function around for backwards compatibility.
        pass

    def start_match(self):

        if self.match_config.networking_role == NetworkingRole.remote_rlbot_client:
            match_settings = self.game_interface.get_match_settings()
            # TODO: merge the match settings into self.match_config
            # And then make sure we still only start the appropriate bot processes
            # that we originally asked for.

        self.logger.info("Python attempting to start match.")
        self.game_interface.start_match()
        time.sleep(
            2
        )  # Wait a moment. If we look too soon, we might see a valid packet from previous game.
        self.game_interface.wait_until_valid_packet()
        self.logger.info("Match has started")

    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
            # TODO windows only library
            if platform.system() == 'Windows':
                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 received 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:
                self.num_metadata_received += 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)

        # Don't kill RLBot.exe. It needs to keep running because if we're in a GUI
        # that will persist after this shut down, the interface dll in charge of starting
        # matches is already locked in to its shared memory files, and if we start a new
        # RLBot.exe, those files will go stale. https://github.com/skyborgff/RLBot/issues/9

        # 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...")
                break

        self.kill_bot_processes()

        if kill_all_pids:
            self.kill_agent_process_ids()

        self.kill_matchcomms_server()

        # 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,
                  bundle: BotConfigBundle, name, team, index, python_file,
                  agent_telemetry_queue, match_config: MatchConfig,
                  matchcomms_root: URL, spawn_id: str):

        agent_class_wrapper = import_agent(python_file)
        config_file = agent_class_wrapper.get_loaded_class(
        ).base_create_agent_configurations()
        config_file.parse_file(bundle.config_obj,
                               config_directory=bundle.config_directory)

        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, match_config,
                                       matchcomms_root, spawn_id)
        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, match_config,
                                      matchcomms_root, spawn_id)
        else:
            bm = BotManagerStruct(terminate_event, callback_event,
                                  reload_request, config_file, name, team,
                                  index, agent_class_wrapper,
                                  agent_telemetry_queue, match_config,
                                  matchcomms_root, spawn_id)
        bm.run()

    def kill_bot_processes(self):
        for process in self.bot_processes.values():
            process.terminate()
        for process in self.bot_processes.values():
            process.join(timeout=1)
        self.bot_processes.clear()
        self.num_metadata_received = 0

    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.")
            except psutil.AccessDenied as ex:
                self.logger.error(
                    f"Access denied when trying to kill a bot pid! {ex}")
            except Exception as ex:
                self.logger.error(
                    f"Unexpected exception when trying to kill a bot pid! {ex}"
                )

    def kill_matchcomms_server(self):
        if self.matchcomms_server:
            self.matchcomms_server.close()
            self.matchcomms_server = None
class Hivemind:
    """
    Sends and receives data from Rocket League, and maintains the list of drones.
    """

    # Some terminology:
    # hivemind = the process which controls the drones.
    # drone = a bot under the hivemind's control.

    def __init__(self, queue, choreo_obj):
        # Sets up the logger. The string is the name of your hivemind.
        # Call this something unique so people can differentiate between hiveminds.
        self.logger = get_logger('Choreography Hivemind')

        # 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)

        self.drones = []

        self.choreo = choreo_obj(self.game_interface)
        self.choreo.generate_sequence(self.drones)

        # Set up queue to know when to stop and reload.
        self.queue = queue

    def start(self):
        """Runs once, sets up the hivemind and its agents."""
        # Prints an activation message into the console.
        # This let's you know that the process is up and running.
        self.logger.info("Hello World!")

        # Loads game interface.
        self.game_interface.load_interface()

        # This is how you access field info.
        # First create the initialise the object...
        field_info = FieldInfoPacket()
        # Then update it.
        self.game_interface.update_field_info_packet(field_info)

        # Same goes for the packet, but that is
        # also updated in the main loop every tick.
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        # Initialise drones list. Will be filled with Drone objects for every drone.
        self.drones = []

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):

        # Creating packet which will be updated every tick.
        packet = GameTickPacket()

        # MAIN LOOP:
        while self.loop_check():
            #print('test')

            prev_time = packet.game_info.seconds_elapsed
            # Updating the game tick packet.
            self.game_interface.update_live_data_packet(packet)

            # Checking if packet is new, otherwise sleep.
            if prev_time == packet.game_info.seconds_elapsed:
                time.sleep(0.001)
                continue

            # Create a Drone object for every drone that holds its information.
            if packet.num_cars > len(self.drones):
                # Clears the list if there are more cars than drones.
                self.drones.clear()
                for index in range(packet.num_cars):
                    self.drones.append(
                        Drone(index, packet.game_cars[index].team))

            # Processing drone data.
            for drone in self.drones:
                drone.update(packet.game_cars[drone.index],
                             packet.game_info.seconds_elapsed)

            # Steps through the choreography.
            self.choreo.step(packet, self.drones)

            # Resets choreography once it has finished.
            if self.choreo.finished:
                # Re-instantiates the choreography.
                self.choreo = self.choreo.__class__(self.game_interface)
                self.choreo.generate_sequence(self.drones)

            # Sends the drone inputs to the drones.
            for drone in self.drones:
                self.game_interface.update_player_input(
                    convert_player_input(drone.ctrl), drone.index)

    def loop_check(self):
        """
        Checks whether the hivemind should keep looping or should die.
        """
        if self.queue.empty():
            return True

        else:
            message = self.queue.get()
            return message != QCommand.STOP
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)
Exemple #4
0
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_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
    bot_bundles: List[BotConfigBundle] = None
    match_config: MatchConfig = None
    extension = None

    def __init__(self):
        self.logger = get_logger(DEFAULT_LOGGER)
        self.game_interface = GameInterface(self.logger)
        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: Dict[int, AgentMetadata] = {}
        self.match_config: MatchConfig = None
        self.launcher_preference = None
        self.rlbot_gateway_process = None
        self.matchcomms_server: MatchcommsServerThread = None
        self.early_start_seconds = 0
        self.num_metadata_received = 0
        self.agent_metadata_queue = mp.Queue()
        self.bot_processes: Dict[int, BotProcessInfo] = {}
        self.script_processes: Dict[int, subprocess.Popen] = {}

    def is_rocket_league_running(self, port) -> bool:
        """
        Returns whether Rocket League is running with the right port.
        """

        try:
            is_rocket_league_running, proc = process_configuration.is_process_running(
                ROCKET_LEAGUE_PROCESS_INFO.PROGRAM,
                ROCKET_LEAGUE_PROCESS_INFO.PROGRAM_NAME,
                ROCKET_LEAGUE_PROCESS_INFO.REQUIRED_ARGS)

            if proc is not None:
                # Check for correct port.
                rocket_league_port = self._read_port_from_rocket_league_args(proc.cmdline())
                if rocket_league_port is not None and rocket_league_port != port:
                    raise Exception(f"Rocket League is already running with port {rocket_league_port} but we wanted "
                                    f"{port}! Please close Rocket League and let us start it for you instead!")
        except WrongProcessArgs:
            raise Exception(f"Rocket League is not running with {ROCKET_LEAGUE_PROCESS_INFO.REQUIRED_ARGS}!\n"
                            "Please close Rocket League and let us start it for you instead!")

        return is_rocket_league_running

    def connect_to_game(self, launcher_preference: RocketLeagueLauncherPreference = None):
        """
        Connects to the game by initializing self.game_interface.
        """
        version.print_current_release_notes()
        port = self.ensure_rlbot_gateway_started()

        # Prevent loading game interface twice.
        if self.has_started:
            if not self.is_rocket_league_running(port):
                raise Exception("Rocket League is not running even though we started it once.\n"
                                "Please restart RLBot.")
            return

        # Currently match_config is None when launching from RLBotGUI.
        if self.match_config is not None and self.match_config.networking_role == 'remote_rlbot_client':
            self.logger.info("Will not start Rocket League because this is configured as a client!")
        # Launch the game if it is not running.
        elif not self.is_rocket_league_running(port):
            mergeTASystemSettings()
            pref = launcher_preference or self.launcher_preference or DEFAULT_LAUNCHER_PREFERENCE
            self.launch_rocket_league(port=port, launcher_preference=pref)

        try:
            self.logger.info("Loading interface...")
            # We're not going to use this game_interface for much, just sending start match messages and inspecting
            # the packet to see if the appropriate cars have been spawned.
            self.game_interface.load_interface(
                port=23234, wants_ball_predictions=False, wants_quick_chat=False, wants_game_messages=False)
        except Exception as e:
            self.logger.error("Terminating rlbot gateway and raising:")
            self.rlbot_gateway_process.terminate()
            raise e
        self.has_started = True

    @staticmethod
    def _read_port_from_rocket_league_args(args):
        for arg in args:
            # The arg will look like RLBot_ControllerURL="127.0.0.1:23233"
            if 'RLBot_ControllerURL' in arg:
                rocket_league_port = int(arg.split(':')[1].replace('"', ''))
                return int(rocket_league_port)
        return None

    def launch_rocket_league(self, port, launcher_preference: RocketLeagueLauncherPreference = DEFAULT_LAUNCHER_PREFERENCE):
        """
        Launches Rocket League but does not connect to it.
        """
        ideal_args = ROCKET_LEAGUE_PROCESS_INFO.get_ideal_args(port)

        if launcher_preference.preferred_launcher == RocketLeagueLauncherPreference.EPIC:
            if launcher_preference.use_login_tricks:
                if launch_with_epic_login_trick(ideal_args):
                    return
                else:
                    self.logger.info("Epic login trick seems to have failed!")
                    # Try Steam after this.
            if launch_with_epic_simple(ideal_args):
                return

        # Try launch via Steam.
        steam_exe_path = try_get_steam_executable_path()
        if steam_exe_path:  # Note: This Python 3.8 feature would be useful here https://www.python.org/dev/peps/pep-0572/#abstract
            exe_and_args = [
                str(steam_exe_path),
                '-applaunch',
                str(ROCKET_LEAGUE_PROCESS_INFO.GAMEID),
            ] + ideal_args
            self.logger.info(f'Launching Rocket League with: {exe_and_args}')
            _ = subprocess.Popen(exe_and_args)  # This is deliberately an orphan process.
            return

        self.logger.warning(f'Launching Rocket League using Steam-only fall-back launch method with args: {ideal_args}')
        self.logger.info("You should see a confirmation pop-up, if you don't see it then click on Steam! "
                         'https://gfycat.com/AngryQuickFinnishspitz')
        args_string = '%20'.join(ideal_args)

        # Try launch via terminal (Linux)
        if platform.system() == 'Linux':
            linux_args = [
                'steam',
                f'steam://rungameid/{ROCKET_LEAGUE_PROCESS_INFO.GAMEID}//{args_string}'
            ]

            try:
                _ = subprocess.Popen(linux_args)
                return
            except OSError:
                self.logger.warning('Could not launch Steam executable on Linux.')

        try:
            self.logger.info("Launching rocket league via steam browser URL as a last resort...")
            webbrowser.open(f'steam://rungameid/{ROCKET_LEAGUE_PROCESS_INFO.GAMEID}//{args_string}')
        except webbrowser.Error:
            self.logger.warning(
                'Unable to launch Rocket League. Please launch Rocket League manually using the -rlbot option to continue.')

    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]

        for player in match_config.player_configs:
            if player.bot and not player.rlbot_controlled and not player.loadout_config:
                set_random_psyonix_bot_preset(player)

        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.bot_bundles = []

        for index, bot in enumerate(match_config.player_configs):
            self.bot_bundles.append(bundles[index])
            if bot.loadout_config is None and bundles[index]:
                bot.loadout_config = bundles[index].generate_loadout_config(index, 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)

        try:
            urlopen("http://google.com")
            checked_environment_requirements = set()
            online = True
        except URLError:
            print("The user is offline, skipping upgrade the bot requirements")
            online = False

        for bundle in self.bot_bundles:
            if bundle is not None and bundle.use_virtual_environment:
                do_post_setup = online

                if do_post_setup:
                    if bundle.requirements_file in checked_environment_requirements:
                        do_post_setup = False
                    else:
                        checked_environment_requirements.add(bundle.requirements_file)

                builder = EnvBuilderWithRequirements(bundle=bundle, do_post_setup=do_post_setup)
                builder.create(Path(bundle.config_directory) / 'venv')

        for script_config in match_config.script_configs:
            script_config_bundle = get_script_config_bundle(script_config.config_path)
            if script_config_bundle.use_virtual_environment:
                do_post_setup = online

                if do_post_setup:
                    if bundle.requirements_file in checked_environment_requirements:
                        do_post_setup = False
                    else:
                        checked_environment_requirements.add(bundle.requirements_file)

                builder = EnvBuilderWithRequirements(bundle=script_config_bundle, do_post_setup=do_post_setup)
                builder.create(Path(script_config_bundle.config_directory) / 'venv')

        self.match_config = match_config
        self.game_interface.match_config = match_config
        self.game_interface.start_match_flatbuffer = match_config.create_flatbuffer()

        if USE_OLD_LAUNCH:
            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)
        raw_launcher_string = framework_config.get(RLBOT_CONFIGURATION_HEADER, LAUNCHER_PREFERENCE_KEY)
        if raw_launcher_string == RocketLeagueLauncherPreference.STEAM:
            self.launcher_preference = RocketLeagueLauncherPreference(RocketLeagueLauncherPreference.STEAM, False)
        else:
            self.launcher_preference = DEFAULT_LAUNCHER_PREFERENCE

    def ensure_rlbot_gateway_started(self) -> int:
        """
        Ensures that RLBot.exe is running. Returns the port that it will be listening on for connections from
        Rocket League. Rocket League should be passed a command line argument so that it starts with this same port.
        :return:
        """

        # TODO: Uncomment this when done with local testing of Remote RLBot.
        self.rlbot_gateway_process, port = gateway_util.find_existing_process()
        if self.rlbot_gateway_process is not None:
            self.logger.info(f"Already have RLBot.exe running! Port is {port}")
            return port

        launch_options = LaunchOptions()
        if self.match_config is not None:  # Currently this is None when launching from RLBotGUI.
            networking_role = NetworkingRole[self.match_config.networking_role]
            launch_options = LaunchOptions(
                networking_role=networking_role,
                remote_address=self.match_config.network_address)

        self.rlbot_gateway_process, port = gateway_util.launch(launch_options)
        self.logger.info(f"Python started RLBot.exe with process id {self.rlbot_gateway_process.pid} "
                         f"and port {port}")
        return port

    def launch_ball_prediction(self):
        # This does nothing now. It's kept here temporarily so that RLBotGUI doesn't break.
        pass

    def has_received_metadata_from_all_bots(self):
        expected_metadata_calls = sum(1 for player in self.match_config.player_configs if player.rlbot_controlled)
        return self.num_metadata_received >= expected_metadata_calls

    def launch_early_start_bot_processes(self, match_config: MatchConfig = None):
        """
        Some bots can start up before the game is ready and not be bothered by missing
        or strange looking values in the game tick packet, etc. Such bots can opt in to the
        early start category and enjoy extra time to load up before the match starts.

        WARNING: Early start is a bad idea if there's any risk that bots will not get their promised
        index. This can happen with remote RLBot, etc.
        """

        if self.match_config.networking_role == NetworkingRole.remote_rlbot_client:
            return  # The bot indices are liable to change, so don't start anything yet.

        self.logger.debug("Launching early-start bot processes")
        num_started = self.launch_bot_process_helper(early_starters_only=True, match_config=match_config or self.match_config)
        self.try_recieve_agent_metadata()
        if num_started > 0 and self.early_start_seconds > 0:
            self.logger.info(f"Waiting for {self.early_start_seconds} seconds to let early-start bots load.")
            end_time = datetime.now() + timedelta(seconds=self.early_start_seconds)
            while datetime.now() < end_time:
                self.try_recieve_agent_metadata()
                time.sleep(0.1)

    def launch_bot_processes(self, match_config: MatchConfig = None):
        self.logger.debug("Launching bot processes")
        self.launch_bot_process_helper(early_starters_only=False, match_config=match_config or self.match_config)

    def launch_bot_process_helper(self, early_starters_only=False, match_config: MatchConfig = None):
        # Start matchcomms here as it's only required for the bots.
        if not self.matchcomms_server:
            self.matchcomms_server = launch_matchcomms_server()
        self.bot_processes = {ind: proc for ind, proc in self.bot_processes.items() if proc.is_alive()}

        num_started = 0

        # Launch processes
        # TODO: this might be the right moment to fix the player indices based on a game tick packet.
        if not early_starters_only:
            if USE_OLD_LAUNCH:
                packet = game_data_struct.GameTickPacket()
                self.game_interface.update_live_data_packet(packet)
            else:
                packet = get_one_packet()

        # TODO: root through the packet and find discrepancies in the player index mapping.
        for i in range(min(self.num_participants, len(match_config.player_configs))):

            player_config = match_config.player_configs[i]
            if not player_config.has_bot_script():
                continue
            if early_starters_only and not self.bot_bundles[i].supports_early_start:
                continue

            spawn_id = player_config.spawn_id

            if early_starters_only:
                # Danger: we have low confidence in this since we're not leveraging the spawn id.
                participant_index = i
            else:
                participant_index = None

                self.logger.info(f'Player in slot {i} was sent with spawn id {spawn_id}, will search in the packet.')
                num_players = packet.num_cars if USE_OLD_LAUNCH else packet.PlayersLength()
                for n in range(0, num_players):
                    packet_spawn_id = packet.game_cars[n].spawn_id if USE_OLD_LAUNCH else packet.Players(n).SpawnId()
                    if spawn_id == packet_spawn_id:
                        self.logger.info(f'Looks good, considering participant index to be {n}')
                        participant_index = n
                if participant_index is None:
                    for prox_index, proc_info in self.bot_processes.items():
                        if spawn_id == proc_info.player_config.spawn_id:
                            participant_index = prox_index
                    if participant_index is None:
                        raise Exception(f"Unable to determine the bot index for spawn id {spawn_id}")

            if participant_index not in self.bot_processes:
                bundle = get_bot_config_bundle(player_config.config_path)
                deduped_name = str(self.match_config.player_configs[i].deduped_name)
                if bundle.supports_standalone:
                    executable = sys.executable
                    if bundle.use_virtual_environment:
                        executable = str(Path(bundle.config_directory) / 'venv' / 'Scripts' / 'python.exe')
                    process = subprocess.Popen([
                        executable,
                        bundle.python_file,
                        '--config-file', str(player_config.config_path),
                        '--name', deduped_name,
                        '--team', str(self.teams[i]),
                        '--player-index', str(participant_index),
                        '--spawn-id', str(spawn_id),
                        '--matchcomms-url', self.matchcomms_server.root_url.geturl()
                    ], cwd=Path(bundle.config_directory).parent)
                    self.bot_processes[participant_index] = BotProcessInfo(process=None, subprocess=process, player_config=player_config)

                    # Insert immediately into the agent metadata map because the standalone process has no way to communicate it back out
                    self.agent_metadata_map[participant_index] = AgentMetadata(participant_index, deduped_name, self.teams[i], {process.pid})
                else:
                    reload_request = mp.Event()
                    quit_callback = mp.Event()
                    self.bot_reload_requests.append(reload_request)
                    self.bot_quit_callbacks.append(quit_callback)
                    process = mp.Process(target=SetupManager.run_agent,
                                         args=(self.quit_event, quit_callback, reload_request, self.bot_bundles[i],
                                               deduped_name,
                                               self.teams[i], participant_index, self.python_files[i], self.agent_metadata_queue,
                                               match_config, self.matchcomms_server.root_url, spawn_id))
                    process.start()
                    self.bot_processes[participant_index] = BotProcessInfo(process=process, subprocess=None, player_config=player_config)
                num_started += 1

        self.logger.info(f"Successfully started {num_started} bot processes")

        process_configuration.configure_processes(self.agent_metadata_map, self.logger)

        scripts_started = 0
        for script_config in match_config.script_configs:
            script_config_bundle = get_script_config_bundle(script_config.config_path)
            if early_starters_only and not script_config_bundle.supports_early_start:
                continue
            executable = sys.executable
            if script_config_bundle.use_virtual_environment:
                executable = str(Path(script_config_bundle.config_directory) / 'venv' / 'Scripts' / 'python.exe')

            process = subprocess.Popen(
                [
                    executable,
                    script_config_bundle.script_file,
                    '--matchcomms-url', self.matchcomms_server.root_url.geturl()
                ],
                cwd=Path(script_config_bundle.config_directory).parent
            )
            self.logger.info(f"Started script with pid {process.pid} using {process.args}")
            self.script_processes[process.pid] = process
            scripts_started += 1

        self.logger.debug(f"Successfully started {scripts_started} scripts")

        return num_started

    def launch_quick_chat_manager(self):
        # Quick chat manager is gone since we're using RLBot.exe now.
        # Keeping this function around for backwards compatibility.
        pass

    def start_match(self):

        if self.match_config.networking_role == NetworkingRole.remote_rlbot_client:
            match_settings = self.game_interface.get_match_settings()
            # TODO: merge the match settings into self.match_config
            # And then make sure we still only start the appropriate bot processes
            # that we originally asked for.

        self.logger.info("Python attempting to start match.")
        self.game_interface.start_match()
        self.game_interface.wait_until_valid_packet()
        self.logger.info("Match has started")

        cleanUpTASystemSettings()

    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
            # TODO windows only library
            if platform.system() == 'Windows':
                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)
            else:
                try:
                    # https://python-forum.io/Thread-msvcrt-getkey-for-linux
                    import termios, sys
                    TERMIOS = termios

                    fd = sys.stdin.fileno()
                    old = termios.tcgetattr(fd)
                    new = termios.tcgetattr(fd)
                    new[3] = new[3] & ~TERMIOS.ICANON & ~TERMIOS.ECHO
                    new[6][TERMIOS.VMIN] = 1
                    new[6][TERMIOS.VTIME] = 0
                    termios.tcsetattr(fd, TERMIOS.TCSANOW, new)
                    command = None
                    try:
                        command = os.read(fd, 1)
                    finally:
                        termios.tcsetattr(fd, TERMIOS.TCSAFLUSH, old)
                    command = command.decode("utf-8")
                    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)
                except:
                    pass

            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 received metadata from.
        """
        num_recieved = 0
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.agent_metadata_queue.get(timeout=0.1)
                num_recieved += 1
                if single_agent_metadata.name not in [pc.deduped_name for pc in self.match_config.player_configs]:
                    self.logger.warn(f"Got agent metadata for {single_agent_metadata.name} but it shouldn't be running!")

                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:
                self.num_metadata_received += num_recieved
                break

        # Let's go through the agent metadata map and see if we can expand it with any child processes.
        # We'll do it every time this function is called (generall periodically),
        # we don't know when an agent might spawn another process.
        if process_configuration.append_child_pids(self.agent_metadata_map):
            process_configuration.configure_processes(self.agent_metadata_map, self.logger)

        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)

        # Don't kill RLBot.exe. It needs to keep running because if we're in a GUI
        # that will persist after this shut down, the interface dll in charge of starting
        # matches is already locked in to its shared memory files, and if we start a new
        # RLBot.exe, those files will go stale. https://github.com/skyborgff/RLBot/issues/9

        # 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...")
                break

        self.kill_bot_processes()
        self.kill_agent_process_ids(set(self.script_processes.keys()))

        if kill_all_pids:
            # The original meaning of the kill_all_pids flag only applied to bots, not scripts,
            # so we are doing that separately.
            self.kill_agent_process_ids(process_configuration.extract_all_pids(self.agent_metadata_map))

        self.kill_matchcomms_server()

        # Drain the agent_metadata_queue to make sure nothing rears its head later.
        while True:  # will exit on queue.Empty
            try:
                metadata = self.agent_metadata_queue.get(timeout=0.1)
                self.logger.warn(f"Drained out metadata for {metadata.name} during shutdown!")
            except queue.Empty:
                break

        # 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, bundle: BotConfigBundle, name, team, index,
                  python_file, agent_telemetry_queue, match_config: MatchConfig, matchcomms_root: URL, spawn_id: str):

        # Set the working directory to one level above the bot cfg file.
        # This mimics the behavior you get when executing run.py in one of the
        # example bot repositories, so bots will be more likely to 'just work'
        # even if the developer is careless about file paths.
        os.chdir(Path(bundle.config_directory).parent)

        agent_class_wrapper = import_agent(python_file)
        config_file = agent_class_wrapper.get_loaded_class().base_create_agent_configurations()
        config_file.parse_file(bundle.config_obj, config_directory=bundle.config_directory)

        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, match_config, matchcomms_root,
                                       spawn_id)
        else:
            bm = BotManagerStruct(terminate_event, callback_event, reload_request, config_file, name, team, index,
                                  agent_class_wrapper, agent_telemetry_queue, match_config, matchcomms_root, spawn_id)
        bm.run()

    def kill_bot_processes(self):
        for process_info in self.bot_processes.values():
            proc = process_info.process or process_info.subprocess
            proc.terminate()
        for process_info in self.bot_processes.values():
            if process_info.process:
                process_info.process.join(timeout=1)
        self.bot_processes.clear()
        self.num_metadata_received = 0

    def send_sigterm_recursive(self, pid: int) -> List[psutil.Process]:
        """
        Returns the list of processes under the pid and including the pid for further handling,
        because they may become orphaned by the sigterm.
        """
        all_processes = []
        immediate_children = []
        try:
            process = psutil.Process(pid)
            immediate_children = [c for c in process.children(recursive=False)]
            all_processes = [process] + [c for c in process.children(recursive=True)]
            process.send_signal(signal.SIGTERM)
        except Exception as ex:
            self.logger.debug(f"Got {ex} while sending sigterm to pid {pid}.")

        for c in immediate_children:
            self.send_sigterm_recursive(c.pid)

        return all_processes

    def kill_agent_process_ids(self, pids: Set[int]):
        all_processes = []
        for pid in pids:
            all_processes += self.send_sigterm_recursive(pid)

        time.sleep(.5)

        for c in all_processes:
            try:
                c.kill()
            except Exception as ex:
                self.logger.debug(f"Got {ex} while killing pid {pid}.")

    def kill_matchcomms_server(self):
        if self.matchcomms_server:
            self.matchcomms_server.close()
            self.matchcomms_server = None
Exemple #5
0
class ScratchManager(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('scratch_mgr')
        self.game_interface = GameInterface(self.logger)
        self.current_sockets = set()
        self.running_indices = set()
        self.metadata_map = dict()
        self.port: int = options['port']
        self.sb3_file = options['sb3-file']
        self.pretend_blue_team = options['pretend_blue_team']
        self.has_received_input = False
        self.scratch_index_to_rlbot = {}
        self.should_flip_field = False

    async def data_exchange(self, websocket, path):
        async for message in websocket:
            try:
                controller_states = json.loads(message)
                if not self.has_received_input:
                    self.has_received_input = True
                    self.logger.info(
                        f"Just got first input from Scratch {self.sb3_file} {self.port}"
                    )

                for key, scratch_state in controller_states.items():
                    scratch_index = int(key)
                    rlbot_index = self.get_rlbot_index(scratch_index)
                    self.game_interface.update_player_input_flat(
                        self.convert_to_flatbuffer(scratch_state, rlbot_index))
            except UnicodeDecodeError:
                backup_location = os.path.join(
                    os.path.dirname(self.sb3_file),
                    f'backup-{str(int(os.path.getmtime(self.sb3_file)))}.sb3')
                print(
                    f"Saving new version of {self.sb3_file} and backing up old version to {backup_location}"
                )
                shutil.move(self.sb3_file, backup_location)
                with open(self.sb3_file, 'wb') as output:
                    output.write(message)

            self.current_sockets.add(websocket)

    def try_receive_agent_metadata(self):
        """
        As agents start up, they will dump their configuration into the metadata_queue.
        Read from it to learn about all the bots intending to use this scratch manager.
        """
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
                self.metadata_map[
                    single_agent_metadata.index] = single_agent_metadata
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        self.logger.info("Starting scratch manager")

        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata
        time.sleep(1)
        self.try_receive_agent_metadata()

        self.logger.info(self.running_indices)

        num_scratch_bots = len(self.running_indices)
        if num_scratch_bots == 0:
            self.logger.error(
                "No scratch bots registered in the scratch manager, exiting!")
            return

        self.setup_index_map()

        all_orange = sum(
            map(lambda meta: meta.team,
                self.metadata_map.values())) == len(self.running_indices)
        self.should_flip_field = self.pretend_blue_team and all_orange

        if self.options['spawn_browser']:
            options = Options()
            options.headless = self.options['headless']

            # This prevents an error message about AudioContext when running in headless mode.
            options.add_argument("--autoplay-policy=no-user-gesture-required")

            players_string = ",".join(
                map(index_to_player_string, range(len(self.running_indices))))

            try:
                driver_path = self.get_driver_path_retryable()
                driver = webdriver.Chrome(driver_path, chrome_options=options)
                driver.get(
                    f"http://scratch.rlbot.org?host=localhost:{str(self.port)}&players={players_string}&awaitBotFile=1"
                )

                if self.sb3_file is not None:
                    element = WebDriverWait(driver, 10).until(
                        EC.presence_of_element_located(
                            (By.ID, "sb3-selenium-uploader")))
                    element.send_keys(self.sb3_file)
                    self.logger.info(
                        f'Loaded sb3 file {self.sb3_file} into chrome window.')

            except SessionNotCreatedException:
                # This can happen if the downloaded chromedriver does not match the version of Chrome that is installed.
                webbrowser.open_new(
                    f"http://scratch.rlbot.org?host=localhost:{str(self.port)}&players={players_string}"
                )
                self.logger.info(
                    f"Could not load the Scratch file automatically! You'll need to upload it yourself "
                    f"from {self.sb3_file}")

        self.logger.info(f'Starting websocket server on port {self.port}')
        asyncio.get_event_loop().run_until_complete(
            websockets.serve(self.data_exchange, port=self.port))
        asyncio.get_event_loop().run_until_complete(self.game_loop())

    def get_driver_path_retryable(self) -> str:
        # Sometimes this code runs concurrently, e.g. if there are many scratch bots which are all
        # using separate_browsers = True. In that case, ChromeDriverManager().install() can run into contention
        # when trying to access the same zip file. Ideally we would use a proper lock here, but that would require
        # a bit of refactoring, and I want to get this experimental fix out to one of our users quickly.
        for _ in range(6):
            try:
                return ChromeDriverManager().install()
            except:
                time.sleep(random())
        return None

    def setup_index_map(self):
        num_scratch_bots = len(self.running_indices)
        sorted_indices = list(self.running_indices)
        sorted_indices.sort()
        self.scratch_index_to_rlbot = {
            i: r
            for i, r in enumerate(sorted_indices)
        }

        leftovers = [
            i for i in range(0, max(self.running_indices))
            if i not in self.running_indices
        ]
        leftovers.sort()
        for i in range(num_scratch_bots, num_scratch_bots + len(leftovers)):
            self.scratch_index_to_rlbot[i] = leftovers[i - num_scratch_bots]

    def get_rlbot_index(self, scratch_index):
        if scratch_index in self.scratch_index_to_rlbot:
            return self.scratch_index_to_rlbot[scratch_index]
        return scratch_index

    async def game_loop(self):

        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

        packet = GameTickPacket()

        # Run until main process tells to stop
        while not self.quit_event.is_set():
            before = datetime.now()

            self.game_interface.update_live_data_packet(packet)

            # Run the Agent only if the gameInfo has updated.
            tick_game_time = packet.game_info.seconds_elapsed
            worth_communicating = tick_game_time != last_tick_game_time or \
                                  datetime.now() - last_call_real_time >= MAX_AGENT_CALL_PERIOD

            ball = packet.game_ball
            if ball is not None and worth_communicating and max(
                    self.running_indices) < packet.num_cars:
                last_tick_game_time = tick_game_time
                last_call_real_time = datetime.now()

                tiny_player_offsets = []
                builder = flatbuffers.Builder(0)

                for i in range(packet.num_cars):
                    tiny_player_offsets.append(
                        self.copy_player(packet.game_cars[i], builder))

                TinyPacket.TinyPacketStartPlayersVector(
                    builder, packet.num_cars)
                for i in reversed(range(0, len(tiny_player_offsets))):
                    rlbot_index = self.get_rlbot_index(i)
                    builder.PrependUOffsetTRelative(
                        tiny_player_offsets[rlbot_index])
                players_offset = builder.EndVector(len(tiny_player_offsets))

                ballOffset = self.copy_ball(ball, builder)

                TinyPacket.TinyPacketStart(builder)
                TinyPacket.TinyPacketAddPlayers(builder, players_offset)
                TinyPacket.TinyPacketAddBall(builder, ballOffset)
                packet_offset = TinyPacket.TinyPacketEnd(builder)

                builder.Finish(packet_offset)
                buffer = bytes(builder.Output())

                filtered_sockets = {s for s in self.current_sockets if s.open}
                for socket in filtered_sockets:
                    await socket.send(buffer)

                self.current_sockets = filtered_sockets

            after = datetime.now()
            duration = (after - before).total_seconds()

            sleep_secs = 1 / 60 - duration
            if sleep_secs > 0:
                await asyncio.sleep(sleep_secs)

    def convert_to_flatbuffer(self, json_state: dict, index: int):
        builder = flatbuffers.Builder(0)

        ControllerState.ControllerStateStart(builder)
        ControllerState.ControllerStateAddSteer(builder, json_state['steer'])
        ControllerState.ControllerStateAddThrottle(builder,
                                                   json_state['throttle'])
        ControllerState.ControllerStateAddPitch(builder, json_state['pitch'])
        ControllerState.ControllerStateAddYaw(builder, json_state['yaw'])
        ControllerState.ControllerStateAddRoll(builder, json_state['roll'])
        ControllerState.ControllerStateAddJump(builder, json_state['jump'])
        ControllerState.ControllerStateAddBoost(builder, json_state['boost'])
        ControllerState.ControllerStateAddHandbrake(builder,
                                                    json_state['handbrake'])

        # This may throw a KeyError for anyone using old cached javascript. You should hard-refresh scratch.rlbot.org.
        ControllerState.ControllerStateAddUseItem(builder,
                                                  json_state['useItem'])
        controller_state = ControllerState.ControllerStateEnd(builder)

        PlayerInput.PlayerInputStart(builder)
        PlayerInput.PlayerInputAddPlayerIndex(builder, index)
        PlayerInput.PlayerInputAddControllerState(builder, controller_state)
        player_input = PlayerInput.PlayerInputEnd(builder)

        builder.Finish(player_input)
        return builder

    def copy_v3(self, v3, builder):
        if self.should_flip_field:
            return Vector3.CreateVector3(builder, -v3.x, -v3.y, v3.z)
        return Vector3.CreateVector3(builder, v3.x, v3.y, v3.z)

    def copy_rot(self, rot, builder):
        yaw = rot.yaw
        if self.should_flip_field:
            yaw = yaw + math.pi if yaw < 0 else yaw - math.pi
        return Rotator.CreateRotator(builder, rot.pitch, yaw, rot.roll)

    def copy_player(self, player, builder):
        TinyPlayer.TinyPlayerStart(builder)
        TinyPlayer.TinyPlayerAddLocation(
            builder, self.copy_v3(player.physics.location, builder))
        TinyPlayer.TinyPlayerAddVelocity(
            builder, self.copy_v3(player.physics.velocity, builder))
        TinyPlayer.TinyPlayerAddRotation(
            builder, self.copy_rot(player.physics.rotation, builder))
        TinyPlayer.TinyPlayerAddTeam(
            builder,
            invert_team(player.team)
            if self.should_flip_field else player.team)
        TinyPlayer.TinyPlayerAddBoost(builder, player.boost)
        return TinyPlayer.TinyPlayerEnd(builder)

    def copy_ball(self, ball, builder):
        phys = ball.physics
        TinyBall.TinyBallStart(builder)
        TinyBall.TinyBallAddLocation(builder,
                                     self.copy_v3(phys.location, builder))
        TinyBall.TinyBallAddVelocity(builder,
                                     self.copy_v3(phys.velocity, builder))
        return TinyBall.TinyBallEnd(builder)
Exemple #6
0
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
Exemple #7
0
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
Exemple #8
0
class Hivemind(BotHelperProcess):
    # TODO Maybe use __slots__ for better performance?

    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('Hivemind')
        self.game_interface = GameInterface(self.logger)
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        """Runs once, sets up the hivemind and its agents."""
        # Prints stuff into the console.
        self.logger.info("Hivemind A C T I V A T E D")
        message = random.choice([
            "Breaking the meta",
            "Welcoming r0bbi3",
            "Annoying chip by reinventing the wheel",
            "Actually texting her",
            "Banning anime",
            "Killing that guy",
            "Trying to pronounce jeroen",
            "Getting banned by Redox",
            "Becomind a mod",
        ])
        self.logger.info(message)

        # Loads game interface.
        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata.
        time.sleep(1)
        self.try_receive_agent_metadata()

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Setting up rate limiter.
        rate_limit = rate_limiter.RateLimiter(120)

        # Setting up data.
        field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(field_info)
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        data.setup(self, packet, field_info, self.running_indices)

        self.ball.predict = BallPrediction()
        # https://github.com/RLBot/RLBotPythonExample/wiki/Ball-Path-Prediction

        # MAIN LOOP:
        while True:
            # Updating the game packet from the game.
            self.game_interface.update_live_data_packet(packet)

            # Processing packet.
            data.process(self, packet)

            # Ball prediction.
            self.game_interface.update_ball_prediction(self.ball.predict)

            # Planning.
            brain.plan(self)

            # Rendering.
            self.render_debug(self.game_interface.renderer)

            # For each drone under the hivemind's control, do something.
            for drone in self.drones:

                # The controls are reset each frame.
                drone.ctrl = PlayerInput(
                )  # Basically the same as SimpleControllerState().

                # Role execution.
                if drone.role is not None:
                    drone.role.execute(self, drone)

                    self.render_role(self.game_interface.renderer, drone)

                # Send the controls to the bots.
                self.game_interface.update_player_input(
                    drone.ctrl, drone.index)

            # Rate limit sleep.
            rate_limit.acquire()

    def render_debug(hive, rndr):
        """Debug rendering for all manner of things.
        
        Arguments:
            hive {Hivemind} -- The hivemind.
            rndr {?} -- The renderer.
        """
        # Rendering Ball prediction.
        locations = [
            step.physics.location for step in hive.ball.predict.slices
        ]
        rndr.begin_rendering('ball prediction')
        rndr.draw_polyline_3d(locations, rndr.pink())
        rndr.end_rendering()

    def render_role(hive, rndr, drone):
        """Renders roles above the drones.
        
        Arguments:
            hive {Hivemind} -- The hivemind.
            rndr {?} -- The renderer.
            drone {Drone} -- The drone who's role is being rendered.
        """
        # Rendering role names above drones.
        above = drone.pos + a3l([0, 0, 100])
        rndr.begin_rendering(f'role_{hive.team}_{drone.index}')
        rndr.draw_string_3d(above, 1, 1, drone.role.name, rndr.cyan())
        rndr.end_rendering()
Exemple #9
0
class ExampleHivemind(BotHelperProcess):

    # Some terminology:
    # hivemind = the process which controls the drones.
    # drone = a bot under the hivemind's control.

    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.
        # Call this something unique so people can differentiate between hiveminds.
        self.logger = get_logger('Example Hivemind')

        # 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)

        # Running indices is a set of bot indices
        # which requested this hivemind with the same key.
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        """Adds all drones with the correct key to our set of running indices."""
        while True:  # will exit on queue.Empty
            try:
                # Adds drone indices to running_indices.
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        """Runs once, sets up the hivemind and its agents."""
        # Prints an activation message into the console.
        # This let's you know that the process is up and running.
        self.logger.info("Hello World!")

        # Loads game interface.
        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata.
        self.logger.info("Snoozing for 3 seconds; give me a moment.")
        time.sleep(3)
        self.try_receive_agent_metadata()

        # This is how you access field info.
        # First create the initialise the object...
        field_info = FieldInfoPacket()
        # Then update it.
        self.game_interface.update_field_info_packet(field_info)

        # Same goes for the packet, but that is
        # also updated in the main loop every tick.
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)
        # Ball prediction works the same. Check the main loop.

        # Create a Ball object for the ball that holds its information.
        self.ball = Ball()

        # Create a Drone object for every drone that holds its information.
        self.drones = []
        for index in range(packet.num_cars):
            if index in self.running_indices:
                self.drones.append(Drone(index, packet.game_cars[index].team))

        # Other attribute initialisation.
        self.state = State.SETUP
        self.pinch_target = None

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Creating packet and ball prediction objects which will be updated every tick.
        packet = GameTickPacket()
        ball_prediction = BallPrediction()

        # Nicknames the renderer to shorten code.
        draw = self.game_interface.renderer

        # MAIN LOOP:
        while True:

            previous_packet = packet

            # Updating the game tick packet.
            self.game_interface.update_live_data_packet(packet)

            # Checking if packet is new, otherwise sleep.
            if previous_packet.game_info.seconds_elapsed == packet.game_info.seconds_elapsed:
                time.sleep(0.001)

            else:
                # Begins rendering at the start of the loop; makes life easier.
                # https://discordapp.com/channels/348658686962696195/446761380654219264/610879527089864737
                draw.begin_rendering(f'Hivemind{self.drones[0].team}')

                # PRE-PROCESSING:

                # Updates the ball prediction.
                self.game_interface.update_ball_prediction(ball_prediction)

                # Processing ball data.
                self.ball.pos = a3v(packet.game_ball.physics.location)

                # Processing drone data.
                for drone in self.drones:
                    drone.pos = a3v(packet.game_cars[drone.index].physics.location)
                    drone.rot = a3r(packet.game_cars[drone.index].physics.rotation)
                    drone.vel = a3v(packet.game_cars[drone.index].physics.velocity)
                    drone.boost = packet.game_cars[drone.index].boost
                    drone.orient_m = orient_matrix(drone.rot)

                    # Reset ctrl every tick.
                    # PlayerInput is practically identical to SimpleControllerState.
                    drone.ctrl = PlayerInput()

                # Game time.
                game_time = packet.game_info.seconds_elapsed

                # Example Team Pinches (2 bots only)
                # There's nothing stopping you from doing it with more ;) Give it a shot!
                if len(self.drones) == 2:

                    # Sorts the drones left to right. (More understandble code below)
                    #right_to_left_drones = sorted(self.drones, key=lambda drone: drone.pos[0]*team_sign(drone.team))

                    # Finds the right and left drones.
                    sign = team_sign(self.drones[0].team)
                    if self.drones[0].pos[0]*sign <= self.drones[1].pos[0]*sign:
                        right = self.drones[0]
                        left = self.drones[1]
                    else:
                        right = self.drones[1]
                        left = self.drones[0]

                    # Bots get boost and go to wait positions.
                    if self.state == State.SETUP:

                        # Some guide positions.
                        right_boost = a3l([-3072.0, -4096.0, 71.1])*sign
                        right_wait = a3l([-1792.0, -4184.0, 71.1])*sign
                        # Making use of symmetry
                        left_boost = right_boost * a3l([-1, 1, 1])
                        left_wait = right_wait * a3l([-1, 1, 1])

                        # First get boost and then go to wait position.
                        if right.boost < 100:
                            slow_to_pos(right, right_boost)
                        else:
                            slow_to_pos(right, right_wait)

                        if left.boost < 100:
                            slow_to_pos(left, left_boost)
                        else:
                            slow_to_pos(left, left_wait)

                        # If both bots are in wait position, switch to WAIT state.
                        if np.linalg.norm(right.pos-right_wait) + np.linalg.norm(left.pos-left_wait) < 200:
                            self.state = State.WAIT

                    # Bots try to face the ball, waiting for perfect moment to team pinch.
                    elif self.state == State.WAIT:

                        # Each drone should try to face the ball.
                        for drone in self.drones:
                            turn_to_pos(drone, self.ball.pos, game_time)

                        # Filters out all the predictions where the ball is too far off the ground.
                        # Result is a list of tuples of positions and time.
                        filtered_prediction = [(a3v(step.physics.location), step.game_seconds)
                                            for step in ball_prediction.slices if step.physics.location.z < 100]

                        if len(filtered_prediction) > 0:
                            # Turns the predition into a numpy array for fast vectorized calculations.
                            filtered_prediction = np.array(filtered_prediction)

                            # Gets the vectors from the drones to the ball prediction.
                            positions = np.vstack(filtered_prediction[:, 0])
                            right_to_prediction = positions - right.pos
                            left_to_prediction = positions - left.pos

                            # Calculates the distances.
                            # Cool blog post about einsum: http://ajcr.net/Basic-guide-to-einsum/
                            right_distances = np.sqrt(
                                np.einsum('ij,ij->i', right_to_prediction, right_to_prediction))
                            left_distances = np.sqrt(
                                np.einsum('ij,ij->i', left_to_prediction, left_to_prediction))

                            # Filters out the predictions which are too close or too far.
                            good_distances = (CLOSEST <= right_distances) & (FARTHEST >= right_distances) & (
                                CLOSEST <= left_distances) & (FARTHEST >= left_distances)
                            valid_targets = filtered_prediction[good_distances]

                            if len(valid_targets) > 0:
                                # Getting the remaining distances after filter.
                                right_distances = right_distances[good_distances]
                                left_distances = left_distances[good_distances]

                                # Getting time estimates to go that distance. (Assuming boosting, and going in a straight line.)
                                # https://www.geogebra.org/m/nnsat4pj
                                right_times = right_distances**0.55 / 41.53
                                right_times[right_distances > 2177.25] = 1/2300 * \
                                    right_distances[right_distances > 2177.25] + 0.70337
                                right_times += game_time + TIME_BUFFER

                                left_times = left_distances**0.55 / 41.53
                                left_times[left_distances > 2177.25] = 1/2300 * \
                                    left_distances[left_distances > 2177.25] + 0.70337
                                left_times += game_time + TIME_BUFFER

                                # Filters out the predictions which we can't get to.
                                good_times = (valid_targets[:, 1] > right_times) & (
                                    valid_targets[:, 1] > left_times)
                                valid_targets = valid_targets[good_times]

                                # To avoid flukes or anomalies, check that the ball is valid for at least 10 steps.
                                # Not exact because there could be more bounce spots but good enough to avoid flukes.
                                if len(valid_targets) > 10:
                                    # Select first valid target.
                                    self.pinch_target = valid_targets[0]
                                    # Reset drone's going attribute.
                                    right.going = False
                                    left.going = False
                                    # Set the state to PINCH.
                                    self.state = State.PINCH

                        # Rendering number of positions viable after each condition.
                        draw.draw_string_2d(
                            10, 70, 2, 2, f'Good height: {len(filtered_prediction)}', draw.white())
                        draw.draw_string_2d(
                            10, 100, 2, 2, f'Good distance: {len(valid_targets)}', draw.white())
                        # Render circles to show distances.
                        draw.draw_polyline_3d(make_circle(
                            CLOSEST, right.pos, 20), draw.cyan())
                        draw.draw_polyline_3d(make_circle(
                            CLOSEST, left.pos, 20), draw.cyan())
                        draw.draw_polyline_3d(make_circle(
                            FARTHEST, right.pos, 30), draw.pink())
                        draw.draw_polyline_3d(make_circle(
                            FARTHEST, left.pos, 30), draw.pink())

                    elif self.state == State.PINCH:

                        # Checks if the ball has been hit recently.
                        if packet.game_ball.latest_touch.time_seconds + 0.1 > game_time:
                            self.pinch_target = None
                            self.state = State.SETUP

                        elif self.pinch_target is not None:
                            if not right.going:
                                # Get the distance to the target.
                                right_distance = np.linalg.norm(
                                    self.pinch_target[0] - right.pos)
                                # Get a time estimate
                                right_time = right_distance**0.55 / \
                                    41.53 if right_distance <= 2177.25 else 1/2300 * right_distance + 0.70337

                                # Waits until time is right to go. Otherwise turns to face the target position.
                                if game_time + right_time + TIME_ERROR >= self.pinch_target[1]:
                                    right.going = True
                                else:
                                    turn_to_pos(
                                        right, self.pinch_target[0], game_time)

                            else:
                                fast_to_pos(right, self.pinch_target[0])

                            # Same for left.
                            if not left.going:
                                left_distance = np.linalg.norm(
                                    self.pinch_target[0] - left.pos)
                                left_time = left_distance**0.55 / \
                                    41.53 if left_distance <= 2177.25 else 1/2300 * left_distance + 0.70337
                                if game_time + left_time + TIME_ERROR >= self.pinch_target[1]:
                                    left.going = True
                                else:
                                    turn_to_pos(
                                        left, self.pinch_target[0], game_time)
                            else:
                                fast_to_pos(left, self.pinch_target[0])

                            # Some rendering.
                            draw.draw_string_2d(
                                10, 70, 2, 2, f'Right going: {right.going}', draw.white())
                            draw.draw_string_2d(
                                10, 100, 2, 2, f'Left going: {left.going}', draw.white())

                else:
                    draw.draw_string_2d(
                        10, 10, 2, 2, 'This example version has only been coded for 2 HiveBots.', draw.red())

                # Use this to send the drone inputs to the drones.
                for drone in self.drones:
                    self.game_interface.update_player_input(
                        drone.ctrl, drone.index)

                # Some example rendering:
                draw.draw_string_2d(10, 10, 3, 3, f'{self.state}', draw.pink())
                # Renders ball prediction
                path = [step.physics.location for step in ball_prediction.slices[::10]]
                draw.draw_polyline_3d(path, draw.pink())

                # Renders drone indices.
                for drone in self.drones:
                    draw.draw_string_3d(drone.pos, 1, 1, str(
                        drone.index), draw.white())

                # Team pinch info.
                if self.pinch_target is not None:
                    draw.draw_rect_3d(
                        self.pinch_target[0], 10, 10, True, draw.red())

                # Ending rendering.
                draw.end_rendering()
Exemple #10
0
class ExecutableWithSocketAgent(BaseIndependentAgent):
    def __init__(self, name, team, index):
        super().__init__(name, team, index)
        self.logger = get_logger('ExeSocket' + str(self.index))
        self.is_retired = False
        self.executable_path = None
        self.game_interface = GameInterface(self.logger)
        self.game_tick_packet = GameTickPacket()
        self.spawn_id_seen = False

    def run_independently(self, terminate_request_event):
        self.game_interface.load_interface()

        while not terminate_request_event.is_set():

            self.game_interface.update_live_data_packet(self.game_tick_packet)
            packet_spawn_id = self.game_tick_packet.game_cars[
                self.index].spawn_id
            if self.spawn_id_seen:
                if packet_spawn_id != self.spawn_id:
                    break  # This will cause the bot to retire.
            elif packet_spawn_id == self.spawn_id and self.game_tick_packet.game_info.is_round_active:
                self.spawn_id_seen = True

            # Continuously make sure the the bot is registered.
            # These functions can be called repeatedly without any bad effects.
            # This is useful for re-engaging the socket server if it gets restarted during development.
            message = self.build_add_command()
            self.send_command(message)
            time.sleep(1)

    def get_helper_process_request(self):
        if self.is_executable_configured():
            return HelperProcessRequest(
                python_file_path=None,
                key=__file__ + str(self.get_port()),
                executable=self.executable_path,
                exe_args=[str(self.get_port())],
                current_working_directory=os.path.dirname(
                    self.executable_path))
        return None

    def retire(self):
        message = self.build_retire_command()
        self.logger.info(f"Sending retire message for {self.name}")
        self.send_command(message)
        self.is_retired = True

    def build_add_command(self) -> str:
        return f"add\n{self.name}\n{self.team}\n{self.index}\n{game_interface.get_dll_directory()}"

    def build_retire_command(self) -> str:
        return f"remove\n{self.index}"

    def send_command(self, message):
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.settimeout(4)
            s.connect(("127.0.0.1", self.get_port()))
            s.send(bytes(message, "ASCII"))
            s.close()
            return True
        except ConnectionRefusedError:
            self.logger.warn("Could not connect to server!")
            return False

    def is_executable_configured(self):
        return self.executable_path is not None and os.path.isfile(
            self.executable_path)

    def get_extra_pids(self):
        """
        Gets the list of process ids that should be marked as high priority.
        :return: A list of process ids that are used by this bot in addition to the ones inside the python process.
        """

        if self.is_executable_configured():
            # The helper process will start the exe and report the PID. Nothing to do here.
            return []

        while not self.is_retired:
            for proc in psutil.process_iter():
                for conn in proc.connections():
                    if conn.laddr.port == self.get_port():
                        self.logger.debug(
                            f'server for {self.name} appears to have pid {proc.pid}'
                        )
                        return [proc.pid]
            time.sleep(1)
            if self.executable_path is None:
                self.logger.info(
                    "Can't auto-start because no executable is configured. Please start manually!"
                )
            else:
                self.logger.info(
                    f"Can't auto-start because {self.executable_path} is not found. "
                    "Please start manually!")

    def get_port(self) -> int:
        raise NotImplementedError
Exemple #11
0
class Hivemind(BotHelperProcess):
    def __init__(self, agent_metadata_queue, quit_event, options):
        super().__init__(agent_metadata_queue, quit_event, options)
        self.logger = get_logger('Hivemind')
        self.game_interface = GameInterface(self.logger)
        self.running_indices = set()

    def try_receive_agent_metadata(self):
        while True:  # will exit on queue.Empty
            try:
                single_agent_metadata: AgentMetadata = self.metadata_queue.get(
                    timeout=0.1)
                self.running_indices.add(single_agent_metadata.index)
            except queue.Empty:
                return
            except Exception as ex:
                self.logger.error(ex)

    def start(self):
        """Runs once, sets up the hivemind and its agents."""
        # Prints stuff into the console.
        self.logger.info("Hivemind A C T I V A T E D")
        self.logger.info("Breaking the meta")
        self.logger.info("Welcoming @r0bbi3#0269")

        # Loads game interface.
        self.game_interface.load_interface()

        # Wait a moment for all agents to have a chance to start up and send metadata.
        time.sleep(1)
        self.try_receive_agent_metadata()

        # Runs the game loop where the hivemind will spend the rest of its time.
        self.game_loop()

    def game_loop(self):
        """The main game loop. This is where your hivemind code goes."""

        # Setting up rate limiter.
        rate_limit = rate_limiter.RateLimiter(120)

        # Setting up data.
        field_info = FieldInfoPacket()
        self.game_interface.update_field_info_packet(field_info)
        packet = GameTickPacket()
        self.game_interface.update_live_data_packet(packet)

        data.setup(self, packet, field_info, self.running_indices)

        self.ball.predict = BallPrediction()

        # MAIN LOOP:
        while True:
            # Updating the game packet from the game.
            self.game_interface.update_live_data_packet(packet)

            # Processing packet.
            data.process(self, packet)

            # Ball prediction.
            self.game_interface.update_ball_prediction(self.ball.predict)

            brain.think(self)

            for drone in self.drones:
                drone.ctrl = PlayerInput()
                if drone.role is not None:
                    drone.role.execute(self, drone)
                self.game_interface.update_player_input(
                    drone.ctrl, drone.index)

            self.draw_debug()

            # Rate limit sleep.
            rate_limit.acquire()

    def draw_debug(self):
        self.game_interface.renderer.begin_rendering()
        path = [
            a3v(step.physics.location) for step in self.ball.predict.slices
        ]
        self.game_interface.renderer.draw_polyline_3d(
            path, self.game_interface.renderer.pink())

        for drone in self.drones:
            if drone.role is not None:
                self.game_interface.renderer.draw_string_3d(
                    drone.pos, 1, 1, drone.role.name,
                    self.game_interface.renderer.white())
        self.game_interface.renderer.end_rendering()
Exemple #12
0
class Commentator():
    def __init__(self):
        self.game_interface = GameInterface(get_logger("Commentator"))
        self.game_interface.load_interface()
        self.game_interface.wait_until_loaded()
        self.touchTimer = 0
        self.currentTime = 0
        self.firstIter = True
        self.overTime = False
        self.shotDetection = True
        self.ballHistory = []
        self.lastTouches = []
        self.teams = []
        self.joinTimer = 0
        self.q = Queue(maxsize=3)
        self.host = threading.Thread(target=host, args=(self.q,))
        self.host.start()
        self.main()
        self.host.join()

    def reset(self):
        self.touchTimer = 0
        self.currentTime = 0
        self.firstIter = True
        self.overTime = False
        self.shotDetection = True
        self.ballHistory = []
        self.lastTouches = []
        self.teams = []
        self.joinTimer = 0
        with self.q.mutex:
            self.q.queue.clear()

    def speak(self, phrase):
        if not self.q.full():
            self.q.put(Comment(phrase, random.randint(0, 1)))

    def timeCheck(self, newTime):
        if newTime - self.currentTime < -1:
            return True
        self.currentTime = newTime
        return False

    def overtimeCheck(self,packet):
        if not self.overTime:
            if packet.game_info.is_overtime:
                self.overTime = True
                self.speak(f"That's the end of regulation time, we're headed into over time with the score tied at {packet.teams[0].score}!")

    def gameWrapUp(self):
        if self.teams[0].score > self.teams[1].score:
            winner = "Blue"
        else:
            winner = "Orange"

        if abs(self.teams[0].score - self.teams[1].score) >=4:
            self.speak(f"Team {winner} has won today's match with a dominant performance.")
            #impressive victory
        else:
            #normal win message
            self.speak(f"Team {winner} clinched the victory this match")

        self.speak("Thank you all for watching today's game and never forget that Diablo is coming for you. G G everyone.")

    def stopHost(self):
        while self.q.full():
            pass
        self.q.put("exit")

    def handleShotDetection(self):
        if self.shotDetection:
            if len(self.ballHistory) > 0:
                shot,goal = shotDetection(self.ballHistory[-1],1)
                if shot:
                    if not self.q.full():
                        if self.lastTouches[-1].team == goal:
                            self.speak(f"That's a potential own goal from {self.lastTouches[-1].player_name}.")
                        else:
                            self.speak(f"{self.lastTouches[-1].player_name} takes a shot at the enemy net!")
                    self.shotDetection = False




    def updateTouches(self, packet):
        contactNames = ["hit","touch","contact"]

        try:
            touch = ballTouch(packet.game_ball.latest_touch)
        except Exception as e:
            touch = None
            print(e)

        if touch:
            if len(self.lastTouches) < 1 or self.lastTouches[-1] != touch:
                self.lastTouches.append(touch)
                self.shotDetection = True
                for team in self.teams:
                    team.update(touch)
                if self.currentTime - self.touchTimer >=4:
                    if self.q.empty():
                        if len(self.ballHistory) >0:
                            _ballHeading = ballHeading(self.ballHistory[-1])
                            if _ballHeading == 0:
                                if touch.team == 0:
                                    if self.ballHistory[-1].location[1] >= 0:
                                        self.speak(
                                            f"{touch.player_name}'s {contactNames[random.randint(0,2)]} pushes the ball back towards blue")
                                    else:
                                        self.speak(
                                            f"{touch.player_name}'s {contactNames[random.randint(0,2)]} moves the ball towards its own goal.")
                                else:
                                    if touch.team == 1:
                                        if self.ballHistory[-1].location[1] <= 0:
                                            self.speak(
                                                f"{touch.player_name}'s {contactNames[random.randint(0,2)]} puts the ball into a dangerous position for blue.")
                                        else:
                                            self.speak(
                                                f"{touch.player_name}'s {contactNames[random.randint(0,2)]} sends the ball towards blue side.")

                            elif _ballHeading == 1:
                                if touch.team == 0:
                                    if self.ballHistory[-1].location[1] >= 0:
                                        self.speak(
                                            f"{touch.player_name}'s {contactNames[random.randint(0,2)]} puts the ball into a dangerous position for orange.")
                                    else:
                                        self.speak(
                                            f"{touch.player_name}'s {contactNames[random.randint(0,2)]} sends the ball towards orange side.")
                                else:
                                    if touch.team == 1:
                                        if self.ballHistory[-1].location[1] >= 0:
                                            self.speak(
                                                f"{touch.player_name}'s {contactNames[random.randint(0,2)]} moves the ball towards its own goal.")
                                        else:
                                            self.speak(
                                                f"{touch.player_name}'s {contactNames[random.randint(0,2)]} pushes the ball back  towards orange")

                            else:
                                self.speak(f"{touch.player_name}'s {contactNames[random.randint(0,2)]} is neutral.")

                            self.touchTimer = self.currentTime


    def updateGameBall(self,packet):
        if packet.game_info.is_round_active:
            currentBall = ballObject(packet.game_ball)
            self.ballHistory.append(currentBall)
        if len(self.ballHistory) >1000:
            del self.ballHistory[0]

    def gatherMatchData(self, packet):
        members = [[], []]
        for i in range(packet.num_cars):
            _car = Car(packet.game_cars[i].name, packet.game_cars[i].team, i)
            members[_car.team].append(_car)

        self.teams.append(Team(0, members[0]))
        self.teams.append(Team(1, members[1]))
        self.speak(
            f"We have an exciting match in store for you today. On team blue We have {', '.join([x.name for x in self.teams[0].members])} ")
        self.speak(
            f" and facing off against them on orange team we have {', '.join([x.name for x in self.teams[1].members])} .")
        self.speak("Good luck everyone.")

    def scoreAnnouncement(self,teamIndex):
        try:
            scorer = self.teams[teamIndex].lastTouch.player_name
            speed = self.ballHistory[-1].getRealSpeed()
            if not self.q.full():
                if speed <= 20:
                    self.speak(f"{scorer} scores! It barely limped across the goal line at {speed} kilometers per hour, but a goal is a goal.")

                elif speed >= 100:
                    self.speak(f"{scorer} scores on a blazingly fast shot at  {speed} kilometers per hour! What a shot!")

                else:
                    self.speak(f"And {scorer}'s shot goes in at {speed} kilometers per hour!")

            if not self.q.full():
                self.speak(f"That goal brings the score to {self.teams[0].score} blue and {self.teams[1].score} orange.")
        except:
            pass

    def scoreCheck(self, packet):
        if self.teams[0].score != packet.teams[0].score:
            self.teams[0].score = packet.teams[0].score
            self.scoreAnnouncement(0)

        if self.teams[1].score != packet.teams[1].score:
            self.teams[1].score = packet.teams[1].score
            self.scoreAnnouncement(1)

    def main(self):
        while True:
            packet = GameTickPacket()
            self.game_interface.update_live_data_packet(packet)
            gametime = "{:.2f}".format(packet.game_info.seconds_elapsed)

            if packet.game_info.is_match_ended:
                print("Game is over, exiting.")
                self.gameWrapUp()
                self.stopHost()
                break

            if self.firstIter:
                if packet.num_cars >= 1:
                    if self.joinTimer <= 0:
                        self.joinTimer = time.time()
                    if time.time() - self.joinTimer >=1: #arbitrary timer to ensure all cars connected
                        self.firstIter = False
                        self.currentTime = float(gametime)
                        self.gatherMatchData(packet)

            if self.timeCheck(float(gametime)):
                print("framework reset, resetting announcerbot")
                self.reset()
            if not self.firstIter:
                self.updateGameBall(packet)
                self.updateTouches(packet)
                self.handleShotDetection()
                self.scoreCheck(packet)
                self.overtimeCheck(packet)