From 14f96e9ec0ce4e6b9c182492fc2168ae57397b39 Mon Sep 17 00:00:00 2001 From: Virx Date: Tue, 6 May 2025 23:28:03 -0400 Subject: [PATCH 01/30] Allow `path` in `load_player_config` to be `None` --- rlbot/config.py | 16 +++++++++++++++- rlbot/version.py | 2 +- tests/nexto/toxic.bot.toml | 1 + tests/psy.toml | 1 - 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/rlbot/config.py b/rlbot/config.py index 20f93a0..20a1946 100644 --- a/rlbot/config.py +++ b/rlbot/config.py @@ -217,7 +217,7 @@ def load_player_loadout(path: Path | str, team: int) -> flat.PlayerLoadout: def load_player_config( - path: Path | str, + path: Path | str | None, type: flat.CustomBot | flat.Psyonix, team: int, name_override: str | None = None, @@ -227,6 +227,20 @@ 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) diff --git a/rlbot/version.py b/rlbot/version.py index f8b01fd..4511e96 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.41" +__version__ = "2.0.0-beta.42" diff --git a/tests/nexto/toxic.bot.toml b/tests/nexto/toxic.bot.toml index 88ddada..e0c5f30 100644 --- a/tests/nexto/toxic.bot.toml +++ b/tests/nexto/toxic.bot.toml @@ -1,3 +1,4 @@ +#:schema https://rlbot.org/schemas/agent.json [settings] name = "Nexto (Toxic!)" loadout_file = "loadout.toml" diff --git a/tests/psy.toml b/tests/psy.toml index c89a493..0a1e856 100644 --- a/tests/psy.toml +++ b/tests/psy.toml @@ -9,7 +9,6 @@ game_map_upk = "Stadium_P" [[cars]] team = 0 -config_file = "psy/bot.toml" type = "Psyonix" skill = "Beginner" From dc73c19eb26f0487570466fc307f3b5bfc97eeb3 Mon Sep 17 00:00:00 2001 From: Virx Date: Fri, 23 May 2025 17:42:02 -0400 Subject: [PATCH 02/30] Update `rlgym_compat` requirement Format --- rlbot/utils/gateway.py | 1 + tests/necto/requirements.txt | 2 +- tests/nexto/requirements.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rlbot/utils/gateway.py b/rlbot/utils/gateway.py index 502cbce..d0ace75 100644 --- a/rlbot/utils/gateway.py +++ b/rlbot/utils/gateway.py @@ -14,6 +14,7 @@ if CURRENT_OS != "Windows": import shlex + def find_main_executable_path( main_executable_path: Path, main_executable_name: str ) -> tuple[Path, Optional[Path]]: diff --git a/tests/necto/requirements.txt b/tests/necto/requirements.txt index 09bf82c..6f0811c 100644 --- a/tests/necto/requirements.txt +++ b/tests/necto/requirements.txt @@ -1,5 +1,5 @@ rlbot==2.* numpy==1.* -rlgym_compat @ git+https://github.com/JPK314/rlgym-compat@rlgymv2-rlbot-v5 +rlgym_compat @ git+https://github.com/JPK314/rlgym-compat --extra-index-url https://download.pytorch.org/whl/cpu torch==2.4.1+cpu diff --git a/tests/nexto/requirements.txt b/tests/nexto/requirements.txt index 09bf82c..6f0811c 100644 --- a/tests/nexto/requirements.txt +++ b/tests/nexto/requirements.txt @@ -1,5 +1,5 @@ rlbot==2.* numpy==1.* -rlgym_compat @ git+https://github.com/JPK314/rlgym-compat@rlgymv2-rlbot-v5 +rlgym_compat @ git+https://github.com/JPK314/rlgym-compat --extra-index-url https://download.pytorch.org/whl/cpu torch==2.4.1+cpu From f1e679b08e465481cf286a51456b848f36ab10f6 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sun, 25 May 2025 10:41:43 +0200 Subject: [PATCH 03/30] Add set_resolution and hsv colors --- rlbot/managers/rendering.py | 50 +++++++++++++++++++++++++++++++------ tests/render_test/render.py | 7 ++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 7dcee97..082e25e 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -1,3 +1,4 @@ +import math from collections.abc import Callable, Sequence from typing import Optional @@ -50,6 +51,9 @@ class Renderer: _group_id: Optional[int] = None _current_renders: list[flat.RenderMessage] = [] + _screen_width_factor = 1.0 + _screen_height_factor = 1.0 + def __init__(self, game_interface: SocketRelay): self._render_group: Callable[[flat.RenderGroup], None] = ( game_interface.send_render_group @@ -59,10 +63,37 @@ def __init__(self, game_interface: SocketRelay): game_interface.remove_render_group ) + def set_resolution(self, screen_width: float, screen_height: float): + """ + By default, the renderer uses screen-space coordinates for 2d, e.g. 0.1 is 10% of screen width. + Use this function to declare the screen's size in pixels, if you prefer working in pixel coordinates. + After setting this, `draw_string_2d('Hi', 100, 200, ...)` will draw 'Hi' at pixel coordinates (100, 200). + """ + self._screen_width_factor = 1.0 / screen_width + self._screen_height_factor = 1.0 / screen_height + @staticmethod def create_color(red: int, green: int, blue: int, alpha: int = 255) -> flat.Color: return flat.Color(red, green, blue, alpha) + @staticmethod + def create_color_hsv(hue: float, saturation: float, value: float) -> flat.Color: + i = math.floor(hue * 6) + f = hue * 6 - i + p = value * (1 - saturation) + q = value * (1 - f * saturation) + 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 + + return flat.Color(math.floor(r * 255), math.floor(g * 255), math.floor(b * 255)) + @staticmethod def team_color(team: int, alt_color: bool = False) -> flat.Color: """ @@ -215,13 +246,14 @@ def draw_string_2d( """ Draws text in 2d space. X and y uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height. + Use `set_resolution` to change to pixel coordinates. Characters of the font are 20 pixels tall and 10 pixels wide when `scale == 1.0`. """ self.draw( flat.String2D( text, - x, - y, + x * self._screen_width_factor, + y * self._screen_height_factor, scale, foreground, background, @@ -243,14 +275,15 @@ def draw_rect_2d( """ Draws a rectangle anchored in 2d space. X, y, width, and height uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height. + Use `set_resolution` to change to pixel coordinates. """ self.draw( flat.Rect2D( - x, - y, - width, - height, + x * self._screen_width_factor, + y * self._screen_height_factor, + width * self._screen_width_factor, + height * self._screen_height_factor, color, h_align, v_align, @@ -269,13 +302,14 @@ def draw_rect_3d( """ Draws a rectangle anchored in 3d space. Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height. + Use `set_resolution` to change to pixel coordinates. The size does not change based on distance to the camera. """ self.draw( flat.Rect3D( _get_anchor(anchor), - width, - height, + width * self._screen_width_factor, + height * self._screen_height_factor, color, h_align, v_align, diff --git a/tests/render_test/render.py b/tests/render_test/render.py index ddc8916..edbf7f4 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -30,6 +30,13 @@ 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.set_resolution(1920, 1080) + self.renderer.draw_string_2d('HSV 300px 50px', 300, 50, 1.0, hsv) + self.renderer.set_resolution(1, 1) + self.renderer.end_rendering() + def do_render(self, radius: float): self.renderer.begin_rendering() From 0dec0fdc44fb3e65dc9922b4453d3508fe90963c Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sun, 25 May 2025 20:04:43 +0200 Subject: [PATCH 04/30] Bump version --- rlbot/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbot/version.py b/rlbot/version.py index 4511e96..2bcdf27 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.42" +__version__ = "2.0.0-beta.43" From 77bda1d1737773ff66ea57d00fc5fa777147e5f3 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 24 May 2025 02:39:22 -0400 Subject: [PATCH 05/30] Initial update for upcoming core v0.7 --- pyproject.toml | 2 +- rlbot/interface.py | 191 +++++++++++++----------------------- rlbot/managers/bot.py | 12 +-- rlbot/managers/hivemind.py | 10 +- rlbot/managers/match.py | 4 +- rlbot/managers/rendering.py | 4 +- rlbot/managers/script.py | 8 +- 7 files changed, 87 insertions(+), 144 deletions(-) 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/interface.py b/rlbot/interface.py index feea530..b8a351f 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,7 @@ class SocketRelay: controllable_team_info_handlers: list[ Callable[[flat.ControllableTeamInfo], None] ] = [] - raw_handlers: list[Callable[[SocketMessage], None]] = [] + raw_handlers: list[Callable[[flat.CoreMessage], None]] = [] def __init__( self, @@ -116,50 +84,45 @@ 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 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 + ), + ): + self.send_bytes(flat.InterfacePacket(msg).pack()) def remove_render_group(self, group_id: int): - flatbuffer = flat.RemoveRenderGroup(group_id).pack() - self.send_bytes(flatbuffer, SocketDataType.REMOVE_RENDER_GROUP) + self.send_msg(flat.RemoveRenderGroup(group_id)) 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 +130,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 +203,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 +248,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 +266,43 @@ 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 _: - pass + self.logger.warning( + "Received unknown message type: %s", + type(flatbuffer.item).__name__, + ) return MsgHandlingResult.MORE_MSGS_QUEUED @@ -364,7 +311,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..9ef397d 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -107,7 +107,7 @@ 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 @@ -154,7 +154,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 @@ -237,7 +237,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 +261,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 +270,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..eacbbad 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -111,7 +111,7 @@ 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 @@ -166,7 +166,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 @@ -251,7 +251,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 +275,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 +284,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..5c1c391 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -55,9 +55,7 @@ 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._render_group: Callable[[flat.RenderGroup], None] = game_interface.send_msg self._remove_render_group: Callable[[int], None] = ( game_interface.remove_render_group diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 613b242..274c999 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -84,7 +84,7 @@ 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 @@ -204,7 +204,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 +228,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 +236,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): """ From e85c051d1e7c3ed1fe59a613afe923b88a55e080 Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 25 May 2025 16:45:40 -0400 Subject: [PATCH 06/30] `spawn_id` -> `player_id` with alias --- rlbot/managers/bot.py | 19 ++++++++++++++++--- rlbot/managers/hivemind.py | 21 +++++++++++++++++---- rlbot/managers/rendering.py | 18 ++++++++++++------ rlbot/version.py | 2 +- tests/render_test/render.py | 12 ++++++------ 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 9ef397d..32c0f00 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,18 @@ class Bot: team: int = -1 index: int = -1 name: str = "" - spawn_id: int = 0 + player_id: int = 0 + + @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() """ @@ -92,7 +105,7 @@ def _try_initialize(self): # Search match settings for our name for player in self.match_config.player_configurations: - if player.spawn_id == self.spawn_id: + if player.player_id == self.player_id: self.name = player.name self.logger = get_logger(self.name) break @@ -124,7 +137,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 diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index eacbbad..c76fdc9 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,18 @@ class Hivemind: team: int = -1 indices: list[int] = [] names: list[str] = [] - spawn_ids: list[int] = [] + player_ids: list[int] = [] + + @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() """ @@ -92,9 +105,9 @@ def _try_initialize(self): return # Search match settings for our spawn ids - for spawn_id in self.spawn_ids: + for player_id in self.player_ids: for player in self.match_config.player_configurations: - if player.spawn_id == spawn_id: + if player.player_id == player_id: self.names.append(player.name) self.loggers.append(get_logger(player.name)) break @@ -128,7 +141,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 diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 5c1c391..31e697d 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -83,12 +83,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)) diff --git a/rlbot/version.py b/rlbot/version.py index 2bcdf27..a7ed31d 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.43" +__version__ = "2.0.0-beta.44" diff --git a/tests/render_test/render.py b/tests/render_test/render.py index edbf7f4..df1c257 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -30,10 +30,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 +76,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, From d6fe01e5efa42f2021e44659a63d5c1094130883 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 31 May 2025 10:53:30 +0200 Subject: [PATCH 07/30] Add context managers for MatchManager and Rendering --- rlbot/managers/match.py | 7 +++++ rlbot/managers/rendering.py | 57 +++++++++++++++++++++++++++++-------- rlbot/version.py | 2 +- tests/render_test/render.py | 16 ++++++----- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index af2cea5..f56a110 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -33,6 +33,13 @@ def __init__( self.rlbot_interface: SocketRelay = SocketRelay("") self.rlbot_interface.packet_handlers.append(self._packet_reporter) + def __enter__(self) -> 'MatchManager': + self.ensure_server_started() + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.disconnect() + def ensure_server_started(self): """ Ensures that RLBotServer is running, starting it if it is not. diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 082e25e..f3d9685 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -1,5 +1,6 @@ import math from collections.abc import Callable, Sequence +from contextlib import contextmanager from typing import Optional from rlbot import flat @@ -51,6 +52,8 @@ class Renderer: _group_id: Optional[int] = None _current_renders: list[flat.RenderMessage] = [] + _default_color = white + _screen_width_factor = 1.0 _screen_height_factor = 1.0 @@ -72,6 +75,12 @@ def set_resolution(self, screen_width: float, screen_height: float): self._screen_width_factor = 1.0 / screen_width self._screen_height_factor = 1.0 / screen_height + def set_default_color(self, color: flat.Color): + """ + Set which color to use when no other color is provided. + """ + self._default_color = color + @staticmethod def create_color(red: int, green: int, blue: int, alpha: int = 255) -> flat.Color: return flat.Color(red, green, blue, alpha) @@ -111,6 +120,30 @@ def team_color(team: int, alt_color: bool = False) -> flat.Color: def _get_group_id(group_id: str) -> int: return hash(str(group_id).encode("utf-8")) % MAX_INT + @contextmanager + def context(self, group_id: str=DEFAULT_GROUP_ID, default_color=None): + """ + Starts rendering as a context usable in with-statements. + After the with-statement the rendering is automatically ended. + Note, the is not possible to have two nested renderings started. + + Example: + + ``` + with renderer.context(default_color=renderer.red): + renderer.draw_line_3d(car.pos, ball.pos) + renderer.draw_line_3d(car.pos, goal.pos) + renderer.draw_line_3d(ball.pos, goal.pos) + ``` + """ + try: + self.begin_rendering(group_id) + if default_color: + self.set_default_color(default_color) + yield + finally: + self.end_rendering() + def begin_rendering(self, group_id: str = DEFAULT_GROUP_ID): """ Begins a new render group. All render messages added after this call will be part of this group. @@ -189,29 +222,29 @@ def draw_line_3d( self, start: flat.RenderAnchor | flat.BallAnchor | flat.CarAnchor | flat.Vector3, end: flat.RenderAnchor | flat.BallAnchor | flat.CarAnchor | flat.Vector3, - color: flat.Color, + color: flat.Color | None = None, ): """ Draws a line between two anchors in 3d space. """ - self.draw(flat.Line3D(_get_anchor(start), _get_anchor(end), color)) + self.draw(flat.Line3D(_get_anchor(start), _get_anchor(end), color or self._default_color)) def draw_polyline_3d( self, points: Sequence[flat.Vector3], - color: flat.Color, + color: flat.Color | None = None, ): """ Draws a line going through each of the provided points. """ - self.draw(flat.PolyLine3D(points, color)) + self.draw(flat.PolyLine3D(points, color or self._default_color)) def draw_string_3d( self, text: str, anchor: flat.RenderAnchor | flat.BallAnchor | flat.CarAnchor | flat.Vector3, scale: float, - foreground: flat.Color, + foreground: flat.Color | None = None, background: flat.Color = flat.Color(a=0), h_align: flat.TextHAlign = flat.TextHAlign.Left, v_align: flat.TextVAlign = flat.TextVAlign.Top, @@ -225,7 +258,7 @@ def draw_string_3d( text, _get_anchor(anchor), scale, - foreground, + foreground or self._default_color, background, h_align, v_align, @@ -238,7 +271,7 @@ def draw_string_2d( x: float, y: float, scale: float, - foreground: flat.Color, + foreground: flat.Color | None = None, background: flat.Color = flat.Color(a=0), h_align: flat.TextHAlign = flat.TextHAlign.Left, v_align: flat.TextVAlign = flat.TextVAlign.Top, @@ -255,7 +288,7 @@ def draw_string_2d( x * self._screen_width_factor, y * self._screen_height_factor, scale, - foreground, + foreground or self._default_color, background, h_align, v_align, @@ -268,7 +301,7 @@ def draw_rect_2d( y: float, width: float, height: float, - color: flat.Color, + color: flat.Color | None = None, h_align: flat.TextHAlign = flat.TextHAlign.Left, v_align: flat.TextVAlign = flat.TextVAlign.Top, ): @@ -284,7 +317,7 @@ def draw_rect_2d( y * self._screen_height_factor, width * self._screen_width_factor, height * self._screen_height_factor, - color, + color or self._default_color, h_align, v_align, ) @@ -295,7 +328,7 @@ def draw_rect_3d( anchor: flat.RenderAnchor | flat.BallAnchor | flat.CarAnchor | flat.Vector3, width: float, height: float, - color: flat.Color, + color: flat.Color | None = None, h_align: flat.TextHAlign = flat.TextHAlign.Left, v_align: flat.TextVAlign = flat.TextVAlign.Top, ): @@ -310,7 +343,7 @@ def draw_rect_3d( _get_anchor(anchor), width * self._screen_width_factor, height * self._screen_height_factor, - color, + color or self._default_color, h_align, v_align, ) diff --git a/rlbot/version.py b/rlbot/version.py index 2bcdf27..a7ed31d 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.43" +__version__ = "2.0.0-beta.44" diff --git a/tests/render_test/render.py b/tests/render_test/render.py index edbf7f4..0079ded 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -1,3 +1,5 @@ +from email.policy import default + from rlbot import flat from rlbot.flat import BallAnchor, CarAnchor, Color, RenderAnchor, Vector3 from rlbot.managers import Script @@ -30,12 +32,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.set_resolution(1920, 1080) - self.renderer.draw_string_2d('HSV 300px 50px', 300, 50, 1.0, hsv) - self.renderer.set_resolution(1, 1) - self.renderer.end_rendering() + with self.renderer.context('tick', self.renderer.red): + self.renderer.set_resolution(1920, 1080) + hsv = self.renderer.create_color_hsv(packet.match_info.seconds_elapsed * 0.1, 1.0, 1.0) + self.renderer.draw_string_2d('HSV 300px 50px', 300, 50, 1.0, hsv) + self.renderer.draw_string_2d('Red 330px 70px', 330, 70, 1.0) # Use default color + self.renderer.set_resolution(1, 1) def do_render(self, radius: float): self.renderer.begin_rendering() @@ -75,7 +77,7 @@ def do_render(self, radius: float): ) self.renderer.draw_rect_2d( - 0.75, 0.75, 0.1, 0.1, Color(150, 30, 100), centered=False + 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 { From 46f2f72781ac95964b8a2e6de61ed75dcb8402a7 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 31 May 2025 10:57:05 +0200 Subject: [PATCH 08/30] Use context manager pattern in test --- tests/run_match.py | 12 ++++-------- tests/run_only.py | 14 ++++++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/tests/run_match.py b/tests/run_match.py index 3843133..8da8ebb 100644 --- a/tests/run_match.py +++ b/tests/run_match.py @@ -10,14 +10,10 @@ RLBOT_SERVER_FOLDER = DIR / "../../core/RLBotCS/bin/Release/" if __name__ == "__main__": - match_manager = MatchManager(RLBOT_SERVER_FOLDER) + with MatchManager(RLBOT_SERVER_FOLDER) as man: + man.start_match(MATCH_CONFIG_PATH) + assert man.packet is not None - match_manager.start_match(MATCH_CONFIG_PATH) - assert match_manager.packet is not None - - try: # wait for the match to end - while match_manager.packet.match_info.match_phase != flat.MatchPhase.Ended: + while man.packet.match_info.match_phase != flat.MatchPhase.Ended: sleep(1.0) - finally: - match_manager.shut_down() diff --git a/tests/run_only.py b/tests/run_only.py index f65fbc7..67334bf 100644 --- a/tests/run_only.py +++ b/tests/run_only.py @@ -14,13 +14,11 @@ match_config_path = Path(sys.argv[1]) assert match_config_path.exists(), f"Match config not found: {match_config_path}" - # start the match - match_manager = MatchManager(RLBOT_SERVER_FOLDER) - match_manager.start_match(match_config_path, False) + with MatchManager(RLBOT_SERVER_FOLDER) as man: + man.start_match(match_config_path, False) - # wait - input("\nPress enter to end the match: ") + # Wait for input + input("\nPress enter to end the match: ") - # end the match and disconnect - match_manager.stop_match() - match_manager.disconnect() + # End the match + man.stop_match() From df401023498eca5b28826e2ad59ef294560c024a Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 31 May 2025 22:03:00 +0200 Subject: [PATCH 09/30] Avoid calling ensure_match_started twice in context manager --- rlbot/managers/match.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index f56a110..21843cb 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -34,7 +34,6 @@ def __init__( self.rlbot_interface.packet_handlers.append(self._packet_reporter) def __enter__(self) -> 'MatchManager': - self.ensure_server_started() return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: From aa43e8ce2756b7f24c6d868ed851a90dd7058eab Mon Sep 17 00:00:00 2001 From: Virx Date: Fri, 6 Jun 2025 20:52:03 -0400 Subject: [PATCH 10/30] Update to newest spec --- rlbot/config.py | 116 ++++++++++++++++++++++++------------- rlbot/managers/bot.py | 10 ++-- rlbot/managers/hivemind.py | 10 ++-- 3 files changed, 87 insertions(+), 49 deletions(-) diff --git a/rlbot/config.py b/rlbot/config.py index 20a1946..4226df8 100644 --- a/rlbot/config.py +++ b/rlbot/config.py @@ -94,30 +94,28 @@ 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() + 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"): @@ -170,7 +168,9 @@ 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=__enum( + match_table, "enable_rendering", flat.DebugRendering.OffByDefault + ), enable_state_setting=__bool(match_table, "enable_state_setting"), freeplay=__bool(match_table, "freeplay"), ) @@ -217,8 +217,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 +226,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 +251,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/managers/bot.py b/rlbot/managers/bot.py index 32c0f00..765b707 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -105,10 +105,12 @@ def _try_initialize(self): # Search match settings for our name for player in self.match_config.player_configurations: - if player.player_id == self.player_id: - self.name = player.name - self.logger = get_logger(self.name) - break + 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 try: self.initialize() diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index c76fdc9..1cf1759 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -107,10 +107,12 @@ def _try_initialize(self): # Search match settings for our spawn ids for player_id in self.player_ids: for player in self.match_config.player_configurations: - if player.player_id == player_id: - self.names.append(player.name) - self.loggers.append(get_logger(player.name)) - break + 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 try: self.initialize() From 4adb857ed3ec31c6ecc67e988fd4b530ce8d736d Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sun, 8 Jun 2025 21:35:39 +0200 Subject: [PATCH 11/30] End rendering on exception raise --- rlbot/managers/bot.py | 2 ++ rlbot/managers/rendering.py | 1 + rlbot/managers/script.py | 2 ++ rlbot/version.py | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 9320967..783df20 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -151,6 +151,8 @@ def _packet_processor(self, packet: flat.GamePacket): e, ) print_exc() + if self.renderer.is_rendering(): + self.renderer.end_rendering() return player_input = flat.PlayerInput(self.index, controller) diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index f3d9685..77b41be 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -154,6 +154,7 @@ def begin_rendering(self, group_id: str = DEFAULT_GROUP_ID): ) return + self._current_renders.clear() self._group_id = Renderer._get_group_id(group_id) self._used_group_ids.add(self._group_id) diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 613b242..6bd33f1 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -123,6 +123,8 @@ def _packet_processor(self, packet: flat.GamePacket): self.logger.error( "Script %s encountered an error to RLBot: %s", self.name, e ) + if self.renderer.is_rendering(): + self.renderer.end_rendering() print_exc() def _run(self): diff --git a/rlbot/version.py b/rlbot/version.py index a7ed31d..27c7602 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.44" +__version__ = "2.0.0-beta.45" From ef8005a0d534a0c0e4c53b261d540854c38b16b5 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 14 Jun 2025 14:10:57 -0400 Subject: [PATCH 12/30] Update to newest spec --- rlbot/config.py | 19 +++++++++++++++---- rlbot/version.py | 2 +- tests/default.toml | 4 ++-- tests/gamemodes/beach_ball.toml | 2 +- tests/gamemodes/boomer_ball.toml | 2 +- tests/gamemodes/gforce_frenzy.toml | 2 +- tests/gamemodes/ghost_hunt.toml | 2 +- tests/gamemodes/gotham_city_rumble.toml | 2 +- tests/gamemodes/nike_fc_showdown.toml | 2 +- tests/gamemodes/speed_demon.toml | 2 +- tests/gamemodes/spike_rush.toml | 2 +- tests/gamemodes/spooky_cube.toml | 2 +- tests/gamemodes/super_cube.toml | 2 +- tests/gamemodes/winter_breakaway.toml | 2 +- tests/hivemind.toml | 2 +- tests/human_vs_atba.toml | 2 +- tests/human_vs_necto.toml | 2 +- tests/minimal.toml | 2 +- tests/nexto/bot.py | 17 +++++++---------- tests/psy.toml | 2 +- tests/render_test.toml | 2 +- tests/rlbot.toml | 2 +- tests/run_forever.py | 6 +++--- tests/series.toml | 2 +- 24 files changed, 47 insertions(+), 39 deletions(-) diff --git a/rlbot/config.py b/rlbot/config.py index 4226df8..6d60049 100644 --- a/rlbot/config.py +++ b/rlbot/config.py @@ -99,7 +99,9 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: load_player_config(abs_config_path, team, name, loadout_file) ) case "psyonix": - abs_config_path = (config_path.parent / car_config).resolve() + abs_config_path = ( + (config_path.parent / car_config).resolve() if car_config else None + ) players.append( load_psyonix_config( team, @@ -153,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"), @@ -168,9 +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=__enum( - match_table, "enable_rendering", flat.DebugRendering.OffByDefault - ), + enable_rendering=enable_rendering, enable_state_setting=__bool(match_table, "enable_state_setting"), freeplay=__bool(match_table, "freeplay"), ) diff --git a/rlbot/version.py b/rlbot/version.py index a7ed31d..07f103d 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.44" +__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..0977446 100644 --- a/tests/render_test.toml +++ b/tests/render_test.toml @@ -3,7 +3,7 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" enable_rendering = true 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/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" From acd455da4769d3a63c15338ea87beb2a2de7a284 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 14 Jun 2025 17:14:18 -0400 Subject: [PATCH 13/30] Support for the `RenderingStatus` message --- rlbot/interface.py | 8 +++-- rlbot/managers/bot.py | 59 +++++++++++++++++++++++++++++++------ rlbot/managers/hivemind.py | 55 +++++++++++++++++++++++++++------- rlbot/managers/rendering.py | 12 ++++---- rlbot/managers/script.py | 37 ++++++++++++++++++++++- tests/render_test.toml | 2 +- tests/render_test/render.py | 3 ++ tests/run_only.py | 2 +- 8 files changed, 146 insertions(+), 32 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index b8a351f..aba08f0 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -45,6 +45,7 @@ class SocketRelay: controllable_team_info_handlers: list[ Callable[[flat.ControllableTeamInfo], None] ] = [] + rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] raw_handlers: list[Callable[[flat.CoreMessage], None]] = [] def __init__( @@ -114,13 +115,11 @@ def send_msg( | flat.StopCommand | flat.SetLoadout | flat.InitComplete + | flat.RenderingStatus ), ): self.send_bytes(flat.InterfacePacket(msg).pack()) - def remove_render_group(self, group_id: int): - self.send_msg(flat.RemoveRenderGroup(group_id)) - def stop_match(self, shutdown_server: bool = False): self.send_msg(flat.StopCommand(shutdown_server)) @@ -298,6 +297,9 @@ def handle_incoming_message(self, incoming_message: bytes) -> MsgHandlingResult: 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 _: self.logger.warning( "Received unknown message type: %s", diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 765b707..99e037b 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -31,6 +31,7 @@ class Bot: index: int = -1 name: str = "" player_id: int = 0 + can_render: bool = False @property def spawn_id(self) -> int: @@ -90,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) @@ -103,15 +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: - 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 - try: self.initialize() except Exception as e: @@ -127,6 +122,24 @@ def _try_initialize(self): 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): @@ -228,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, diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index 1cf1759..53af8ea 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -33,6 +33,7 @@ class Hivemind: indices: list[int] = [] names: list[str] = [] player_ids: list[int] = [] + can_render: bool = False @property def spawn_ids(self) -> list[int]: @@ -92,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) @@ -104,16 +108,6 @@ def _try_initialize(self): ): return - # 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 - try: self.initialize() except Exception as e: @@ -131,6 +125,17 @@ def _try_initialize(self): 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): @@ -229,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, diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 31e697d..73449fa 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -55,10 +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_msg - - 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): @@ -141,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 @@ -151,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): @@ -160,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 274c999..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) @@ -88,12 +92,15 @@ def _try_initialize(self): 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, diff --git a/tests/render_test.toml b/tests/render_test.toml index 0977446..c3421a5 100644 --- a/tests/render_test.toml +++ b/tests/render_test.toml @@ -5,7 +5,7 @@ launcher = "Steam" [match] 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 df1c257..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 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__": From 1db1bab86b45b989e4e8650d57c42b9c9d83ce04 Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 22 Jun 2025 13:19:32 -0400 Subject: [PATCH 14/30] Add new map --- rlbot/utils/maps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index 56a544a..100eb70 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -82,6 +82,7 @@ "DriftWoods": "Woods_P", "Neotokyo_Arcade": "NeoTokyo_Arcade_P", "FuturaGarden": "UF_Day_P", + "DFHStadium_Anniversary": "stadium_10a_p", } STANDARD_MAPS = [ @@ -135,4 +136,5 @@ "DriftWoods", "Neotokyo_Arcade", "FuturaGarden", + "DFHStadium_Anniversary", ] From ce1801c94885eab5236fe555e6f791908598ef97 Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 22 Jun 2025 18:07:38 -0400 Subject: [PATCH 15/30] Add new map Also run black --- rlbot/managers/match.py | 2 +- rlbot/managers/rendering.py | 8 ++++++-- rlbot/utils/maps.py | 1 + tests/render_test/render.py | 12 ++++++++---- tests/run_only.py | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index b6ea5ec..e666de5 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -33,7 +33,7 @@ def __init__( self.rlbot_interface: SocketRelay = SocketRelay("") self.rlbot_interface.packet_handlers.append(self._packet_reporter) - def __enter__(self) -> 'MatchManager': + def __enter__(self) -> "MatchManager": return self def __exit__(self, exc_type, exc_val, exc_tb) -> None: diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 0a4dae6..f5e5b4e 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -123,7 +123,7 @@ def _get_group_id(group_id: str) -> int: return hash(str(group_id).encode("utf-8")) % MAX_INT @contextmanager - def context(self, group_id: str=DEFAULT_GROUP_ID, default_color=None): + def context(self, group_id: str = DEFAULT_GROUP_ID, default_color=None): """ Starts rendering as a context usable in with-statements. After the with-statement the rendering is automatically ended. @@ -230,7 +230,11 @@ def draw_line_3d( """ Draws a line between two anchors in 3d space. """ - self.draw(flat.Line3D(_get_anchor(start), _get_anchor(end), color or self._default_color)) + self.draw( + flat.Line3D( + _get_anchor(start), _get_anchor(end), color or self._default_color + ) + ) def draw_polyline_3d( self, diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index 100eb70..405b8e0 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -83,6 +83,7 @@ "Neotokyo_Arcade": "NeoTokyo_Arcade_P", "FuturaGarden": "UF_Day_P", "DFHStadium_Anniversary": "stadium_10a_p", + "Holyfield": "Labs_Holyfield_Space_P", } STANDARD_MAPS = [ diff --git a/tests/render_test/render.py b/tests/render_test/render.py index 0919734..3b543ea 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -35,11 +35,15 @@ def handle_packet(self, packet: flat.GamePacket): self.do_render(radius) - with self.renderer.context('tick', self.renderer.red): + with self.renderer.context("tick", self.renderer.red): self.renderer.set_resolution(1920, 1080) - hsv = self.renderer.create_color_hsv(packet.match_info.seconds_elapsed * 0.1, 1.0, 1.0) - self.renderer.draw_string_2d('HSV 300px 50px', 300, 50, 1.0, hsv) - self.renderer.draw_string_2d('Red 330px 70px', 330, 70, 1.0) # Use default color + hsv = self.renderer.create_color_hsv( + packet.match_info.seconds_elapsed * 0.1, 1.0, 1.0 + ) + self.renderer.draw_string_2d("HSV 300px 50px", 300, 50, 1.0, hsv) + self.renderer.draw_string_2d( + "Red 330px 70px", 330, 70, 1.0 + ) # Use default color self.renderer.set_resolution(1, 1) def do_render(self, radius: float): diff --git a/tests/run_only.py b/tests/run_only.py index e7496b0..8353683 100644 --- a/tests/run_only.py +++ b/tests/run_only.py @@ -5,7 +5,7 @@ DIR = Path(__file__).parent -MATCH_CONFIG_PATH = DIR / "render_test.toml" +MATCH_CONFIG_PATH = DIR / "rlbot.toml" RLBOT_SERVER_FOLDER = DIR / "../" if __name__ == "__main__": From f87ffbd70ecc56d35208afab256c607cda1b77fa Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 29 Jun 2025 15:04:45 -0400 Subject: [PATCH 16/30] Move `can_render` into `Renderer` --- rlbot/managers/bot.py | 7 +++---- rlbot/managers/hivemind.py | 7 +++---- rlbot/managers/rendering.py | 2 ++ rlbot/managers/script.py | 7 +++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index cd311b0..f5279c2 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -31,7 +31,6 @@ class Bot: index: int = -1 name: str = "" player_id: int = 0 - can_render: bool = False @property def spawn_id(self) -> int: @@ -247,10 +246,10 @@ 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. + By default, this will update `self.renderer.can_render` if appropriate. """ if update.is_bot and update.index == self.index: - self.can_render = update.status + self.renderer.can_render = update.status def update_rendering_status( self, @@ -261,7 +260,7 @@ def update_rendering_status( """ 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`. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.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. diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index 53af8ea..ef8ba4a 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -33,7 +33,6 @@ class Hivemind: indices: list[int] = [] names: list[str] = [] player_ids: list[int] = [] - can_render: bool = False @property def spawn_ids(self) -> list[int]: @@ -238,10 +237,10 @@ 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. + By default, this will update `self.renderer.can_render` if appropriate. """ if update.is_bot and update.index in self.indices: - self.can_render = update.status + self.renderer.can_render = update.status def update_rendering_status( self, @@ -252,7 +251,7 @@ def update_rendering_status( """ 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`. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.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. diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index f5e5b4e..963bd18 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -46,6 +46,8 @@ class Renderer: purple = flat.Color(128, 0, 128) teal = flat.Color(0, 128, 128) + can_render: bool = False + _logger = get_logger("renderer") _used_group_ids: set[int] = set() diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index d6bf6ae..2c7b747 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -26,7 +26,6 @@ class Script: index: int = 0 name: str = "UnknownScript" - can_render: bool = False match_config = flat.MatchConfiguration() field_info = flat.FieldInfo() @@ -193,10 +192,10 @@ 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. + By default, this will update `self.renderer.can_render` if appropriate. """ if not update.is_bot and update.index == self.index: - self.can_render = update.status + self.renderer.can_render = update.status def update_rendering_status( self, @@ -207,7 +206,7 @@ def update_rendering_status( """ 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`. + If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.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. From 1a2f4b057bce95044fdde189fb9972f851020add Mon Sep 17 00:00:00 2001 From: Virx Date: Tue, 8 Jul 2025 01:05:58 -0400 Subject: [PATCH 17/30] Fix match config bug --- rlbot/interface.py | 2 +- rlbot/managers/bot.py | 33 ++++++++++++++++----------------- rlbot/managers/hivemind.py | 32 +++++++++++++++++++------------- rlbot/managers/match.py | 2 +- rlbot/managers/script.py | 28 ++++++++++++++-------------- rlbot/version.py | 2 +- tests/render_test/render.py | 2 -- 7 files changed, 52 insertions(+), 49 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index aba08f0..491cf42 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -134,7 +134,7 @@ def start_match(self, match_config: Path | flat.MatchConfiguration): flatbuffer = settings case _: raise ValueError( - "Expected MatchSettings or path to match settings toml file" + "Expected MatchConfiguration or path to match config toml file" ) self.send_msg(flatbuffer) diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index f5279c2..43e6984 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -106,6 +106,19 @@ def _try_initialize(self): # Not ready to initialize return + 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 configuration", + self._game_interface.agent_id, + ) + try: self.initialize() except Exception as e: @@ -125,20 +138,6 @@ def _handle_match_config(self, match_config: flat.MatchConfiguration): 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): @@ -259,7 +258,7 @@ def update_rendering_status( ): """ 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. + Will be ignored if rendering has been set to AlwaysOff in the match configuration. If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.can_render`. - `status`: `True` to enable rendering, `False` to disable. @@ -281,7 +280,7 @@ def handle_match_comm( """ Called when a match communication message is received. See `send_match_comm`. - NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match settings. + NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match configuration. """ def send_match_comm( @@ -331,7 +330,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: Optional[int] = None): def initialize(self): """ - Called when the bot is ready for initialization. Field info, match settings, name, index, and team are + Called when the bot is ready for initialization. Field info, match configuration, name, index, and team are fully loaded at this point, and will not return garbage data unlike in `__init__`. """ diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index ef8ba4a..311ac25 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -107,6 +107,22 @@ def _try_initialize(self): ): return + # Search match configuration 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 + else: # else block runs if break was not hit + self._logger.warning( + "Hivemind with agent id '%s' did not find itself in the match configuration for player id %s", + self._game_interface.agent_id, + player_id, + ) + try: self.initialize() except Exception as e: @@ -125,16 +141,6 @@ 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): @@ -250,7 +256,7 @@ def update_rendering_status( ): """ 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. + Will be ignored if rendering has been set to AlwaysOff in the match configuration. If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.can_render`. - `status`: `True` to enable rendering, `False` to disable. @@ -283,7 +289,7 @@ def handle_match_comm( """ Called when a match communication message is received. See `send_match_comm`. - NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match settings. + NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match configuration. """ def send_match_comm( @@ -337,7 +343,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: int): def initialize(self): """ - Called when the bot is ready for initialization. Field info, match settings, name, index, and team are + Called when the bot is ready for initialization. Field info, match configuration, name, index, and team are fully loaded at this point, and will not return garbage data unlike in `__init__`. """ diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index e666de5..d6e3692 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -108,7 +108,7 @@ def start_match( ensure_server_started: bool = True, ): """ - Starts a match using the given match settings or a path to a match settings toml file. + Starts a match using the given match configuration or a path to a match config toml file. Connection is automatically established if missing. Call `connect` if you want this process to receive match communication or ball prediction messages. """ diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 2c7b747..8aea134 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -75,6 +75,17 @@ def _try_initialize(self): self.logger = get_logger(self.name) + for i, script in enumerate(self.match_config.script_configurations): + if script.agent_id == self._game_interface.agent_id: + self.index = i + self.name = script.name + break + else: # else block runs if break was not hit + self.logger.warning( + "Script with agent id '%s' did not find itself in the match configuration", + self._game_interface.agent_id, + ) + try: self.initialize() except Exception as e: @@ -96,17 +107,6 @@ def _handle_match_config(self, match_config: flat.MatchConfiguration): 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 - break - else: # else block runs if break was not hit - self.logger.warning( - "Script 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): @@ -205,7 +205,7 @@ def update_rendering_status( ): """ 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. + Will be ignored if rendering has been set to AlwaysOff in the match configuration. If the status is successfully updated, the `self.rendering_status_update` method will be called which will update `self.renderer.can_render`. - `status`: `True` to enable rendering, `False` to disable. @@ -227,7 +227,7 @@ def handle_match_comm( """ Called when a match communication message is received. See `send_match_comm`. - NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match settings. + NOTE: Messages from scripts will have `team == 2` and the index will be its index in the match configuration. """ def send_match_comm( @@ -276,7 +276,7 @@ def set_loadout(self, loadout: flat.PlayerLoadout, index: int): def initialize(self): """ - Called when the script is ready for initialization. Field info, match settings, name, and index are + Called when the script is ready for initialization. Field info, match configuration, name, and index are fully loaded at this point, and will not return garbage data unlike in `__init__`. """ diff --git a/rlbot/version.py b/rlbot/version.py index 07f103d..5a6dd25 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.46" +__version__ = "2.0.0-beta.47" diff --git a/tests/render_test/render.py b/tests/render_test/render.py index 3b543ea..9487bcd 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -1,5 +1,3 @@ -from email.policy import default - from rlbot import flat from rlbot.flat import BallAnchor, CarAnchor, Color, RenderAnchor, Vector3 from rlbot.managers import Script From 72295563c133b6514996866a95bbe59424cf1039 Mon Sep 17 00:00:00 2001 From: Virx Date: Wed, 9 Jul 2025 00:29:28 -0400 Subject: [PATCH 18/30] Tiny refactor --- rlbot/interface.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index 491cf42..a4ffda4 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -243,21 +243,6 @@ def handle_incoming_messages(self, blocking: bool = False) -> MsgHandlingResult: try: self.socket.setblocking(blocking) incoming_message = self.read_message() - try: - return self.handle_incoming_message(incoming_message) - except flat.InvalidFlatbuffer as e: - self.logger.error( - "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", - e, - ) - return MsgHandlingResult.TERMINATED except BlockingIOError: # No incoming messages and blocking==False return MsgHandlingResult.NO_INCOMING_MSGS @@ -265,10 +250,26 @@ def handle_incoming_messages(self, blocking: bool = False) -> MsgHandlingResult: self.logger.error("SocketRelay disconnected unexpectedly!") return MsgHandlingResult.TERMINATED + try: + return self.handle_incoming_message(incoming_message) + except flat.InvalidFlatbuffer as e: + self.logger.error( + "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", + e, + ) + return MsgHandlingResult.TERMINATED + 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). + Returns True if the message was NOT a shutdown request """ flatbuffer = flat.CorePacket.unpack(incoming_message).message From be40669e0baef6c42f596a4f123c9f0befd74691 Mon Sep 17 00:00:00 2001 From: Virx Date: Wed, 9 Jul 2025 13:08:21 -0400 Subject: [PATCH 19/30] Add `DriftWoods_Night` Remove `Farmstead_Upsidedown` --- rlbot/utils/maps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index 405b8e0..689e6ce 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -71,7 +71,6 @@ "EstadioVida_Dusk": "ff_dusk_p", "Mannfield_Dusk": "eurostadium_dusk_p", "Farmstead_Pitched": "farm_grs_p", - "Farmstead_Upsidedown": "farm_hw_p", "Wasteland_Pitched": "wasteland_grs_p", "Neotokyo_Hacked": "neotokyo_hax_p", "AquaDome_Shallows": "Underwater_GRS_P", @@ -84,6 +83,7 @@ "FuturaGarden": "UF_Day_P", "DFHStadium_Anniversary": "stadium_10a_p", "Holyfield": "Labs_Holyfield_Space_P", + "DriftWoods_Night": "woods_night_p" } STANDARD_MAPS = [ @@ -138,4 +138,5 @@ "Neotokyo_Arcade", "FuturaGarden", "DFHStadium_Anniversary", + "DriftWoods_Night" ] From 74efdbc171215ad98ef4aa210c6708ba3a9e6697 Mon Sep 17 00:00:00 2001 From: Virx Date: Tue, 29 Jul 2025 15:24:35 -0400 Subject: [PATCH 20/30] Add `get_human` --- rlbot/config.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rlbot/config.py b/rlbot/config.py index 6d60049..771aa0e 100644 --- a/rlbot/config.py +++ b/rlbot/config.py @@ -3,7 +3,6 @@ from typing import Any, Literal import rlbot.flat as flat -from rlbot.utils.logging import DEFAULT_LOGGER as logger from rlbot.utils.os_detector import CURRENT_OS, OS @@ -112,7 +111,7 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: ) ) case "human": - players.append(flat.PlayerConfiguration(flat.Human(), team, 0)) + players.append(get_human(team)) case t: raise ConfigParsingException( f"Invalid player type {repr(t)} for player {len(players)}." @@ -323,6 +322,13 @@ def load_psyonix_config( ) +def get_human(team: int) -> flat.PlayerConfiguration: + """ + Creates a `PlayerConfiguration` for a human player on the given team. + """ + return flat.PlayerConfiguration(flat.Human(), team) + + def load_script_config(path: Path | str) -> flat.ScriptConfiguration: """ Reads the script toml file at the provided path and creates a `ScriptConfiguration` from it. From 8257d0f9975c2b32b1f55c0f8b317d514e6c1205 Mon Sep 17 00:00:00 2001 From: Virx Date: Tue, 29 Jul 2025 15:25:03 -0400 Subject: [PATCH 21/30] Fix script in py 3.11 --- tests/cfg_to_toml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/cfg_to_toml.py b/tests/cfg_to_toml.py index 4a1b560..d98e21f 100644 --- a/tests/cfg_to_toml.py +++ b/tests/cfg_to_toml.py @@ -113,9 +113,9 @@ def convert_to_toml(self) -> dict[str, dict[str, Any]]: toml_dict["settings"] = self._convert_settings() toml_dict["details"] = self._convert_details() - toml_dict["settings"][ - "agent_id" - ] = f"{toml_dict["details"]["developer"]}/{toml_dict["settings"]["name"]}" + toml_dict["settings"]["agent_id"] = ( + f"{toml_dict['details']['developer']}/{toml_dict['settings']['name']}" + ) return toml_dict From 13b1654aa49a2da0b87f2fcc782523e68e0a64a8 Mon Sep 17 00:00:00 2001 From: Virx Date: Tue, 29 Jul 2025 15:39:02 -0400 Subject: [PATCH 22/30] Migrate from black to ruff --- README.md | 7 ++++--- pyproject.toml | 9 +++++++++ rlbot/__init__.py | 2 +- rlbot/interface.py | 6 +++--- rlbot/utils/maps.py | 4 ++-- tests/fashion/bot.py | 2 +- tests/nexto/bot.py | 1 - tests/nexto/nexto_obs.py | 3 +-- tests/run_forever.py | 1 - tests/runner.py | 3 +-- 10 files changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9f6c354..2c00216 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,11 @@ The following is how to setup a development environment for this project, NOT ho - `pip uninstall rlbot_flatbuffers` - `pip install --editable ` -This project is formatted using [Black](https://github.com/psf/black). +This project is formatted using [Ruff](https://docs.astral.sh/ruff/formatter/). -- Install: `pip install black`. -- Use: `black .` +- Install: `pip install ruff`. +- Sort imports: `ruff check --select I --fix` +- Format code: `ruff format` ## Testing diff --git a/pyproject.toml b/pyproject.toml index 21019fe..2372924 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,3 +23,12 @@ Repository = "https://github.com/RLBot/python-interface" [tool.setuptools.dynamic] version = {attr = "rlbot.version.__version__"} + +[dependency-groups] +dev = [ + "ruff>=0.12.5", + "toml>=0.10.2", +] + +[tool.ruff.format] +docstring-code-format = true diff --git a/rlbot/__init__.py b/rlbot/__init__.py index 58f3ace..614d684 100644 --- a/rlbot/__init__.py +++ b/rlbot/__init__.py @@ -1 +1 @@ -from .version import __version__ +from .version import __version__ as __version__ diff --git a/rlbot/interface.py b/rlbot/interface.py index a4ffda4..a00c8d9 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -323,7 +323,7 @@ def disconnect(self): self.logger.critical("RLBot is not responding to our disconnect request!?") self._running = False - assert ( - not self._running - ), "Disconnect request or timeout should have set self._running to False" + assert not self._running, ( + "Disconnect request or timeout should have set self._running to False" + ) self.is_connected = False diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index 689e6ce..e397772 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -83,7 +83,7 @@ "FuturaGarden": "UF_Day_P", "DFHStadium_Anniversary": "stadium_10a_p", "Holyfield": "Labs_Holyfield_Space_P", - "DriftWoods_Night": "woods_night_p" + "DriftWoods_Night": "woods_night_p", } STANDARD_MAPS = [ @@ -138,5 +138,5 @@ "Neotokyo_Arcade", "FuturaGarden", "DFHStadium_Anniversary", - "DriftWoods_Night" + "DriftWoods_Night", ] diff --git a/tests/fashion/bot.py b/tests/fashion/bot.py index 553a385..d1bf4bc 100644 --- a/tests/fashion/bot.py +++ b/tests/fashion/bot.py @@ -50,7 +50,7 @@ def get_output(self, packet: flat.GamePacket) -> flat.ControllerState: ), ) - self.logger.info(f"State setting new loadout") + self.logger.info("State setting new loadout") self.set_loadout(loadout) self.last_tick = packet.match_info.frame_num diff --git a/tests/nexto/bot.py b/tests/nexto/bot.py index c4af5a9..a57ac28 100644 --- a/tests/nexto/bot.py +++ b/tests/nexto/bot.py @@ -369,7 +369,6 @@ def toxicity(self, packet: GamePacket): return for p in human_opps: - d = math.sqrt( (p.physics.location.x - bad_goal[0]) ** 2 + (p.physics.location.y - bad_goal[1]) ** 2 diff --git a/tests/nexto/nexto_obs.py b/tests/nexto/nexto_obs.py index b54c3df..8f22669 100644 --- a/tests/nexto/nexto_obs.py +++ b/tests/nexto/nexto_obs.py @@ -286,8 +286,7 @@ def batched_build_obs(self, encoded_states: np.ndarray): for i in range(n_players): encoded_player = encoded_states[ :, - players_start_index - + i * player_length : players_start_index + players_start_index + i * player_length : players_start_index + (i + 1) * player_length, ] diff --git a/tests/run_forever.py b/tests/run_forever.py index 749772a..830a85a 100644 --- a/tests/run_forever.py +++ b/tests/run_forever.py @@ -13,7 +13,6 @@ if __name__ == "__main__": match_manager = MatchManager(RLBOT_SERVER_FOLDER) - match_manager.ensure_server_started() current_map = -1 diff --git a/tests/runner.py b/tests/runner.py index d93d930..e74390c 100644 --- a/tests/runner.py +++ b/tests/runner.py @@ -16,7 +16,6 @@ match_manager = MatchManager(RLBOT_SERVER_FOLDER) try: - match_manager.ensure_server_started() match_manager.start_match(MATCH_CONFIG_PATH) logger.info("Waiting before shutdown...") @@ -25,7 +24,7 @@ except KeyboardInterrupt: logger.warning("Shutting down early due to interrupt") except Exception: - logger.critical(f"Shutting down early due to the following error:") + logger.critical("Shutting down early due to the following error:") print_exc() match_manager.shut_down() From 239215ac4f2d3f1006fb3abf5d7c111fce86473e Mon Sep 17 00:00:00 2001 From: Virx Date: Thu, 31 Jul 2025 00:20:42 -0400 Subject: [PATCH 23/30] Move `self.rlbot_interface.run(...)` into `self.connect(...)` --- .gitignore | 1 + rlbot/managers/match.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index bcbe491..daa991a 100644 --- a/.gitignore +++ b/.gitignore @@ -175,6 +175,7 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python +uv.lock .vscode/ *.obj RLBotServer* diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index d6e3692..a6fbb0a 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -93,6 +93,7 @@ def connect( rlbot_server_ip=rlbot_server_ip, rlbot_server_port=rlbot_server_port or self.rlbot_server_port, ) + self.rlbot_interface.run(background_thread=True) def wait_for_first_packet(self): while self.packet is None or self.packet.match_info.match_phase in { @@ -122,7 +123,6 @@ def start_match( wants_ball_predictions=False, close_between_matches=False, ) - self.rlbot_interface.run(background_thread=True) self.rlbot_interface.start_match(config) @@ -136,7 +136,7 @@ def start_match( def disconnect(self): """ - Disconnect from the RLBotServer. + Disconnect from RLBotServer. Note that the server will continue running as long as Rocket League does. """ self.rlbot_interface.disconnect() @@ -167,8 +167,8 @@ def shut_down(self, use_force_if_necessary: bool = True): self.logger.info("Shutting down RLBot...") - # In theory this is all we need for the server to cleanly shut itself down try: + # In theory this is all we need for the server to cleanly shut itself down self.rlbot_interface.stop_match(shutdown_server=True) except BrokenPipeError: match gateway.find_server_process(self.main_executable_name)[0]: From 68c39095c626c8ebbac5cc45efcb3ad3b7d8b133 Mon Sep 17 00:00:00 2001 From: Virx Date: Thu, 31 Jul 2025 00:21:29 -0400 Subject: [PATCH 24/30] Infinite match series testing --- tests/atba/atba.bot.toml | 2 +- tests/atba/atba.py | 23 +++-------- tests/many_match.py | 84 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 tests/many_match.py diff --git a/tests/atba/atba.bot.toml b/tests/atba/atba.bot.toml index a2f5885..efe7374 100644 --- a/tests/atba/atba.bot.toml +++ b/tests/atba/atba.bot.toml @@ -14,7 +14,7 @@ root_dir = "" run_command = "..\\..\\venv\\Scripts\\python atba.py" # The command RLBot will call to start your bot on linux # If this isn't set, RLBot may attempt to run the Windows command under Wine -run_command_linux = "../../venv/bin/python atba.py" +run_command_linux = "../../.venv/bin/python atba.py" [details] description = "Made possible by RLBot" diff --git a/tests/atba/atba.py b/tests/atba/atba.py index a986804..4d5cc3c 100644 --- a/tests/atba/atba.py +++ b/tests/atba/atba.py @@ -1,7 +1,6 @@ from __future__ import annotations import math -from typing import Optional from rlbot import flat from rlbot.managers import Bot @@ -67,9 +66,9 @@ def __init__(self): def initialize(self): self.logger.info("Initializing agent!") - - num_boost_pads = len(self.field_info.boost_pads) - self.logger.info(f"There are {num_boost_pads} boost pads on the field.") + self.logger.info( + f"There are {len(self.field_info.boost_pads)} boost pads and {len(self.field_info.goals)} goals on the field." + ) if self.rendering: self.renderer.begin_rendering("custom one-time rendering group") @@ -83,16 +82,6 @@ def initialize(self): ) self.renderer.end_rendering() - def handle_match_comm( - self, - index: int, - team: int, - content: bytes, - display: Optional[str], - team_only: bool, - ): - self.logger.info(f"Received match communication from index {index}! {display}") - def get_output(self, packet: flat.GamePacket) -> flat.ControllerState: if self.rendering: self.test_rendering(packet) @@ -110,10 +99,10 @@ def get_output(self, packet: flat.GamePacket) -> flat.ControllerState: if self.state_setting: self.test_state_setting(packet) - if self.match_comms: + if self.match_comms and packet.match_info.match_phase == flat.MatchPhase.Active: # Limit packet spam if packet.match_info.frame_num - self.last_send >= 360: - self.send_match_comm(b"", "Hello world!", True) + self.send_match_comm(b"", "Hello world!", False) self.last_send = packet.match_info.frame_num ball_location = Vector2(packet.balls[0].physics.location) @@ -145,7 +134,7 @@ def test_state_setting(self, packet: flat.GamePacket): i: flat.DesiredCarState( flat.DesiredPhysics(rotation=flat.RotatorPartial(yaw=0)) ) - for i, car in enumerate(packet.players) + for i in range(len(packet.players)) }, ) diff --git a/tests/many_match.py b/tests/many_match.py new file mode 100644 index 0000000..90450af --- /dev/null +++ b/tests/many_match.py @@ -0,0 +1,84 @@ +from pathlib import Path +from time import sleep + +from rlbot import flat +from rlbot.config import load_player_config +from rlbot.managers import MatchManager + +DIR = Path(__file__).parent + +BOT_PATH = DIR / "atba/atba.bot.toml" +RLBOT_SERVER_FOLDER = DIR / "../../core/RLBotCS/bin/Release/" + +num_comms = set() + + +def handle_match_comm(comm: flat.MatchComm): + global num_comms + if comm.team < 2: + num_comms.add(comm.index) + + +if __name__ == "__main__": + match_manager = MatchManager(RLBOT_SERVER_FOLDER) + match_manager.rlbot_interface.match_comm_handlers.append(handle_match_comm) + match_manager.ensure_server_started() + match_manager.connect( + wants_match_communications=True, + wants_ball_predictions=False, + close_between_matches=False, + ) + + current_map = -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, + wait_for_agents=True, + existing_match_behavior=flat.ExistingMatchBehavior.Restart, + game_map_upk="Stadium_P", + instant_start=True, + enable_state_setting=True, + player_configurations=[ + blue_bot, + blue_bot, + blue_bot, + blue_bot, + blue_bot, + orange_bot, + orange_bot, + orange_bot, + orange_bot, + orange_bot, + ], + ) + + num_games = 0 + paused = False + + while not paused: + num_games += 1 + print(f"Starting match # {num_games}") + + match_manager.start_match(match_settings, ensure_server_started=False) + # when calling start_match, by default it will wait for the first packet + assert match_manager.packet is not None + + sleep(2) + num_comms.clear() + while len(num_comms) < 10: + # give an extra 5 seconds for the match to start before calling it a failure + if ( + match_manager.packet.match_info.match_phase == flat.MatchPhase.Active + and match_manager.packet.match_info.game_time_remaining < 60 * 4 + 55 + ): + match_manager.set_game_state(commands=["Pause"]) + paused = True + break + sleep(1) + + print("Failed to start match. Paused and exiting.") + match_manager.disconnect() From 6435cf854d43ff38bbafe9fc0c7bba50c1bad297 Mon Sep 17 00:00:00 2001 From: Virx Date: Thu, 31 Jul 2025 00:21:33 -0400 Subject: [PATCH 25/30] Bump version --- rlbot/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbot/version.py b/rlbot/version.py index 5a6dd25..1a64d22 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.47" +__version__ = "2.0.0-beta.48" From 1c7d7c9aa757345e64e53e759f45b07acb8ecd1d Mon Sep 17 00:00:00 2001 From: VirxEC Date: Tue, 12 Aug 2025 20:31:08 -0400 Subject: [PATCH 26/30] Add `NeoTokyo_Comic` to `STANDARD_MAPS` --- rlbot/utils/maps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index e397772..a8c2acb 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -60,7 +60,6 @@ "Barricade": "Labs_PillarHeat_P", "Colossus": "Labs_PillarWings_P", "BeckwithPark_Snowy": "Park_Snowy_P", - "NeoTokyo_Comic": "NeoTokyo_Toon_P", "UtopiaColiseum_Gilded": "UtopiaStadium_Lux_P", "SovereignHeights": "Street_P", "Hoops_TheBlock": "HoopsStreet_P", @@ -139,4 +138,5 @@ "FuturaGarden", "DFHStadium_Anniversary", "DriftWoods_Night", + "NeoTokyo_Comic", ] From 47e465bb153447a33a64f41a1c7a3933f585e0be Mon Sep 17 00:00:00 2001 From: VirxEC Date: Tue, 12 Aug 2025 20:33:17 -0400 Subject: [PATCH 27/30] Re-add `NeoTokyo_Comic` to `GAME_MAP_TO_UPK` (whoops) --- rlbot/utils/maps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index a8c2acb..98f14b2 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -60,6 +60,7 @@ "Barricade": "Labs_PillarHeat_P", "Colossus": "Labs_PillarWings_P", "BeckwithPark_Snowy": "Park_Snowy_P", + "NeoTokyo_Comic": "NeoTokyo_Toon_P", "UtopiaColiseum_Gilded": "UtopiaStadium_Lux_P", "SovereignHeights": "Street_P", "Hoops_TheBlock": "HoopsStreet_P", From baa380e453d759cf1d1a26eddb925737eb91f2cc Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 13 Aug 2025 17:04:20 -0400 Subject: [PATCH 28/30] Add `run` and `connect_and_run` to `MatchManager` --- rlbot/managers/match.py | 40 ++++++++++++++++++++++++++++++++++++++-- tests/many_match.py | 3 ++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index a6fbb0a..fdaf125 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -93,7 +93,42 @@ def connect( rlbot_server_ip=rlbot_server_ip, rlbot_server_port=rlbot_server_port or self.rlbot_server_port, ) - self.rlbot_interface.run(background_thread=True) + + def run(self, *, background_thread: bool = False): + """ + Handle incoming messages until disconnected. + + - background_thread: If `True`, a background thread will be started to process messages. + """ + self.rlbot_interface.run(background_thread=background_thread) + + def connect_and_run( + self, + *, + wants_match_communications: bool, + wants_ball_predictions: bool, + close_between_matches: bool = True, + rlbot_server_ip: str = RLBOT_SERVER_IP, + rlbot_server_port: Optional[int] = None, + background_thread: bool = False, + ): + """ + Connects to the RLBot server specifying the given settings. + + - wants_match_communications: Whether match communication messages should be sent to this process. + - wants_ball_predictions: Whether ball prediction messages should be sent to this process. + - close_between_matches: Whether RLBot should close this connection between matches, specifically upon + `StartMatch` and `StopMatch` messages, since RLBot does not actually detect the ending of matches. + - background_thread: If `True`, a background thread will be started to process messages. + """ + self.connect( + wants_match_communications=wants_match_communications, + wants_ball_predictions=wants_ball_predictions, + close_between_matches=close_between_matches, + rlbot_server_ip=rlbot_server_ip, + rlbot_server_port=rlbot_server_port, + ) + self.run(background_thread=background_thread) def wait_for_first_packet(self): while self.packet is None or self.packet.match_info.match_phase in { @@ -118,10 +153,11 @@ def start_match( self.ensure_server_started() if not self.rlbot_interface.is_connected: - self.connect( + self.connect_and_run( wants_match_communications=False, wants_ball_predictions=False, close_between_matches=False, + background_thread=True, ) self.rlbot_interface.start_match(config) diff --git a/tests/many_match.py b/tests/many_match.py index 90450af..8cc093b 100644 --- a/tests/many_match.py +++ b/tests/many_match.py @@ -23,10 +23,11 @@ def handle_match_comm(comm: flat.MatchComm): match_manager = MatchManager(RLBOT_SERVER_FOLDER) match_manager.rlbot_interface.match_comm_handlers.append(handle_match_comm) match_manager.ensure_server_started() - match_manager.connect( + match_manager.connect_and_run( wants_match_communications=True, wants_ball_predictions=False, close_between_matches=False, + background_thread=False, ) current_map = -1 From 201bec665a672ace6cf97f194b71cbb83de1b49c Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 14 Sep 2025 18:58:38 -0400 Subject: [PATCH 29/30] Update `rlbot_flatbuffers` to v0.18 --- pyproject.toml | 4 ++-- rlbot/interface.py | 9 ++++----- rlbot/managers/bot.py | 30 ++++++++---------------------- rlbot/managers/hivemind.py | 28 +++++++--------------------- rlbot/managers/match.py | 13 ++++++------- rlbot/managers/rendering.py | 3 +-- rlbot/managers/script.py | 13 ++++++------- rlbot/utils/__init__.py | 4 +--- rlbot/utils/gateway.py | 5 ++--- rlbot/version.py | 2 +- 10 files changed, 38 insertions(+), 73 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2372924..2cfdce7 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.17.0", + "rlbot_flatbuffers~=0.18.2", "psutil==7.*", ] readme = "README.md" @@ -26,7 +26,7 @@ version = {attr = "rlbot.version.__version__"} [dependency-groups] dev = [ - "ruff>=0.12.5", + "ruff>=0.13.0", "toml>=0.10.2", ] diff --git a/rlbot/interface.py b/rlbot/interface.py index a00c8d9..bc934de 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -5,7 +5,6 @@ from pathlib import Path from socket import IPPROTO_TCP, TCP_NODELAY, socket from threading import Thread -from typing import Optional from rlbot import flat from rlbot.utils.logging import get_logger @@ -46,13 +45,13 @@ class SocketRelay: Callable[[flat.ControllableTeamInfo], None] ] = [] rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] - raw_handlers: list[Callable[[flat.CoreMessage], None]] = [] + raw_handlers: list[Callable[[flat.CorePacket], None]] = [] def __init__( self, agent_id: str, connection_timeout: float = 120, - logger: Optional[logging.Logger] = None, + logger: logging.Logger | None = None, ): self.agent_id = agent_id self.connection_timeout = connection_timeout @@ -272,12 +271,12 @@ def handle_incoming_message(self, incoming_message: bytes) -> MsgHandlingResult: Returns True if the message was NOT a shutdown request """ - flatbuffer = flat.CorePacket.unpack(incoming_message).message + flatbuffer = flat.CorePacket.unpack(incoming_message) for raw_handler in self.raw_handlers: raw_handler(flatbuffer) - match flatbuffer.item: + match flatbuffer.message: case flat.DisconnectSignal(): return MsgHandlingResult.TERMINATED case flat.GamePacket() as packet: diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 43e6984..57eae8f 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -1,6 +1,5 @@ import os from traceback import print_exc -from typing import Optional from rlbot import flat from rlbot.interface import ( @@ -13,8 +12,6 @@ from rlbot.utils import fill_desired_game_state from rlbot.utils.logging import DEFAULT_LOGGER, get_logger -WARNED_SPAWN_ID_DEPRECATED = False - class Bot: """ @@ -32,17 +29,6 @@ class Bot: name: str = "" player_id: int = 0 - @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() """ Contains info about what map you're on, game mode, mutators, etc. @@ -63,10 +49,10 @@ def spawn_id(self) -> int: _has_field_info = False _has_player_mapping = False - _latest_packet: Optional[flat.GamePacket] = None + _latest_packet: flat.GamePacket | None = None _latest_prediction = flat.BallPrediction() - def __init__(self, default_agent_id: Optional[str] = None): + def __init__(self, default_agent_id: str | None = None): agent_id = os.environ.get("RLBOT_AGENT_ID") or default_agent_id if agent_id is None: @@ -107,7 +93,7 @@ def _try_initialize(self): return for player in self.match_config.player_configurations: - match player.variety.item: + match player.variety: case flat.CustomBot(name): if player.player_id == self.player_id: self.name = name @@ -253,7 +239,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): def update_rendering_status( self, status: bool, - index: Optional[int] = None, + index: int | None = None, is_bot: bool = True, ): """ @@ -274,7 +260,7 @@ def handle_match_comm( index: int, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ @@ -284,7 +270,7 @@ def handle_match_comm( """ def send_match_comm( - self, content: bytes, display: Optional[str] = None, team_only: bool = False + self, content: bytes, display: str | None = None, team_only: bool = False ): """ Emits a match communication message to other bots and scripts. @@ -307,7 +293,7 @@ def set_game_state( self, balls: dict[int, flat.DesiredBallState] = {}, cars: dict[int, flat.DesiredCarState] = {}, - match_info: Optional[flat.DesiredMatchInfo] = None, + match_info: flat.DesiredMatchInfo | None = None, commands: list[str] = [], ): """ @@ -319,7 +305,7 @@ def set_game_state( game_state = fill_desired_game_state(balls, cars, match_info, commands) self._game_interface.send_msg(game_state) - def set_loadout(self, loadout: flat.PlayerLoadout, index: Optional[int] = None): + def set_loadout(self, loadout: flat.PlayerLoadout, index: int | None = None): """ Sets the loadout of a bot. Can be used to select or generate a loadout for the match when called inside `initialize`. diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index 311ac25..9af78b2 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -1,7 +1,6 @@ import os from logging import Logger from traceback import print_exc -from typing import Optional from rlbot import flat from rlbot.interface import ( @@ -14,8 +13,6 @@ from rlbot.utils import fill_desired_game_state from rlbot.utils.logging import DEFAULT_LOGGER, get_logger -WARNED_SPAWN_ID_DEPRECATED = False - class Hivemind: """ @@ -34,17 +31,6 @@ class Hivemind: names: list[str] = [] player_ids: list[int] = [] - @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() """ Contains info about what map you're on, game mode, mutators, etc. @@ -65,10 +51,10 @@ def spawn_ids(self) -> list[int]: _has_field_info = False _has_player_mapping = False - _latest_packet: Optional[flat.GamePacket] = None + _latest_packet: flat.GamePacket | None = None _latest_prediction = flat.BallPrediction() - def __init__(self, default_agent_id: Optional[str] = None): + def __init__(self, default_agent_id: str | None = None): agent_id = os.environ.get("RLBOT_AGENT_ID") or default_agent_id if agent_id is None: @@ -110,7 +96,7 @@ def _try_initialize(self): # Search match configuration for our spawn ids for player_id in self.player_ids: for player in self.match_config.player_configurations: - match player.variety.item: + match player.variety: case flat.CustomBot(name): if player.player_id == player_id: self.names.append(name) @@ -251,7 +237,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): def update_rendering_status( self, status: bool, - index: Optional[int] = None, + index: int | None = None, is_bot: bool = True, ): """ @@ -283,7 +269,7 @@ def handle_match_comm( index: int, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ @@ -296,7 +282,7 @@ def send_match_comm( self, index: int, content: bytes, - display: Optional[str] = None, + display: str | None = None, team_only: bool = False, ): """ @@ -320,7 +306,7 @@ def set_game_state( self, balls: dict[int, flat.DesiredBallState] = {}, cars: dict[int, flat.DesiredCarState] = {}, - match_info: Optional[flat.DesiredMatchInfo] = None, + match_info: flat.DesiredMatchInfo | None = None, commands: list[str] = [], ): """ diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index fdaf125..ba4ea7b 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -1,6 +1,5 @@ from pathlib import Path from time import sleep -from typing import Optional import psutil @@ -17,14 +16,14 @@ class MatchManager: """ logger = DEFAULT_LOGGER - packet: Optional[flat.GamePacket] = None - rlbot_server_process: Optional[psutil.Process] = None + packet: flat.GamePacket | None = None + rlbot_server_process: psutil.Process | None = None rlbot_server_port = RLBOT_SERVER_PORT initialized = False def __init__( self, - main_executable_path: Optional[Path] = None, + main_executable_path: Path | None = None, main_executable_name: str = MAIN_EXECUTABLE_NAME, ): self.main_executable_path = main_executable_path @@ -76,7 +75,7 @@ def connect( wants_ball_predictions: bool, close_between_matches: bool = True, rlbot_server_ip: str = RLBOT_SERVER_IP, - rlbot_server_port: Optional[int] = None, + rlbot_server_port: int | None = None, ): """ Connects to the RLBot server specifying the given settings. @@ -109,7 +108,7 @@ def connect_and_run( wants_ball_predictions: bool, close_between_matches: bool = True, rlbot_server_ip: str = RLBOT_SERVER_IP, - rlbot_server_port: Optional[int] = None, + rlbot_server_port: int | None = None, background_thread: bool = False, ): """ @@ -184,7 +183,7 @@ def set_game_state( self, balls: dict[int, flat.DesiredBallState] = {}, cars: dict[int, flat.DesiredCarState] = {}, - match_info: Optional[flat.DesiredMatchInfo] = None, + match_info: flat.DesiredMatchInfo | None = None, commands: list[str] = [], ): """ diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 963bd18..8eef087 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -1,7 +1,6 @@ import math from collections.abc import Callable, Sequence from contextlib import contextmanager -from typing import Optional from rlbot import flat from rlbot.interface import SocketRelay @@ -51,7 +50,7 @@ class Renderer: _logger = get_logger("renderer") _used_group_ids: set[int] = set() - _group_id: Optional[int] = None + _group_id: int | None = None _current_renders: list[flat.RenderMessage] = [] _default_color = white diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 8aea134..a3f09f7 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -1,6 +1,5 @@ import os from traceback import print_exc -from typing import Optional from rlbot import flat from rlbot.interface import ( @@ -35,10 +34,10 @@ class Script: _has_match_settings = False _has_field_info = False - _latest_packet: Optional[flat.GamePacket] = None + _latest_packet: flat.GamePacket | None = None _latest_prediction = flat.BallPrediction() - def __init__(self, default_agent_id: Optional[str] = None): + def __init__(self, default_agent_id: str | None = None): agent_id = os.environ.get("RLBOT_AGENT_ID") or default_agent_id if agent_id is None: @@ -200,7 +199,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): def update_rendering_status( self, status: bool, - index: Optional[int] = None, + index: int | None = None, is_bot: bool = False, ): """ @@ -221,7 +220,7 @@ def handle_match_comm( index: int, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ @@ -231,7 +230,7 @@ def handle_match_comm( """ def send_match_comm( - self, content: bytes, display: Optional[str] = None, team_only: bool = False + self, content: bytes, display: str | None = None, team_only: bool = False ): """ Emits a match communication message to other bots and scripts. @@ -254,7 +253,7 @@ def set_game_state( self, balls: dict[int, flat.DesiredBallState] = {}, cars: dict[int, flat.DesiredCarState] = {}, - match_info: Optional[flat.DesiredMatchInfo] = None, + match_info: flat.DesiredMatchInfo | None = None, commands: list[str] = [], ): """ diff --git a/rlbot/utils/__init__.py b/rlbot/utils/__init__.py index c478c2e..86e683e 100644 --- a/rlbot/utils/__init__.py +++ b/rlbot/utils/__init__.py @@ -1,12 +1,10 @@ -from typing import Optional - from rlbot import flat def fill_desired_game_state( balls: dict[int, flat.DesiredBallState] = {}, cars: dict[int, flat.DesiredCarState] = {}, - match_info: Optional[flat.DesiredMatchInfo] = None, + match_info: flat.DesiredMatchInfo | None = None, commands: list[str] = [], ) -> flat.DesiredGameState: """ diff --git a/rlbot/utils/gateway.py b/rlbot/utils/gateway.py index d0ace75..1df3e37 100644 --- a/rlbot/utils/gateway.py +++ b/rlbot/utils/gateway.py @@ -3,7 +3,6 @@ import stat import subprocess from pathlib import Path -from typing import Optional import psutil @@ -17,7 +16,7 @@ def find_main_executable_path( main_executable_path: Path, main_executable_name: str -) -> tuple[Path, Optional[Path]]: +) -> tuple[Path, Path | None]: main_executable_path = main_executable_path.absolute().resolve() # check if the path is directly to the main executable @@ -88,7 +87,7 @@ def launch( def find_server_process( main_executable_name: str, -) -> tuple[Optional[psutil.Process], int]: +) -> tuple[psutil.Process | None, int]: logger = DEFAULT_LOGGER for proc in psutil.process_iter(): try: diff --git a/rlbot/version.py b/rlbot/version.py index 1a64d22..57b560f 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.48" +__version__ = "2.0.0-beta.49" From 3fc04a94633d5ea5ee421975e5a9d953870e4cd8 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 20 Sep 2025 14:46:26 -0400 Subject: [PATCH 30/30] Fix render test --- tests/render_test/render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/render_test/render.py b/tests/render_test/render.py index 9487bcd..3582d67 100644 --- a/tests/render_test/render.py +++ b/tests/render_test/render.py @@ -25,7 +25,7 @@ def handle_packet(self, packet: flat.GamePacket): radius = 0 if len(packet.balls) > 0: - match packet.balls[0].shape.item: + match packet.balls[0].shape: case flat.SphereShape() | flat.CylinderShape() as shape: radius = shape.diameter / 2 case flat.BoxShape() as shape: