diff --git a/pyproject.toml b/pyproject.toml index 408021b..21019fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "A high performance Python interface for communicating with RLBot dynamic = ["version"] requires-python = ">= 3.11" dependencies = [ - "rlbot_flatbuffers~=0.16.0", + "rlbot_flatbuffers~=0.17.0", "psutil==7.*", ] readme = "README.md" diff --git a/rlbot/config.py b/rlbot/config.py index 20a1946..6d60049 100644 --- a/rlbot/config.py +++ b/rlbot/config.py @@ -94,30 +94,30 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: match variant: case "rlbot": - variety, use_config = flat.CustomBot(), True + abs_config_path = (config_path.parent / car_config).resolve() + players.append( + load_player_config(abs_config_path, team, name, loadout_file) + ) case "psyonix": - variety, use_config = flat.Psyonix(skill), True + abs_config_path = ( + (config_path.parent / car_config).resolve() if car_config else None + ) + players.append( + load_psyonix_config( + team, + skill, + name, + loadout_file, + abs_config_path, + ) + ) case "human": - variety, use_config = flat.Human(), False - case "partymember": - logger.warning("PartyMember player type is not supported yet.") - variety, use_config = flat.PartyMember(), False + players.append(flat.PlayerConfiguration(flat.Human(), team, 0)) case t: raise ConfigParsingException( f"Invalid player type {repr(t)} for player {len(players)}." ) - if use_config and car_config: - abs_config_path = (config_path.parent / car_config).resolve() - players.append( - load_player_config(abs_config_path, variety, team, name, loadout_file) # type: ignore - ) - else: - loadout = load_player_loadout(loadout_file, team) if loadout_file else None - players.append( - flat.PlayerConfiguration(variety, name, team, loadout=loadout) - ) - scripts: list[flat.ScriptConfiguration] = [] for script_table in config.get("scripts", []): if script_config := __str(script_table, "config_file"): @@ -155,6 +155,17 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: audio=__enum(mutator_table, "audio", flat.AudioMutator), ) + try: + enable_rendering = ( + flat.DebugRendering.OnByDefault + if __bool(match_table, "enable_rendering") + else flat.DebugRendering.OffByDefault + ) + except ConfigParsingException: + enable_rendering = __enum( + match_table, "enable_rendering", flat.DebugRendering.AlwaysOff + ) + return flat.MatchConfiguration( launcher=__enum(rlbot_table, "launcher", flat.Launcher), launcher_arg=__str(rlbot_table, "launcher_arg"), @@ -170,7 +181,7 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: existing_match_behavior=__enum( match_table, "existing_match_behavior", flat.ExistingMatchBehavior ), - enable_rendering=__bool(match_table, "enable_rendering"), + enable_rendering=enable_rendering, enable_state_setting=__bool(match_table, "enable_state_setting"), freeplay=__bool(match_table, "freeplay"), ) @@ -217,8 +228,7 @@ def load_player_loadout(path: Path | str, team: int) -> flat.PlayerLoadout: def load_player_config( - path: Path | str | None, - type: flat.CustomBot | flat.Psyonix, + path: Path | str, team: int, name_override: str | None = None, loadout_override: Path | str | None = None, @@ -227,20 +237,6 @@ def load_player_config( Reads the bot toml file at the provided path and creates a `PlayerConfiguration` of the given type for the given team. """ - if path is None: - loadout = ( - load_player_loadout(loadout_override, team) - if loadout_override is not None - else None - ) - - return flat.PlayerConfiguration( - type, - name_override or "", - team, - loadout=loadout, - ) - path = Path(path) with open(path, "rb") as f: config = tomllib.load(f) @@ -266,15 +262,64 @@ def load_player_config( ) return flat.PlayerConfiguration( - type, - name_override or __str(settings, "name"), + flat.CustomBot( + name_override or __str(settings, "name"), + str(root_dir), + run_command, + loadout, + __str(settings, "agent_id"), + __bool(settings, "hivemind"), + ), + team, + 0, + ) + + +def load_psyonix_config( + team: int, + skill_level: flat.PsyonixSkill, + name_override: str | None = None, + loadout_override: Path | str | None = None, + path: Path | str | None = None, +) -> flat.PlayerConfiguration: + """ + Creates a `PlayerConfiguration` for a Psyonix bot of the given team and skill. + If a path is provided, it will be used override the default name and loadout. + """ + name = name_override + loadout_path = loadout_override + + # Don't parse the toml file if we have no data we need to extract, + # even if a path to a toml file is provided. + if path is not None and (name is None or loadout_path is None): + path = Path(path) + with open(path, "rb") as f: + config = tomllib.load(f) + + settings = __table(config, "settings") + + if name is None: + name = __str(settings, "name") + + if loadout_path is None: + loadout_path = ( + path.parent / Path(__str(settings, "loadout_file")) + if "loadout_file" in settings + else None + ) + + loadout = ( + load_player_loadout(loadout_path, team) if loadout_path is not None else None + ) + + return flat.PlayerConfiguration( + flat.PsyonixBot( + name or "", + loadout, + skill_level, + ), team, - str(root_dir), - run_command, - loadout, 0, - __str(settings, "agent_id"), - __bool(settings, "hivemind"), ) diff --git a/rlbot/interface.py b/rlbot/interface.py index feea530..aba08f0 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -1,7 +1,6 @@ import logging import time from collections.abc import Callable -from dataclasses import dataclass from enum import IntEnum from pathlib import Path from socket import IPPROTO_TCP, TCP_NODELAY, socket @@ -18,36 +17,6 @@ RLBOT_SERVER_PORT = 23234 -class SocketDataType(IntEnum): - """ - See https://github.com/RLBot/core/blob/master/RLBotCS/Types/DataType.cs - and https://wiki.rlbot.org/framework/sockets-specification/#data-types - """ - - NONE = 0 - GAME_PACKET = 1 - FIELD_INFO = 2 - START_COMMAND = 3 - MATCH_CONFIGURATION = 4 - PLAYER_INPUT = 5 - DESIRED_GAME_STATE = 6 - RENDER_GROUP = 7 - REMOVE_RENDER_GROUP = 8 - MATCH_COMMUNICATION = 9 - BALL_PREDICTION = 10 - CONNECTION_SETTINGS = 11 - STOP_COMMAND = 12 - SET_LOADOUT = 13 - INIT_COMPLETE = 14 - CONTROLLABLE_TEAM_INFO = 15 - - -@dataclass(repr=False, eq=False, frozen=True, match_args=False, slots=True) -class SocketMessage: - type: SocketDataType - data: bytes - - class MsgHandlingResult(IntEnum): TERMINATED = 0 NO_INCOMING_MSGS = 1 @@ -66,7 +35,6 @@ class SocketRelay: is_connected = False _running = False """Indicates whether a messages are being handled by the `run` loop (potentially in a background thread)""" - _ball_pred = flat.BallPrediction() on_connect_handlers: list[Callable[[], None]] = [] packet_handlers: list[Callable[[flat.GamePacket], None]] = [] @@ -77,7 +45,8 @@ class SocketRelay: controllable_team_info_handlers: list[ Callable[[flat.ControllableTeamInfo], None] ] = [] - raw_handlers: list[Callable[[SocketMessage], None]] = [] + rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] + raw_handlers: list[Callable[[flat.CoreMessage], None]] = [] def __init__( self, @@ -116,50 +85,43 @@ def _read_exact(self, n: int) -> bytes: pos += cr return bytes(buff) - def read_message(self) -> SocketMessage: - type_int = self._read_int() + def read_message(self) -> bytes: size = self._read_int() - data = self._read_exact(size) - return SocketMessage(SocketDataType(type_int), data) + return self._read_exact(size) - def send_bytes(self, data: bytes, data_type: SocketDataType): + def send_bytes(self, data: bytes): assert self.is_connected, "Connection has not been established" size = len(data) if size > MAX_SIZE_2_BYTES: - self.logger.error( - "Couldn't send %s message because it was too big!", data_type.name - ) + self.logger.error("Couldn't send message because it was too big!") return - message = self._int_to_bytes(data_type) + self._int_to_bytes(size) + data + message = self._int_to_bytes(size) + data self.socket.sendall(message) - def send_init_complete(self): - self.send_bytes(bytes(), SocketDataType.INIT_COMPLETE) - - def send_set_loadout(self, set_loadout: flat.SetLoadout): - self.send_bytes(set_loadout.pack(), SocketDataType.SET_LOADOUT) - - def send_match_comm(self, match_comm: flat.MatchComm): - self.send_bytes(match_comm.pack(), SocketDataType.MATCH_COMMUNICATION) - - def send_player_input(self, player_input: flat.PlayerInput): - self.send_bytes(player_input.pack(), SocketDataType.PLAYER_INPUT) - - def send_game_state(self, game_state: flat.DesiredGameState): - self.send_bytes(game_state.pack(), SocketDataType.DESIRED_GAME_STATE) - - def send_render_group(self, render_group: flat.RenderGroup): - self.send_bytes(render_group.pack(), SocketDataType.RENDER_GROUP) - - def remove_render_group(self, group_id: int): - flatbuffer = flat.RemoveRenderGroup(group_id).pack() - self.send_bytes(flatbuffer, SocketDataType.REMOVE_RENDER_GROUP) + def send_msg( + self, + msg: ( + flat.DisconnectSignal + | flat.StartCommand + | flat.MatchConfiguration + | flat.PlayerInput + | flat.DesiredGameState + | flat.RenderGroup + | flat.RemoveRenderGroup + | flat.MatchComm + | flat.ConnectionSettings + | flat.StopCommand + | flat.SetLoadout + | flat.InitComplete + | flat.RenderingStatus + ), + ): + self.send_bytes(flat.InterfacePacket(msg).pack()) def stop_match(self, shutdown_server: bool = False): - flatbuffer = flat.StopCommand(shutdown_server).pack() - self.send_bytes(flatbuffer, SocketDataType.STOP_COMMAND) + self.send_msg(flat.StopCommand(shutdown_server)) def start_match(self, match_config: Path | flat.MatchConfiguration): self.logger.info("Python interface is attempting to start match...") @@ -167,17 +129,15 @@ def start_match(self, match_config: Path | flat.MatchConfiguration): match match_config: case Path() as path: string_path = str(path.absolute().resolve()) - flatbuffer = flat.StartCommand(string_path).pack() - flat_type = SocketDataType.START_COMMAND + flatbuffer = flat.StartCommand(string_path) case flat.MatchConfiguration() as settings: - flatbuffer = settings.pack() - flat_type = SocketDataType.MATCH_CONFIGURATION + flatbuffer = settings case _: raise ValueError( "Expected MatchSettings or path to match settings toml file" ) - self.send_bytes(flatbuffer, flat_type) + self.send_msg(flatbuffer) def connect( self, @@ -242,13 +202,14 @@ def connect( for handler in self.on_connect_handlers: handler() - flatbuffer = flat.ConnectionSettings( - agent_id=self.agent_id, - wants_ball_predictions=wants_ball_predictions, - wants_comms=wants_match_communications, - close_between_matches=close_between_matches, - ).pack() - self.send_bytes(flatbuffer, SocketDataType.CONNECTION_SETTINGS) + self.send_msg( + flat.ConnectionSettings( + agent_id=self.agent_id, + wants_ball_predictions=wants_ball_predictions, + wants_comms=wants_match_communications, + close_between_matches=close_between_matches, + ) + ) def run(self, *, background_thread: bool = False): """ @@ -286,16 +247,14 @@ def handle_incoming_messages(self, blocking: bool = False) -> MsgHandlingResult: return self.handle_incoming_message(incoming_message) except flat.InvalidFlatbuffer as e: self.logger.error( - "Error while unpacking message of type %s (%s bytes): %s", - incoming_message.type.name, - len(incoming_message.data), + "Error while unpacking message (%s bytes): %s", + len(incoming_message), e, ) return MsgHandlingResult.TERMINATED except Exception as e: self.logger.error( - "Unexpected error while handling message of type %s: %s", - incoming_message.type.name, + "Unexpected error while handling message of type: %s", e, ) return MsgHandlingResult.TERMINATED @@ -306,56 +265,46 @@ def handle_incoming_messages(self, blocking: bool = False) -> MsgHandlingResult: self.logger.error("SocketRelay disconnected unexpectedly!") return MsgHandlingResult.TERMINATED - def handle_incoming_message( - self, incoming_message: SocketMessage - ) -> MsgHandlingResult: + def handle_incoming_message(self, incoming_message: bytes) -> MsgHandlingResult: """ Handles a messages by passing it to the relevant handlers. Returns True if the message was NOT a shutdown request (i.e. NONE). """ + flatbuffer = flat.CorePacket.unpack(incoming_message).message + for raw_handler in self.raw_handlers: - raw_handler(incoming_message) + raw_handler(flatbuffer) - match incoming_message.type: - case SocketDataType.NONE: + match flatbuffer.item: + case flat.DisconnectSignal(): return MsgHandlingResult.TERMINATED - case SocketDataType.GAME_PACKET: - if len(self.packet_handlers) > 0: - packet = flat.GamePacket.unpack(incoming_message.data) - for handler in self.packet_handlers: - handler(packet) - case SocketDataType.FIELD_INFO: - if len(self.field_info_handlers) > 0: - field_info = flat.FieldInfo.unpack(incoming_message.data) - for handler in self.field_info_handlers: - handler(field_info) - case SocketDataType.MATCH_CONFIGURATION: - if len(self.match_config_handlers) > 0: - match_settings = flat.MatchConfiguration.unpack( - incoming_message.data - ) - for handler in self.match_config_handlers: - handler(match_settings) - case SocketDataType.MATCH_COMMUNICATION: - if len(self.match_comm_handlers) > 0: - match_comm = flat.MatchComm.unpack(incoming_message.data) - for handler in self.match_comm_handlers: - handler(match_comm) - case SocketDataType.BALL_PREDICTION: - if len(self.ball_prediction_handlers) > 0: - self._ball_pred.unpack_with(incoming_message.data) - for handler in self.ball_prediction_handlers: - handler(self._ball_pred) - case SocketDataType.CONTROLLABLE_TEAM_INFO: - if len(self.controllable_team_info_handlers) > 0: - player_mappings = flat.ControllableTeamInfo.unpack( - incoming_message.data - ) - for handler in self.controllable_team_info_handlers: - handler(player_mappings) + case flat.GamePacket() as packet: + for handler in self.packet_handlers: + handler(packet) + case flat.FieldInfo() as field_info: + for handler in self.field_info_handlers: + handler(field_info) + case flat.MatchConfiguration() as match_config: + for handler in self.match_config_handlers: + handler(match_config) + case flat.MatchComm() as match_comm: + for handler in self.match_comm_handlers: + handler(match_comm) + case flat.BallPrediction() as ball_prediction: + for handler in self.ball_prediction_handlers: + handler(ball_prediction) + case flat.ControllableTeamInfo() as controllable_team_info: + for handler in self.controllable_team_info_handlers: + handler(controllable_team_info) + case flat.RenderingStatus() as rendering_status: + for handler in self.rendering_status_handlers: + handler(rendering_status) case _: - pass + self.logger.warning( + "Received unknown message type: %s", + type(flatbuffer.item).__name__, + ) return MsgHandlingResult.MORE_MSGS_QUEUED @@ -364,7 +313,7 @@ def disconnect(self): self.logger.warning("Asked to disconnect but was already disconnected.") return - self.send_bytes(bytes([1]), SocketDataType.NONE) + self.send_msg(flat.DisconnectSignal()) timeout = 5.0 while self._running and timeout > 0: time.sleep(0.1) diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 9320967..99e037b 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -13,6 +13,8 @@ from rlbot.utils import fill_desired_game_state from rlbot.utils.logging import DEFAULT_LOGGER, get_logger +WARNED_SPAWN_ID_DEPRECATED = False + class Bot: """ @@ -28,7 +30,19 @@ class Bot: team: int = -1 index: int = -1 name: str = "" - spawn_id: int = 0 + player_id: int = 0 + can_render: bool = False + + @property + def spawn_id(self) -> int: + global WARNED_SPAWN_ID_DEPRECATED + if not WARNED_SPAWN_ID_DEPRECATED: + WARNED_SPAWN_ID_DEPRECATED = True + self.logger.warning( + "'spawn_id' getter accessed, which is deprecated in favor of 'player_id'." + ) + + return self.player_id match_config = flat.MatchConfiguration() """ @@ -77,6 +91,9 @@ def __init__(self, default_agent_id: Optional[str] = None): self._handle_controllable_team_info ) self._game_interface.packet_handlers.append(self._handle_packet) + self._game_interface.rendering_status_handlers.append( + self.rendering_status_update + ) self.renderer = Renderer(self._game_interface) @@ -90,13 +107,6 @@ def _try_initialize(self): # Not ready to initialize return - # Search match settings for our name - for player in self.match_config.player_configurations: - if player.spawn_id == self.spawn_id: - self.name = player.name - self.logger = get_logger(self.name) - break - try: self.initialize() except Exception as e: @@ -107,11 +117,29 @@ def _try_initialize(self): exit() self._initialized_bot = True - self._game_interface.send_init_complete() + self._game_interface.send_msg(flat.InitComplete()) def _handle_match_config(self, match_config: flat.MatchConfiguration): self.match_config = match_config self._has_match_settings = True + self.can_render = ( + match_config.enable_rendering == flat.DebugRendering.OnByDefault + ) + + # Search match settings for our name + for player in self.match_config.player_configurations: + match player.variety.item: + case flat.CustomBot(name): + if player.player_id == self.player_id: + self.name = name + self.logger = get_logger(self.name) + break + else: # else block runs if break was not hit + self.logger.warning( + "Bot with agent id '%s' did not find itself in the match settings", + self._game_interface.agent_id, + ) + self._try_initialize() def _handle_field_info(self, field_info: flat.FieldInfo): @@ -124,7 +152,7 @@ def _handle_controllable_team_info( ): self.team = player_mappings.team controllable = player_mappings.controllables[0] - self.spawn_id = controllable.spawn_id + self.player_id = controllable.identifier self.index = controllable.index self._has_player_mapping = True @@ -154,7 +182,7 @@ def _packet_processor(self, packet: flat.GamePacket): return player_input = flat.PlayerInput(self.index, controller) - self._game_interface.send_player_input(player_input) + self._game_interface.send_msg(player_input) def _run(self): running = True @@ -213,6 +241,34 @@ def _handle_match_communication(self, match_comm: flat.MatchComm): match_comm.team_only, ) + def rendering_status_update(self, update: flat.RenderingStatus): + """ + Called when the server sends a rendering status update for ANY bot or script. + + By default, this will update `self.can_render` if appropriate. + """ + if update.is_bot and update.index == self.index: + self.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: Optional[int] = None, + is_bot: bool = True, + ): + """ + Requests the server to update the status of the ability for this bot to render. + Will be ignored if rendering has been set to AlwaysOff in the match settings. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.can_render`. + + - `status`: `True` to enable rendering, `False` to disable. + - `index`: The index of the bot to update. If `None`, uses the bot's own index. + - `is_bot`: `True` if `index` is a bot index, `False` if it is a script index. + """ + self._game_interface.send_msg( + flat.RenderingStatus(self.index if index is None else index, is_bot, status) + ) + def handle_match_comm( self, index: int, @@ -237,7 +293,7 @@ def send_match_comm( - `display`: The message to be displayed in the game in "quick chat", or `None` to display nothing. - `team_only`: If True, only your team will receive the message. """ - self._game_interface.send_match_comm( + self._game_interface.send_msg( flat.MatchComm( self.index, self.team, @@ -261,7 +317,7 @@ def set_game_state( """ game_state = fill_desired_game_state(balls, cars, match_info, commands) - self._game_interface.send_game_state(game_state) + self._game_interface.send_msg(game_state) def set_loadout(self, loadout: flat.PlayerLoadout, index: Optional[int] = None): """ @@ -270,9 +326,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: Optional[int] = None): Does nothing if called outside `initialize` unless state setting is enabled in which case it respawns the car with the new loadout. """ - self._game_interface.send_set_loadout( - flat.SetLoadout(index or self.index, loadout) - ) + self._game_interface.send_msg(flat.SetLoadout(index or self.index, loadout)) def initialize(self): """ diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index c9ed52a..53af8ea 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -14,6 +14,8 @@ from rlbot.utils import fill_desired_game_state from rlbot.utils.logging import DEFAULT_LOGGER, get_logger +WARNED_SPAWN_ID_DEPRECATED = False + class Hivemind: """ @@ -30,7 +32,19 @@ class Hivemind: team: int = -1 indices: list[int] = [] names: list[str] = [] - spawn_ids: list[int] = [] + player_ids: list[int] = [] + can_render: bool = False + + @property + def spawn_ids(self) -> list[int]: + global WARNED_SPAWN_ID_DEPRECATED + if not WARNED_SPAWN_ID_DEPRECATED: + WARNED_SPAWN_ID_DEPRECATED = True + self._logger.warning( + "'spawn_id' getter accessed, which is deprecated in favor of 'player_id'." + ) + + return self.player_ids match_config = flat.MatchConfiguration() """ @@ -79,6 +93,9 @@ def __init__(self, default_agent_id: Optional[str] = None): self._handle_controllable_team_info ) self._game_interface.packet_handlers.append(self._handle_packet) + self._game_interface.rendering_status_handlers.append( + self.rendering_status_update + ) self.renderer = Renderer(self._game_interface) @@ -91,14 +108,6 @@ def _try_initialize(self): ): return - # Search match settings for our spawn ids - for spawn_id in self.spawn_ids: - for player in self.match_config.player_configurations: - if player.spawn_id == spawn_id: - self.names.append(player.name) - self.loggers.append(get_logger(player.name)) - break - try: self.initialize() except Exception as e: @@ -111,11 +120,22 @@ def _try_initialize(self): exit() self._initialized_bot = True - self._game_interface.send_init_complete() + self._game_interface.send_msg(flat.InitComplete()) def _handle_match_config(self, match_config: flat.MatchConfiguration): self.match_config = match_config self._has_match_settings = True + + # Search match settings for our spawn ids + for player_id in self.player_ids: + for player in self.match_config.player_configurations: + match player.variety.item: + case flat.CustomBot(name): + if player.player_id == player_id: + self.names.append(name) + self.loggers.append(get_logger(name)) + break + self._try_initialize() def _handle_field_info(self, field_info: flat.FieldInfo): @@ -128,7 +148,7 @@ def _handle_controllable_team_info( ): self.team = player_mappings.team for controllable in player_mappings.controllables: - self.spawn_ids.append(controllable.spawn_id) + self.player_ids.append(controllable.identifier) self.indices.append(controllable.index) self._has_player_mapping = True @@ -166,7 +186,7 @@ def _packet_processor(self, packet: flat.GamePacket): ", ".join(map(str, self.indices)), ) player_input = flat.PlayerInput(index, controller) - self._game_interface.send_player_input(player_input) + self._game_interface.send_msg(player_input) def _run(self): running = True @@ -214,6 +234,36 @@ def run( self.retire() del self._game_interface + def rendering_status_update(self, update: flat.RenderingStatus): + """ + Called when the server sends a rendering status update for ANY bot or script. + + By default, this will update `self.can_render` if appropriate. + """ + if update.is_bot and update.index in self.indices: + self.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: Optional[int] = None, + is_bot: bool = True, + ): + """ + Requests the server to update the status of the ability for this bot to render. + Will be ignored if rendering has been set to AlwaysOff in the match settings. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.can_render`. + + - `status`: `True` to enable rendering, `False` to disable. + - `index`: The index of the bot to update. If `None`, uses the bot's own index. + - `is_bot`: `True` if `index` is a bot index, `False` if it is a script index. + """ + self._game_interface.send_msg( + flat.RenderingStatus( + self.indices[0] if index is None else index, is_bot, status + ) + ) + def _handle_match_communication(self, match_comm: flat.MatchComm): self.handle_match_comm( match_comm.index, @@ -251,7 +301,7 @@ def send_match_comm( - `display`: The message to be displayed in the game in "quick chat", or `None` to display nothing. - `team_only`: If True, only your team will receive the message. """ - self._game_interface.send_match_comm( + self._game_interface.send_msg( flat.MatchComm( index, self.team, @@ -275,7 +325,7 @@ def set_game_state( """ game_state = fill_desired_game_state(balls, cars, match_info, commands) - self._game_interface.send_game_state(game_state) + self._game_interface.send_msg(game_state) def set_loadout(self, loadout: flat.PlayerLoadout, index: int): """ @@ -284,7 +334,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: int): Does nothing if called outside `initialize` unless state setting is enabled in which case it respawns the car with the new loadout. """ - self._game_interface.send_set_loadout(flat.SetLoadout(index, loadout)) + self._game_interface.send_msg(flat.SetLoadout(index, loadout)) def initialize(self): """ diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index af2cea5..c90decb 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -121,7 +121,7 @@ def start_match( self.rlbot_interface.start_match(config) if not self.initialized: - self.rlbot_interface.send_init_complete() + self.rlbot_interface.send_msg(flat.InitComplete()) self.initialized = True if wait_for_start: @@ -152,7 +152,7 @@ def set_game_state( """ game_state = fill_desired_game_state(balls, cars, match_info, commands) - self.rlbot_interface.send_game_state(game_state) + self.rlbot_interface.send_msg(game_state) def shut_down(self, use_force_if_necessary: bool = True): """ diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 082e25e..73449fa 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -55,12 +55,8 @@ class Renderer: _screen_height_factor = 1.0 def __init__(self, game_interface: SocketRelay): - self._render_group: Callable[[flat.RenderGroup], None] = ( - game_interface.send_render_group - ) - - self._remove_render_group: Callable[[int], None] = ( - game_interface.remove_render_group + self._send_msg: Callable[[flat.RenderGroup | flat.RemoveRenderGroup], None] = ( + game_interface.send_msg ) def set_resolution(self, screen_width: float, screen_height: float): @@ -85,12 +81,18 @@ def create_color_hsv(hue: float, saturation: float, value: float) -> flat.Color: t = value * (1 - (1 - f) * saturation) match i % 6: - case 0: r, g, b = value, t, p - case 1: r, g, b = q, value, p - case 2: r, g, b = p, value, t - case 3: r, g, b = p, q, value - case 4: r, g, b = t, p, value - case 5: r, g, b = value, p, q + case 0: + r, g, b = value, t, p + case 1: + r, g, b = q, value, p + case 2: + r, g, b = p, value, t + case 3: + r, g, b = p, q, value + case 4: + r, g, b = t, p, value + case 5: + r, g, b = value, p, q return flat.Color(math.floor(r * 255), math.floor(g * 255), math.floor(b * 255)) @@ -137,7 +139,7 @@ def end_rendering(self): ) return - self._render_group(flat.RenderGroup(self._current_renders, self._group_id)) + self._send_msg(flat.RenderGroup(self._current_renders, self._group_id)) self._current_renders.clear() self._group_id = None @@ -147,7 +149,7 @@ def clear_render_group(self, group_id: str = DEFAULT_GROUP_ID): Note: It is not possible to clear render groups of other bots. """ group_id_hash = Renderer._get_group_id(group_id) - self._remove_render_group(group_id_hash) + self._send_msg(flat.RemoveRenderGroup(group_id_hash)) self._used_group_ids.discard(group_id_hash) def clear_all_render_groups(self): @@ -156,7 +158,7 @@ def clear_all_render_groups(self): Note: This does not clear render groups created by other bots. """ for group_id in self._used_group_ids: - self._remove_render_group(group_id) + self._send_msg(flat.RemoveRenderGroup(group_id)) self._used_group_ids.clear() def is_rendering(self): diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 613b242..f9ef194 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -26,6 +26,7 @@ class Script: index: int = 0 name: str = "UnknownScript" + can_render: bool = False match_config = flat.MatchConfiguration() field_info = flat.FieldInfo() @@ -59,6 +60,9 @@ def __init__(self, default_agent_id: Optional[str] = None): self._handle_ball_prediction ) self._game_interface.packet_handlers.append(self._handle_packet) + self._game_interface.rendering_status_handlers.append( + self.rendering_status_update + ) self.renderer = Renderer(self._game_interface) @@ -84,16 +88,19 @@ def _try_initialize(self): exit() self._initialized_script = True - self._game_interface.send_init_complete() + self._game_interface.send_msg(flat.InitComplete()) def _handle_match_config(self, match_config: flat.MatchConfiguration): self.match_config = match_config + self._has_match_settings = True + self.can_render = ( + match_config.enable_rendering == flat.DebugRendering.OnByDefault + ) for i, script in enumerate(match_config.script_configurations): if script.agent_id == self._game_interface.agent_id: self.index = i self.name = script.name - self._has_match_settings = True break else: # else block runs if break was not hit self.logger.warning( @@ -180,6 +187,34 @@ def _handle_match_communication(self, match_comm: flat.MatchComm): match_comm.team_only, ) + def rendering_status_update(self, update: flat.RenderingStatus): + """ + Called when the server sends a rendering status update for ANY bot or script. + + By default, this will update `self.can_render` if appropriate. + """ + if not update.is_bot and update.index == self.index: + self.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: Optional[int] = None, + is_bot: bool = False, + ): + """ + Requests the server to update the status of the ability for this bot to render. + Will be ignored if rendering has been set to AlwaysOff in the match settings. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.can_render`. + + - `status`: `True` to enable rendering, `False` to disable. + - `index`: The index of the bot to update. If `None`, uses the script's own index. + - `is_bot`: `True` if `index` is a bot index, `False` if it is a script index. + """ + self._game_interface.send_msg( + flat.RenderingStatus(self.index if index is None else index, is_bot, status) + ) + def handle_match_comm( self, index: int, @@ -204,7 +239,7 @@ def send_match_comm( - `display`: The message to be displayed in the game in "quick chat", or `None` to display nothing. - `team_only`: If True, only your team will receive the message. For scripts, this means other scripts. """ - self._game_interface.send_match_comm( + self._game_interface.send_msg( flat.MatchComm( self.index, 2, @@ -228,7 +263,7 @@ def set_game_state( """ game_state = fill_desired_game_state(balls, cars, match_info, commands) - self._game_interface.send_game_state(game_state) + self._game_interface.send_msg(game_state) def set_loadout(self, loadout: flat.PlayerLoadout, index: int): """ @@ -236,7 +271,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: int): Will be ignored if called when state setting is disabled. """ - self._game_interface.send_set_loadout(flat.SetLoadout(index, loadout)) + self._game_interface.send_msg(flat.SetLoadout(index, loadout)) def initialize(self): """ diff --git a/rlbot/version.py b/rlbot/version.py index 2bcdf27..07f103d 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.43" +__version__ = "2.0.0-beta.46" diff --git a/tests/default.toml b/tests/default.toml index 517cb5d..3ca7586 100644 --- a/tests/default.toml +++ b/tests/default.toml @@ -11,8 +11,8 @@ wait_for_agents = true [match] # What game mode the game should load. -# Accepted values are "Soccer", "Hoops", "Dropshot", "Hockey", "Rumble", "Heatseeker", "Gridiron", "Knockout" -game_mode = "Soccer" +# Accepted values are "Soccar", "Hoops", "Dropshot", "Snowday", "Rumble", "Heatseeker", "Gridiron", "Knockout" +game_mode = "Soccar" # Which map the game should load into. Ensure the map doesn't end in '.upk'. game_map_upk = "Stadium_P" # Automatically skip replays after a goal. Also stops match replays from being saved. diff --git a/tests/gamemodes/beach_ball.toml b/tests/gamemodes/beach_ball.toml index 52cd083..4ab56d3 100644 --- a/tests/gamemodes/beach_ball.toml +++ b/tests/gamemodes/beach_ball.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" # Map set currently unknown game_map_upk = "Stadium_P" diff --git a/tests/gamemodes/boomer_ball.toml b/tests/gamemodes/boomer_ball.toml index ee72a37..9b989e9 100644 --- a/tests/gamemodes/boomer_ball.toml +++ b/tests/gamemodes/boomer_ball.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" [mutators] boost_amount = "UnlimitedBoost" diff --git a/tests/gamemodes/gforce_frenzy.toml b/tests/gamemodes/gforce_frenzy.toml index 1949151..641eaff 100644 --- a/tests/gamemodes/gforce_frenzy.toml +++ b/tests/gamemodes/gforce_frenzy.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" [mutators] boost_amount = "UnlimitedBoost" diff --git a/tests/gamemodes/ghost_hunt.toml b/tests/gamemodes/ghost_hunt.toml index 97655aa..a5b1026 100644 --- a/tests/gamemodes/ghost_hunt.toml +++ b/tests/gamemodes/ghost_hunt.toml @@ -2,7 +2,7 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Haunted_TrainStation_P" [mutators] diff --git a/tests/gamemodes/gotham_city_rumble.toml b/tests/gamemodes/gotham_city_rumble.toml index f24cc56..75aac82 100644 --- a/tests/gamemodes/gotham_city_rumble.toml +++ b/tests/gamemodes/gotham_city_rumble.toml @@ -2,7 +2,7 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Park_Bman_P" [mutators] diff --git a/tests/gamemodes/nike_fc_showdown.toml b/tests/gamemodes/nike_fc_showdown.toml index 8639866..72d1590 100644 --- a/tests/gamemodes/nike_fc_showdown.toml +++ b/tests/gamemodes/nike_fc_showdown.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "swoosh_p" [mutators] diff --git a/tests/gamemodes/speed_demon.toml b/tests/gamemodes/speed_demon.toml index c681f21..528dd88 100644 --- a/tests/gamemodes/speed_demon.toml +++ b/tests/gamemodes/speed_demon.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" [mutators] boost_amount = "UnlimitedBoost" diff --git a/tests/gamemodes/spike_rush.toml b/tests/gamemodes/spike_rush.toml index 87e2e8f..3bef014 100644 --- a/tests/gamemodes/spike_rush.toml +++ b/tests/gamemodes/spike_rush.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "ThrowbackStadium_P" [mutators] diff --git a/tests/gamemodes/spooky_cube.toml b/tests/gamemodes/spooky_cube.toml index 1b2cc5d..3735afe 100644 --- a/tests/gamemodes/spooky_cube.toml +++ b/tests/gamemodes/spooky_cube.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Farm_HW_P" [mutators] diff --git a/tests/gamemodes/super_cube.toml b/tests/gamemodes/super_cube.toml index cc2e108..714aa48 100644 --- a/tests/gamemodes/super_cube.toml +++ b/tests/gamemodes/super_cube.toml @@ -1,5 +1,5 @@ [match] -game_mode = "Soccer" +game_mode = "Soccar" [mutators] ball_max_speed = "SuperFast" diff --git a/tests/gamemodes/winter_breakaway.toml b/tests/gamemodes/winter_breakaway.toml index 48bdf15..c87cf00 100644 --- a/tests/gamemodes/winter_breakaway.toml +++ b/tests/gamemodes/winter_breakaway.toml @@ -1,3 +1,3 @@ [match] -game_mode = "Hockey" +game_mode = "Snowday" game_map_upk = "ThrowbackHockey_p" diff --git a/tests/hivemind.toml b/tests/hivemind.toml index 5ab9ceb..0a17b53 100644 --- a/tests/hivemind.toml +++ b/tests/hivemind.toml @@ -3,7 +3,7 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" [mutators] diff --git a/tests/human_vs_atba.toml b/tests/human_vs_atba.toml index 2b4d03b..c3d1433 100644 --- a/tests/human_vs_atba.toml +++ b/tests/human_vs_atba.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" enable_rendering = true diff --git a/tests/human_vs_necto.toml b/tests/human_vs_necto.toml index c1617d2..ad5acc0 100644 --- a/tests/human_vs_necto.toml +++ b/tests/human_vs_necto.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" [mutators] diff --git a/tests/minimal.toml b/tests/minimal.toml index 9c1da51..b2a6b95 100644 --- a/tests/minimal.toml +++ b/tests/minimal.toml @@ -3,5 +3,5 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" diff --git a/tests/nexto/bot.py b/tests/nexto/bot.py index 2bbcc16..c4af5a9 100644 --- a/tests/nexto/bot.py +++ b/tests/nexto/bot.py @@ -5,6 +5,7 @@ import torch from agent import Agent from nexto_obs import BOOST_LOCATIONS, NextoObsBuilder +from rlbot_flatbuffers import GameMode from rlgym_compat.v1_game_state import V1GameState as GameState from rlbot.flat import ControllerState, GamePacket, MatchPhase, Vector3 @@ -37,10 +38,10 @@ ) GAME_MODES = [ - "soccer", + "soccar", "hoops", "dropshot", - "hockey", + "snowday", "rumble", "heatseeker", ] @@ -63,7 +64,7 @@ class Nexto(Bot): ticks = tick_skip # So we take an action the first tick prev_tick = 0 kickoff_index = -1 - gamemode = "" + gamemode = GameMode.Soccar # toxic handling orange_goals = 0 @@ -95,10 +96,7 @@ def initialize(self): "Also check out the RLGym Twitch stream to watch live bot training and occasional showmatches!" ) - game_mode_idx = int(self.match_config.game_mode) - self.gamemode = ( - GAME_MODES[game_mode_idx] if game_mode_idx < len(GAME_MODES) else 0 - ) + self.gamemode = self.match_config.game_mode def render_attention_weights(self, weights, positions, n=3): if weights is None: @@ -171,8 +169,7 @@ def get_output(self, packet: GamePacket) -> ControllerState: self.game_state.players = [player] + teammates + opponents - # todo add heatseeker later - if self.gamemode == "heatseeker": + if self.gamemode == GameMode.Heatseeker: self._modify_ball_info_for_heatseeker(packet, self.game_state) obs = self.obs_builder.build_obs(player, self.game_state, self.action) @@ -263,7 +260,7 @@ def update_controls(self, action): self.controls.jump = action[5] > 0 self.controls.boost = action[6] > 0 self.controls.handbrake = action[7] > 0 - if self.gamemode == "rumble": + if self.gamemode == GameMode.Rumble: self.controls.use_item = np.random.random() > ( self.tick_skip / 1200 ) # On average once every 10 seconds diff --git a/tests/psy.toml b/tests/psy.toml index 0a1e856..5da1149 100644 --- a/tests/psy.toml +++ b/tests/psy.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" [[cars]] diff --git a/tests/render_test.toml b/tests/render_test.toml index 79e0fd1..c3421a5 100644 --- a/tests/render_test.toml +++ b/tests/render_test.toml @@ -3,9 +3,9 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" -enable_rendering = true +enable_rendering = "OffByDefault" [[cars]] type = "Human" diff --git a/tests/render_test/render.py b/tests/render_test/render.py index edbf7f4..68fea71 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -8,6 +8,9 @@ class RenderFun(Script): last_state = flat.MatchPhase.Inactive player_count = 0 + def initialize(self): + self.update_rendering_status(True) + def handle_packet(self, packet: flat.GamePacket): if ( packet.match_info.match_phase != flat.MatchPhase.Replay @@ -30,10 +33,12 @@ def handle_packet(self, packet: flat.GamePacket): self.do_render(radius) - self.renderer.begin_rendering('tick') - hsv = self.renderer.create_color_hsv(packet.match_info.seconds_elapsed * 0.1, 1.0, 1.0) + self.renderer.begin_rendering("tick") + hsv = self.renderer.create_color_hsv( + packet.match_info.seconds_elapsed * 0.1, 1.0, 1.0 + ) self.renderer.set_resolution(1920, 1080) - self.renderer.draw_string_2d('HSV 300px 50px', 300, 50, 1.0, hsv) + self.renderer.draw_string_2d("HSV 300px 50px", 300, 50, 1.0, hsv) self.renderer.set_resolution(1, 1) self.renderer.end_rendering() @@ -74,9 +79,7 @@ def do_render(self, radius: float): CarAnchor(0, Vector3(200, 0, 0)), 0.02, 0.02, self.renderer.blue ) - self.renderer.draw_rect_2d( - 0.75, 0.75, 0.1, 0.1, Color(150, 30, 100), centered=False - ) + self.renderer.draw_rect_2d(0.75, 0.75, 0.1, 0.1, Color(150, 30, 100)) self.renderer.draw_rect_2d(0.75, 0.75, 0.1, 0.1, self.renderer.black) for hkey, h in { "left": flat.TextHAlign.Left, diff --git a/tests/rlbot.toml b/tests/rlbot.toml index 437453f..8436ce4 100644 --- a/tests/rlbot.toml +++ b/tests/rlbot.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" skip_replays = false start_without_countdown = false diff --git a/tests/run_forever.py b/tests/run_forever.py index f5b5661..749772a 100644 --- a/tests/run_forever.py +++ b/tests/run_forever.py @@ -17,13 +17,13 @@ current_map = -1 - blue_bot = load_player_config(BOT_PATH, flat.CustomBot(), 0) - orange_bot = load_player_config(BOT_PATH, flat.CustomBot(), 1) + blue_bot = load_player_config(BOT_PATH, 0) + orange_bot = load_player_config(BOT_PATH, 1) match_settings = flat.MatchConfiguration( launcher=flat.Launcher.Steam, auto_start_agents=True, - game_mode=flat.GameMode.Soccer, + game_mode=flat.GameMode.Soccar, enable_state_setting=True, existing_match_behavior=flat.ExistingMatchBehavior.Restart, skip_replays=True, diff --git a/tests/run_only.py b/tests/run_only.py index f65fbc7..9e450d3 100644 --- a/tests/run_only.py +++ b/tests/run_only.py @@ -5,7 +5,7 @@ DIR = Path(__file__).parent -MATCH_CONFIG_PATH = DIR / "human_vs_necto.toml" +MATCH_CONFIG_PATH = DIR / "render_test.toml" RLBOT_SERVER_FOLDER = DIR / "../" if __name__ == "__main__": diff --git a/tests/series.toml b/tests/series.toml index b0915ba..bd45fdd 100644 --- a/tests/series.toml +++ b/tests/series.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" enable_state_setting = true existing_match_behavior = "ContinueAndSpawn"