From 201bec665a672ace6cf97f194b71cbb83de1b49c Mon Sep 17 00:00:00 2001 From: Virx Date: Sun, 14 Sep 2025 18:58:38 -0400 Subject: [PATCH 1/9] 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 2/9] 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: From dd280aa6a74bafcf1a52911af0396ecd303cf4e3 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 23 Oct 2025 16:08:43 -0400 Subject: [PATCH 3/9] Wait to create the socket, setting ipv4/v6 --- rlbot/interface.py | 24 +++++++++++++++++------- rlbot/version.py | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index bc934de..cdffee7 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -2,8 +2,10 @@ import time from collections.abc import Callable from enum import IntEnum +from ipaddress import ip_address from pathlib import Path -from socket import IPPROTO_TCP, TCP_NODELAY, socket +from socket import AF_INET, AF_INET6, IPPROTO_TCP, TCP_NODELAY +from socket import socket as sock from threading import Thread from rlbot import flat @@ -47,6 +49,8 @@ class SocketRelay: rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] raw_handlers: list[Callable[[flat.CorePacket], None]] = [] + socket: sock | None = None + def __init__( self, agent_id: str, @@ -57,13 +61,9 @@ def __init__( self.connection_timeout = connection_timeout self.logger = get_logger("interface") if logger is None else logger - self.socket = socket() - - # Allow sending packets before getting a response from core - self.socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) - def __del__(self): - self.socket.close() + if self.socket is not None: + self.socket.close() @staticmethod def _int_to_bytes(val: int) -> bytes: @@ -73,6 +73,8 @@ def _read_int(self) -> int: return int.from_bytes(self._read_exact(2), "big") def _read_exact(self, n: int) -> bytes: + assert self.socket is not None, "Socket has not been established" + buff = bytearray(n) view = memoryview(buff) @@ -89,6 +91,7 @@ def read_message(self) -> bytes: return self._read_exact(size) def send_bytes(self, data: bytes): + assert self.socket is not None, "Socket has not been established" assert self.is_connected, "Connection has not been established" size = len(data) @@ -157,7 +160,13 @@ def connect( """ assert not self.is_connected, "Connection has already been established" + # Check if the IP is IPv4 or IPv6 and configure the socket accordingly + family = AF_INET if ip_address(rlbot_server_ip).version == 4 else AF_INET6 + self.socket = sock(family) self.socket.settimeout(self.connection_timeout) + # Allow sending packets before getting a response from core + self.socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1) + try: begin_time = time.time() next_warning = 10 @@ -238,6 +247,7 @@ def handle_incoming_messages(self, blocking: bool = False) -> MsgHandlingResult: Second boolean returns true if there might be more messages to handle without a delay. """ + assert self.socket is not None, "Socket has not been established" assert self.is_connected, "Connection has not been established" try: self.socket.setblocking(blocking) diff --git a/rlbot/version.py b/rlbot/version.py index 57b560f..f8837d2 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.49" +__version__ = "2.0.0-beta.50" From 10fa440f0ba5fa3d8f143dd2666977535ef4b6f1 Mon Sep 17 00:00:00 2001 From: Virx Date: Fri, 2 Jan 2026 20:14:09 -0500 Subject: [PATCH 4/9] Properly update `Renderer.can_render` --- rlbot/interface.py | 6 ++++++ rlbot/managers/bot.py | 5 +---- rlbot/managers/hivemind.py | 2 +- rlbot/managers/rendering.py | 7 +++++-- rlbot/managers/script.py | 5 +---- rlbot/version.py | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index cdffee7..99b5e4e 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -33,6 +33,8 @@ class SocketRelay: from `rlbot.managers`. """ + can_render = False + """Indicates whether RLBotServer has given permission to send rendering commands""" is_connected = False _running = False """Indicates whether a messages are being handled by the `run` loop (potentially in a background thread)""" @@ -296,6 +298,10 @@ def handle_incoming_message(self, incoming_message: bytes) -> MsgHandlingResult: for handler in self.field_info_handlers: handler(field_info) case flat.MatchConfiguration() as match_config: + self.can_render = ( + match_config.enable_rendering == flat.DebugRendering.OnByDefault + ) + for handler in self.match_config_handlers: handler(match_config) case flat.MatchComm() as match_comm: diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 57eae8f..4db1aab 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -120,9 +120,6 @@ 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 - ) self._try_initialize() @@ -234,7 +231,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): By default, this will update `self.renderer.can_render` if appropriate. """ if update.is_bot and update.index == self.index: - self.renderer.can_render = update.status + self._game_interface.can_render = update.status def update_rendering_status( self, diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index 9af78b2..a0154c6 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -232,7 +232,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): By default, this will update `self.renderer.can_render` if appropriate. """ if update.is_bot and update.index in self.indices: - self.renderer.can_render = update.status + self._game_interface.can_render = update.status def update_rendering_status( self, diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 8eef087..aec3978 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -45,8 +45,6 @@ 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() @@ -62,6 +60,11 @@ def __init__(self, game_interface: SocketRelay): self._send_msg: Callable[[flat.RenderGroup | flat.RemoveRenderGroup], None] = ( game_interface.send_msg ) + self._game_interface = game_interface + + @property + def can_render(self) -> bool: + return self._game_interface.can_render def set_resolution(self, screen_width: float, screen_height: float): """ diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index a3f09f7..ef756d2 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -102,9 +102,6 @@ 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 - ) self._try_initialize() @@ -194,7 +191,7 @@ def rendering_status_update(self, update: flat.RenderingStatus): By default, this will update `self.renderer.can_render` if appropriate. """ if not update.is_bot and update.index == self.index: - self.renderer.can_render = update.status + self._game_interface.can_render = update.status def update_rendering_status( self, diff --git a/rlbot/version.py b/rlbot/version.py index f8837d2..281caf5 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.50" +__version__ = "2.0.0-beta.51" From 219839505f44eed1e3fb7d09c486299b67bcb62a Mon Sep 17 00:00:00 2001 From: Virx Date: Fri, 2 Jan 2026 20:14:17 -0500 Subject: [PATCH 5/9] Add new maps --- rlbot/utils/maps.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rlbot/utils/maps.py b/rlbot/utils/maps.py index 98f14b2..9f140d0 100644 --- a/rlbot/utils/maps.py +++ b/rlbot/utils/maps.py @@ -84,6 +84,12 @@ "DFHStadium_Anniversary": "stadium_10a_p", "Holyfield": "Labs_Holyfield_Space_P", "DriftWoods_Night": "woods_night_p", + "BoostfieldMall": "Mall_Day_P", + "ParcDeParis": "Paname_Dusk_P", + "Roadblock": "Labs_Octagon_B2B_02_P", + "Mannfield_Quads": "Labs_4v4_Arena15_EuroStadium_Night_P", + "MidnightMetro_Quads": "Labs_4v4_Arena15_Blackout_P", + "SunsetDunes_Quads": "Labs_4v4_Arena15_Retro_P", } STANDARD_MAPS = [ @@ -140,4 +146,6 @@ "DFHStadium_Anniversary", "DriftWoods_Night", "NeoTokyo_Comic", + "BoostfieldMall", + "ParcDeParis", ] From f1e420a03e60df912b57d277f6f9176efa3a8569 Mon Sep 17 00:00:00 2001 From: Virx Date: Sat, 3 Jan 2026 12:19:55 -0500 Subject: [PATCH 6/9] Handle `flat.RenderingStatus` entirely within `SocketRelay` --- rlbot/interface.py | 4 +--- rlbot/managers/bot.py | 12 ------------ rlbot/managers/hivemind.py | 12 ------------ rlbot/managers/script.py | 12 ------------ 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/rlbot/interface.py b/rlbot/interface.py index 99b5e4e..a4d0d3d 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -48,7 +48,6 @@ class SocketRelay: controllable_team_info_handlers: list[ Callable[[flat.ControllableTeamInfo], None] ] = [] - rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] raw_handlers: list[Callable[[flat.CorePacket], None]] = [] socket: sock | None = None @@ -314,8 +313,7 @@ def handle_incoming_message(self, incoming_message: bytes) -> MsgHandlingResult: 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) + self.can_render = rendering_status.status case _: self.logger.warning( "Received unknown message type: %s", diff --git a/rlbot/managers/bot.py b/rlbot/managers/bot.py index 4db1aab..935eb04 100644 --- a/rlbot/managers/bot.py +++ b/rlbot/managers/bot.py @@ -76,9 +76,6 @@ def __init__(self, default_agent_id: str | None = 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) @@ -224,15 +221,6 @@ 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.renderer.can_render` if appropriate. - """ - if update.is_bot and update.index == self.index: - self._game_interface.can_render = update.status - def update_rendering_status( self, status: bool, diff --git a/rlbot/managers/hivemind.py b/rlbot/managers/hivemind.py index a0154c6..53ca25c 100644 --- a/rlbot/managers/hivemind.py +++ b/rlbot/managers/hivemind.py @@ -78,9 +78,6 @@ def __init__(self, default_agent_id: str | None = 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) @@ -225,15 +222,6 @@ 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.renderer.can_render` if appropriate. - """ - if update.is_bot and update.index in self.indices: - self._game_interface.can_render = update.status - def update_rendering_status( self, status: bool, diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index ef756d2..afb9944 100644 --- a/rlbot/managers/script.py +++ b/rlbot/managers/script.py @@ -58,9 +58,6 @@ def __init__(self, default_agent_id: str | None = 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) @@ -184,15 +181,6 @@ 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.renderer.can_render` if appropriate. - """ - if not update.is_bot and update.index == self.index: - self._game_interface.can_render = update.status - def update_rendering_status( self, status: bool, From 4bd9a37f3583d56586dda1529d2f5447700385e6 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Fri, 9 Jan 2026 17:55:28 +0100 Subject: [PATCH 7/9] Look for RLBotServer in cwd or %localappdata% if not specified --- rlbot/managers/match.py | 73 +++++++++++++++++++++++++++++--------- rlbot/utils/gateway.py | 65 ++++++++++++--------------------- rlbot/utils/os_detector.py | 6 ++-- tests/many_match.py | 4 ++- tests/run_forever.py | 6 ++-- tests/run_match.py | 6 ++-- 6 files changed, 94 insertions(+), 66 deletions(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index ba4ea7b..00d0801 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -1,3 +1,5 @@ +import os +import stat from pathlib import Path from time import sleep @@ -7,7 +9,7 @@ from rlbot.interface import RLBOT_SERVER_IP, RLBOT_SERVER_PORT, SocketRelay from rlbot.utils import fill_desired_game_state, gateway from rlbot.utils.logging import DEFAULT_LOGGER -from rlbot.utils.os_detector import MAIN_EXECUTABLE_NAME +from rlbot.utils.os_detector import RLBOT_SERVER_NAME, OS, CURRENT_OS class MatchManager: @@ -23,11 +25,17 @@ class MatchManager: def __init__( self, - main_executable_path: Path | None = None, - main_executable_name: str = MAIN_EXECUTABLE_NAME, + rlbot_server_path: Path | None = None ): - self.main_executable_path = main_executable_path - self.main_executable_name = main_executable_name + """ + Initialize a MatchManager. + Args: + rlbot_server_path: The path to the RLBotServer executable. The path is used to launch the server + if it is not already running. If set to None, the RLBotServer in the current working directory will + be used, and otherwise the globally installed RLBotServer will be used. + """ + + self.rlbot_server_path = rlbot_server_path self.rlbot_interface: SocketRelay = SocketRelay("") self.rlbot_interface.packet_handlers.append(self._packet_reporter) @@ -41,27 +49,60 @@ def __exit__(self, exc_type, exc_val, exc_tb) -> None: def ensure_server_started(self): """ Ensures that RLBotServer is running, starting it if it is not. + If no path to an RLBotServer executable was passed during initialization of the MatchManager, + the function will use the RLBotServer executable in the current working directory, if any, or + otherwise the global installed RLBotServer will be used, if any. """ - self.rlbot_server_process, self.rlbot_server_port = gateway.find_server_process( - self.main_executable_name - ) + exe_name = self.rlbot_server_path.stem if self.rlbot_server_path is not None and self.rlbot_server_path.is_file() else RLBOT_SERVER_NAME + self.rlbot_server_process, self.rlbot_server_port = gateway.find_server_process(exe_name) if self.rlbot_server_process is not None: - self.logger.info("Already have %s running!", self.main_executable_name) + self.logger.info("%s is already running!", exe_name) return - if self.main_executable_path is None: - self.main_executable_path = Path.cwd() + if self.rlbot_server_path is None: + # Look in cwd or localappdata + path = Path.cwd() / RLBOT_SERVER_NAME + if not path.exists() and CURRENT_OS == OS.WINDOWS: + self.logger.debug(f"Could not find RLBotServer in cwd ('{path.parent}'), trying %localappdata% instead.") + path = Path(os.environ.get("LOCALAPPDATA")) / "RLBot5" / "bin" / RLBOT_SERVER_NAME + if not path.exists(): + raise FileNotFoundError( + "Unable to find RLBotServer in the current working directory " + "or in the default installation location. " + "Is your antivirus messing you up? Check " + "https://github.com/RLBot/RLBot/wiki/Antivirus-Notes." + ) + else: + # User specified path + path = self.rlbot_server_path + if path.exists() and path.is_dir(): + path = path / RLBOT_SERVER_NAME + if not path.exists(): + raise FileNotFoundError(f"Unable to find RLBotServer at the specified path '{path}'.") + + if path is None or not os.access(path, os.F_OK): + raise FileNotFoundError( + f"Unable to find RLBotServer at '{path}'. " + "Is your antivirus messing you up? Check " + "https://github.com/RLBot/RLBot/wiki/Antivirus-Notes." + ) + + if not os.access(path, os.X_OK): + os.chmod(path, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - rlbot_server_process, self.rlbot_server_port = gateway.launch( - self.main_executable_path, - self.main_executable_name, - ) + if not os.access(path, os.X_OK): + raise PermissionError( + "Unable to execute RLBotServer due to file permissions! Is your antivirus messing you up? " + f"Check https://github.com/RLBot/RLBot/wiki/Antivirus-Notes. The exact path is '{path}'" + ) + + rlbot_server_process, self.rlbot_server_port = gateway.launch(path) self.rlbot_server_process = psutil.Process(rlbot_server_process.pid) self.logger.info( "Started %s with process id %s", - self.main_executable_name, + path, self.rlbot_server_process.pid, ) diff --git a/rlbot/utils/gateway.py b/rlbot/utils/gateway.py index 1df3e37..abff987 100644 --- a/rlbot/utils/gateway.py +++ b/rlbot/utils/gateway.py @@ -14,21 +14,23 @@ import shlex -def find_main_executable_path( - main_executable_path: Path, main_executable_name: str -) -> tuple[Path, Path | None]: - main_executable_path = main_executable_path.absolute().resolve() - - # check if the path is directly to the main executable - if main_executable_path.is_file(): - return main_executable_path.parent, main_executable_path - - # search subdirectories for the main executable - for path in main_executable_path.glob(f"**/{main_executable_name}"): +def find_file( + base_dir: Path, file_name: str +) -> Path | None: + """ + Looks for a file called `file_name` in the given `base_dir` directory and its subdirectories. + Returns the path to the file, or None if it was not found. + """ + + base_dir = base_dir.absolute().resolve() + assert base_dir.exists() and base_dir.is_dir(), f"'{base_dir}' is not a directory!" + + # Search subdirectories for the file + for path in base_dir.glob(f"**/{file_name}"): if path.is_file(): - return path.parent, path + return path - return main_executable_path, None + return None def is_port_accessible(port: int): @@ -51,47 +53,26 @@ def find_open_server_port() -> int: ) -def launch( - main_executable_path: Path, main_executable_name: str -) -> tuple[subprocess.Popen[bytes], int]: - directory, path = find_main_executable_path( - main_executable_path, main_executable_name - ) - - if path is None or not os.access(path, os.F_OK): - raise FileNotFoundError( - f"Unable to find RLBotServer at '{main_executable_path}'. " - "Is your antivirus messing you up? Check " - "https://github.com/RLBot/RLBot/wiki/Antivirus-Notes." - ) - - if not os.access(path, os.X_OK): - os.chmod(path, stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - if not os.access(path, os.X_OK): - raise PermissionError( - "Unable to execute RLBotServer due to file permissions! Is your antivirus messing you up? " - f"Check https://github.com/RLBot/RLBot/wiki/Antivirus-Notes. The exact path is {path}" - ) +def launch(exe_path: Path) -> tuple[subprocess.Popen[bytes], int]: port = find_open_server_port() if CURRENT_OS == "Windows": - args = [str(path), str(port)] + args = [str(exe_path), str(port)] else: - args = f"{shlex.quote(path.as_posix())} {port}" # on Unix, when shell=True, args must be a string for flags to reach the executable - DEFAULT_LOGGER.info("Launching RLBotServer with via %s", args) + args = f"{shlex.quote(exe_path.as_posix())} {port}" # on Unix, when shell=True, args must be a string for flags to reach the executable + DEFAULT_LOGGER.info("Launching RLBotServer via %s", args) - return subprocess.Popen(args, shell=True, cwd=directory), port + return subprocess.Popen(args, shell=True, cwd=exe_path.parent), port def find_server_process( - main_executable_name: str, + exe_name: str, ) -> tuple[psutil.Process | None, int]: logger = DEFAULT_LOGGER for proc in psutil.process_iter(): try: - if proc.name() != main_executable_name: + if proc.name() != exe_name: continue args = proc.cmdline() @@ -106,7 +87,7 @@ def find_server_process( except Exception as e: logger.error( "Failed to read the name of a process while hunting for %s: %s", - main_executable_name, + exe_name, e, ) diff --git a/rlbot/utils/os_detector.py b/rlbot/utils/os_detector.py index 5ef4f8d..761878c 100644 --- a/rlbot/utils/os_detector.py +++ b/rlbot/utils/os_detector.py @@ -10,15 +10,15 @@ class OS(IntEnum): match platform.system(): case "Windows": - MAIN_EXECUTABLE_NAME = "RLBotServer.exe" + RLBOT_SERVER_NAME = "RLBotServer.exe" CURRENT_OS = OS.WINDOWS case "Linux": - MAIN_EXECUTABLE_NAME = "RLBotServer" + RLBOT_SERVER_NAME = "RLBotServer" CURRENT_OS = OS.LINUX case _ as unknown_os: from rlbot.utils.logging import get_logger - MAIN_EXECUTABLE_NAME = "" + RLBOT_SERVER_NAME = "" CURRENT_OS = OS.UNKNOWN get_logger("os_detector").warning("Unknown OS: %s", unknown_os) diff --git a/tests/many_match.py b/tests/many_match.py index 8cc093b..1cd8613 100644 --- a/tests/many_match.py +++ b/tests/many_match.py @@ -4,11 +4,13 @@ from rlbot import flat from rlbot.config import load_player_config from rlbot.managers import MatchManager +from rlbot.utils.gateway import find_file +from rlbot.utils.os_detector import RLBOT_SERVER_NAME DIR = Path(__file__).parent BOT_PATH = DIR / "atba/atba.bot.toml" -RLBOT_SERVER_FOLDER = DIR / "../../core/RLBotCS/bin/Release/" +RLBOT_SERVER_FOLDER = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) num_comms = set() diff --git a/tests/run_forever.py b/tests/run_forever.py index 830a85a..cd40f5f 100644 --- a/tests/run_forever.py +++ b/tests/run_forever.py @@ -4,15 +4,17 @@ from rlbot import flat from rlbot.config import load_player_config from rlbot.managers import MatchManager +from rlbot.utils.gateway import find_file from rlbot.utils.maps import GAME_MAP_TO_UPK, STANDARD_MAPS +from rlbot.utils.os_detector import RLBOT_SERVER_NAME DIR = Path(__file__).parent BOT_PATH = DIR / "atba/atba.bot.toml" -RLBOT_SERVER_FOLDER = DIR / "../../core/RLBotCS/bin/Release/" +RLBOT_SERVER_PATH = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) if __name__ == "__main__": - match_manager = MatchManager(RLBOT_SERVER_FOLDER) + match_manager = MatchManager(RLBOT_SERVER_PATH) current_map = -1 diff --git a/tests/run_match.py b/tests/run_match.py index 8da8ebb..c1c3739 100644 --- a/tests/run_match.py +++ b/tests/run_match.py @@ -3,14 +3,16 @@ from rlbot import flat from rlbot.managers import MatchManager +from rlbot.utils.gateway import find_file +from rlbot.utils.os_detector import RLBOT_SERVER_NAME DIR = Path(__file__).parent MATCH_CONFIG_PATH = DIR / "human_vs_atba.toml" -RLBOT_SERVER_FOLDER = DIR / "../../core/RLBotCS/bin/Release/" +RLBOT_SERVER_PATH = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) if __name__ == "__main__": - with MatchManager(RLBOT_SERVER_FOLDER) as man: + with MatchManager(RLBOT_SERVER_PATH) as man: man.start_match(MATCH_CONFIG_PATH) assert man.packet is not None From d0fe1197866f73001c7533d5d743a8e0e6c14a48 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Fri, 9 Jan 2026 17:56:25 +0100 Subject: [PATCH 8/9] Bump version to beta52 --- rlbot/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rlbot/version.py b/rlbot/version.py index 281caf5..a7dae53 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.51" +__version__ = "2.0.0-beta.52" From 561eb83229c78caa1ba34975976f398a4c5c2484 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 10 Jan 2026 09:03:26 +0100 Subject: [PATCH 9/9] Format --- rlbot/managers/match.py | 32 ++++++++++++++++++++++---------- rlbot/utils/gateway.py | 7 +------ tests/many_match.py | 6 ++++-- tests/run_forever.py | 4 +++- tests/run_match.py | 4 +++- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/rlbot/managers/match.py b/rlbot/managers/match.py index 00d0801..de05208 100644 --- a/rlbot/managers/match.py +++ b/rlbot/managers/match.py @@ -9,7 +9,7 @@ from rlbot.interface import RLBOT_SERVER_IP, RLBOT_SERVER_PORT, SocketRelay from rlbot.utils import fill_desired_game_state, gateway from rlbot.utils.logging import DEFAULT_LOGGER -from rlbot.utils.os_detector import RLBOT_SERVER_NAME, OS, CURRENT_OS +from rlbot.utils.os_detector import CURRENT_OS, OS, RLBOT_SERVER_NAME class MatchManager: @@ -23,10 +23,7 @@ class MatchManager: rlbot_server_port = RLBOT_SERVER_PORT initialized = False - def __init__( - self, - rlbot_server_path: Path | None = None - ): + def __init__(self, rlbot_server_path: Path | None = None): """ Initialize a MatchManager. Args: @@ -54,8 +51,14 @@ def ensure_server_started(self): otherwise the global installed RLBotServer will be used, if any. """ - exe_name = self.rlbot_server_path.stem if self.rlbot_server_path is not None and self.rlbot_server_path.is_file() else RLBOT_SERVER_NAME - self.rlbot_server_process, self.rlbot_server_port = gateway.find_server_process(exe_name) + exe_name = ( + self.rlbot_server_path.stem + if self.rlbot_server_path is not None and self.rlbot_server_path.is_file() + else RLBOT_SERVER_NAME + ) + self.rlbot_server_process, self.rlbot_server_port = gateway.find_server_process( + exe_name + ) if self.rlbot_server_process is not None: self.logger.info("%s is already running!", exe_name) return @@ -64,8 +67,15 @@ def ensure_server_started(self): # Look in cwd or localappdata path = Path.cwd() / RLBOT_SERVER_NAME if not path.exists() and CURRENT_OS == OS.WINDOWS: - self.logger.debug(f"Could not find RLBotServer in cwd ('{path.parent}'), trying %localappdata% instead.") - path = Path(os.environ.get("LOCALAPPDATA")) / "RLBot5" / "bin" / RLBOT_SERVER_NAME + self.logger.debug( + f"Could not find RLBotServer in cwd ('{path.parent}'), trying %localappdata% instead." + ) + path = ( + Path(os.environ.get("LOCALAPPDATA")) + / "RLBot5" + / "bin" + / RLBOT_SERVER_NAME + ) if not path.exists(): raise FileNotFoundError( "Unable to find RLBotServer in the current working directory " @@ -79,7 +89,9 @@ def ensure_server_started(self): if path.exists() and path.is_dir(): path = path / RLBOT_SERVER_NAME if not path.exists(): - raise FileNotFoundError(f"Unable to find RLBotServer at the specified path '{path}'.") + raise FileNotFoundError( + f"Unable to find RLBotServer at the specified path '{path}'." + ) if path is None or not os.access(path, os.F_OK): raise FileNotFoundError( diff --git a/rlbot/utils/gateway.py b/rlbot/utils/gateway.py index abff987..0565d32 100644 --- a/rlbot/utils/gateway.py +++ b/rlbot/utils/gateway.py @@ -1,6 +1,4 @@ -import os import socket -import stat import subprocess from pathlib import Path @@ -14,9 +12,7 @@ import shlex -def find_file( - base_dir: Path, file_name: str -) -> Path | None: +def find_file(base_dir: Path, file_name: str) -> Path | None: """ Looks for a file called `file_name` in the given `base_dir` directory and its subdirectories. Returns the path to the file, or None if it was not found. @@ -54,7 +50,6 @@ def find_open_server_port() -> int: def launch(exe_path: Path) -> tuple[subprocess.Popen[bytes], int]: - port = find_open_server_port() if CURRENT_OS == "Windows": diff --git a/tests/many_match.py b/tests/many_match.py index 1cd8613..4942ada 100644 --- a/tests/many_match.py +++ b/tests/many_match.py @@ -10,7 +10,9 @@ DIR = Path(__file__).parent BOT_PATH = DIR / "atba/atba.bot.toml" -RLBOT_SERVER_FOLDER = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) +RLBOT_SERVER_PATH = find_file( + DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME +) num_comms = set() @@ -22,7 +24,7 @@ def handle_match_comm(comm: flat.MatchComm): if __name__ == "__main__": - match_manager = MatchManager(RLBOT_SERVER_FOLDER) + match_manager = MatchManager(RLBOT_SERVER_PATH) match_manager.rlbot_interface.match_comm_handlers.append(handle_match_comm) match_manager.ensure_server_started() match_manager.connect_and_run( diff --git a/tests/run_forever.py b/tests/run_forever.py index cd40f5f..8efe3b1 100644 --- a/tests/run_forever.py +++ b/tests/run_forever.py @@ -11,7 +11,9 @@ DIR = Path(__file__).parent BOT_PATH = DIR / "atba/atba.bot.toml" -RLBOT_SERVER_PATH = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) +RLBOT_SERVER_PATH = find_file( + DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME +) if __name__ == "__main__": match_manager = MatchManager(RLBOT_SERVER_PATH) diff --git a/tests/run_match.py b/tests/run_match.py index c1c3739..bf8cc96 100644 --- a/tests/run_match.py +++ b/tests/run_match.py @@ -9,7 +9,9 @@ DIR = Path(__file__).parent MATCH_CONFIG_PATH = DIR / "human_vs_atba.toml" -RLBOT_SERVER_PATH = find_file(DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME) +RLBOT_SERVER_PATH = find_file( + DIR / "../../core/RLBotCS/bin/Release/", RLBOT_SERVER_NAME +) if __name__ == "__main__": with MatchManager(RLBOT_SERVER_PATH) as man: