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/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 408021b..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.16.0", + "rlbot_flatbuffers~=0.18.2", "psutil==7.*", ] readme = "README.md" @@ -23,3 +23,12 @@ Repository = "https://github.com/RLBot/python-interface" [tool.setuptools.dynamic] version = {attr = "rlbot.version.__version__"} + +[dependency-groups] +dev = [ + "ruff>=0.13.0", + "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/config.py b/rlbot/config.py index 20f93a0..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 @@ -94,30 +93,30 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: match variant: case "rlbot": - variety, use_config = flat.CustomBot(), True + abs_config_path = (config_path.parent / car_config).resolve() + players.append( + load_player_config(abs_config_path, team, name, loadout_file) + ) case "psyonix": - variety, use_config = flat.Psyonix(skill), True + abs_config_path = ( + (config_path.parent / car_config).resolve() if car_config else None + ) + players.append( + load_psyonix_config( + team, + skill, + name, + loadout_file, + abs_config_path, + ) + ) case "human": - variety, use_config = flat.Human(), False - case "partymember": - logger.warning("PartyMember player type is not supported yet.") - variety, use_config = flat.PartyMember(), False + players.append(get_human(team)) case t: raise ConfigParsingException( f"Invalid player type {repr(t)} for player {len(players)}." ) - if use_config and car_config: - abs_config_path = (config_path.parent / car_config).resolve() - players.append( - load_player_config(abs_config_path, variety, team, name, loadout_file) # type: ignore - ) - else: - loadout = load_player_loadout(loadout_file, team) if loadout_file else None - players.append( - flat.PlayerConfiguration(variety, name, team, loadout=loadout) - ) - scripts: list[flat.ScriptConfiguration] = [] for script_table in config.get("scripts", []): if script_config := __str(script_table, "config_file"): @@ -155,6 +154,17 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: audio=__enum(mutator_table, "audio", flat.AudioMutator), ) + try: + enable_rendering = ( + flat.DebugRendering.OnByDefault + if __bool(match_table, "enable_rendering") + else flat.DebugRendering.OffByDefault + ) + except ConfigParsingException: + enable_rendering = __enum( + match_table, "enable_rendering", flat.DebugRendering.AlwaysOff + ) + return flat.MatchConfiguration( launcher=__enum(rlbot_table, "launcher", flat.Launcher), launcher_arg=__str(rlbot_table, "launcher_arg"), @@ -170,7 +180,7 @@ def load_match_config(config_path: Path | str) -> flat.MatchConfiguration: existing_match_behavior=__enum( match_table, "existing_match_behavior", flat.ExistingMatchBehavior ), - enable_rendering=__bool(match_table, "enable_rendering"), + enable_rendering=enable_rendering, enable_state_setting=__bool(match_table, "enable_state_setting"), freeplay=__bool(match_table, "freeplay"), ) @@ -218,7 +228,6 @@ def load_player_loadout(path: Path | str, team: int) -> flat.PlayerLoadout: def load_player_config( path: Path | str, - type: flat.CustomBot | flat.Psyonix, team: int, name_override: str | None = None, loadout_override: Path | str | None = None, @@ -252,18 +261,74 @@ 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"), ) +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. diff --git a/rlbot/interface.py b/rlbot/interface.py index feea530..bc934de 100644 --- a/rlbot/interface.py +++ b/rlbot/interface.py @@ -1,12 +1,10 @@ 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 from threading import Thread -from typing import Optional from rlbot import flat from rlbot.utils.logging import get_logger @@ -18,36 +16,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 +34,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,13 +44,14 @@ class SocketRelay: controllable_team_info_handlers: list[ Callable[[flat.ControllableTeamInfo], None] ] = [] - raw_handlers: list[Callable[[SocketMessage], None]] = [] + rendering_status_handlers: list[Callable[[flat.RenderingStatus], None]] = [] + raw_handlers: list[Callable[[flat.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 @@ -116,50 +84,43 @@ def _read_exact(self, n: int) -> bytes: pos += cr return bytes(buff) - def read_message(self) -> SocketMessage: - type_int = self._read_int() + def read_message(self) -> bytes: size = self._read_int() - data = self._read_exact(size) - return SocketMessage(SocketDataType(type_int), data) + return self._read_exact(size) - def send_bytes(self, data: bytes, data_type: SocketDataType): + def send_bytes(self, data: bytes): assert self.is_connected, "Connection has not been established" size = len(data) if size > MAX_SIZE_2_BYTES: - self.logger.error( - "Couldn't send %s message because it was too big!", data_type.name - ) + self.logger.error("Couldn't send message because it was too big!") return - message = self._int_to_bytes(data_type) + self._int_to_bytes(size) + data + message = self._int_to_bytes(size) + data self.socket.sendall(message) - def send_init_complete(self): - self.send_bytes(bytes(), SocketDataType.INIT_COMPLETE) - - def send_set_loadout(self, set_loadout: flat.SetLoadout): - self.send_bytes(set_loadout.pack(), SocketDataType.SET_LOADOUT) - - def send_match_comm(self, match_comm: flat.MatchComm): - self.send_bytes(match_comm.pack(), SocketDataType.MATCH_COMMUNICATION) - - def send_player_input(self, player_input: flat.PlayerInput): - self.send_bytes(player_input.pack(), SocketDataType.PLAYER_INPUT) - - def send_game_state(self, game_state: flat.DesiredGameState): - self.send_bytes(game_state.pack(), SocketDataType.DESIRED_GAME_STATE) - - def send_render_group(self, render_group: flat.RenderGroup): - self.send_bytes(render_group.pack(), SocketDataType.RENDER_GROUP) - - def remove_render_group(self, group_id: int): - flatbuffer = flat.RemoveRenderGroup(group_id).pack() - self.send_bytes(flatbuffer, SocketDataType.REMOVE_RENDER_GROUP) + def send_msg( + self, + msg: ( + flat.DisconnectSignal + | flat.StartCommand + | flat.MatchConfiguration + | flat.PlayerInput + | flat.DesiredGameState + | flat.RenderGroup + | flat.RemoveRenderGroup + | flat.MatchComm + | flat.ConnectionSettings + | flat.StopCommand + | flat.SetLoadout + | flat.InitComplete + | flat.RenderingStatus + ), + ): + self.send_bytes(flat.InterfacePacket(msg).pack()) def stop_match(self, shutdown_server: bool = False): - flatbuffer = flat.StopCommand(shutdown_server).pack() - self.send_bytes(flatbuffer, SocketDataType.STOP_COMMAND) + self.send_msg(flat.StopCommand(shutdown_server)) def start_match(self, match_config: Path | flat.MatchConfiguration): self.logger.info("Python interface is attempting to start match...") @@ -167,17 +128,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" + "Expected MatchConfiguration or path to match config toml file" ) - self.send_bytes(flatbuffer, flat_type) + self.send_msg(flatbuffer) def connect( self, @@ -242,13 +201,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): """ @@ -282,23 +242,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 of type %s (%s bytes): %s", - incoming_message.type.name, - len(incoming_message.data), - e, - ) - return MsgHandlingResult.TERMINATED - except Exception as e: - self.logger.error( - "Unexpected error while handling message of type %s: %s", - incoming_message.type.name, - e, - ) - return MsgHandlingResult.TERMINATED except BlockingIOError: # No incoming messages and blocking==False return MsgHandlingResult.NO_INCOMING_MSGS @@ -306,56 +249,62 @@ 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: + 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) + for raw_handler in self.raw_handlers: - raw_handler(incoming_message) + raw_handler(flatbuffer) - match incoming_message.type: - case SocketDataType.NONE: + match flatbuffer.message: + case flat.DisconnectSignal(): return MsgHandlingResult.TERMINATED - case SocketDataType.GAME_PACKET: - if len(self.packet_handlers) > 0: - packet = flat.GamePacket.unpack(incoming_message.data) - for handler in self.packet_handlers: - handler(packet) - case SocketDataType.FIELD_INFO: - if len(self.field_info_handlers) > 0: - field_info = flat.FieldInfo.unpack(incoming_message.data) - for handler in self.field_info_handlers: - handler(field_info) - case SocketDataType.MATCH_CONFIGURATION: - if len(self.match_config_handlers) > 0: - match_settings = flat.MatchConfiguration.unpack( - incoming_message.data - ) - for handler in self.match_config_handlers: - handler(match_settings) - case SocketDataType.MATCH_COMMUNICATION: - if len(self.match_comm_handlers) > 0: - match_comm = flat.MatchComm.unpack(incoming_message.data) - for handler in self.match_comm_handlers: - handler(match_comm) - case SocketDataType.BALL_PREDICTION: - if len(self.ball_prediction_handlers) > 0: - self._ball_pred.unpack_with(incoming_message.data) - for handler in self.ball_prediction_handlers: - handler(self._ball_pred) - case SocketDataType.CONTROLLABLE_TEAM_INFO: - if len(self.controllable_team_info_handlers) > 0: - player_mappings = flat.ControllableTeamInfo.unpack( - incoming_message.data - ) - for handler in self.controllable_team_info_handlers: - handler(player_mappings) + case flat.GamePacket() as packet: + for handler in self.packet_handlers: + handler(packet) + case flat.FieldInfo() as field_info: + for handler in self.field_info_handlers: + handler(field_info) + case flat.MatchConfiguration() as match_config: + for handler in self.match_config_handlers: + handler(match_config) + case flat.MatchComm() as match_comm: + for handler in self.match_comm_handlers: + handler(match_comm) + case flat.BallPrediction() as ball_prediction: + for handler in self.ball_prediction_handlers: + handler(ball_prediction) + case flat.ControllableTeamInfo() as controllable_team_info: + for handler in self.controllable_team_info_handlers: + handler(controllable_team_info) + case flat.RenderingStatus() as rendering_status: + for handler in self.rendering_status_handlers: + handler(rendering_status) case _: - pass + self.logger.warning( + "Received unknown message type: %s", + type(flatbuffer.item).__name__, + ) return MsgHandlingResult.MORE_MSGS_QUEUED @@ -364,7 +313,7 @@ def disconnect(self): self.logger.warning("Asked to disconnect but was already disconnected.") return - self.send_bytes(bytes([1]), SocketDataType.NONE) + self.send_msg(flat.DisconnectSignal()) timeout = 5.0 while self._running and timeout > 0: time.sleep(0.1) @@ -373,7 +322,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/managers/bot.py b/rlbot/managers/bot.py index 9320967..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 ( @@ -28,7 +27,7 @@ class Bot: team: int = -1 index: int = -1 name: str = "" - spawn_id: int = 0 + player_id: int = 0 match_config = flat.MatchConfiguration() """ @@ -50,10 +49,10 @@ class Bot: _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: @@ -77,6 +76,9 @@ def __init__(self, default_agent_id: Optional[str] = None): self._handle_controllable_team_info ) self._game_interface.packet_handlers.append(self._handle_packet) + self._game_interface.rendering_status_handlers.append( + self.rendering_status_update + ) self.renderer = Renderer(self._game_interface) @@ -90,12 +92,18 @@ def _try_initialize(self): # Not ready to initialize return - # Search match settings for our name for player in self.match_config.player_configurations: - if player.spawn_id == self.spawn_id: - self.name = player.name - self.logger = get_logger(self.name) - break + match player.variety: + 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() @@ -107,11 +115,15 @@ def _try_initialize(self): exit() self._initialized_bot = True - self._game_interface.send_init_complete() + self._game_interface.send_msg(flat.InitComplete()) def _handle_match_config(self, match_config: flat.MatchConfiguration): self.match_config = match_config self._has_match_settings = True + self.can_render = ( + match_config.enable_rendering == flat.DebugRendering.OnByDefault + ) + self._try_initialize() def _handle_field_info(self, field_info: flat.FieldInfo): @@ -124,7 +136,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 @@ -151,10 +163,12 @@ 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) - self._game_interface.send_player_input(player_input) + self._game_interface.send_msg(player_input) def _run(self): running = True @@ -213,22 +227,50 @@ 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.renderer.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: int | None = 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 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. + - `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, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ 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( - 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. @@ -237,7 +279,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, @@ -251,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] = [], ): """ @@ -261,22 +303,20 @@ 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): + 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`. 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): """ - 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 c9ed52a..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 ( @@ -30,7 +29,7 @@ class Hivemind: team: int = -1 indices: list[int] = [] names: list[str] = [] - spawn_ids: list[int] = [] + player_ids: list[int] = [] match_config = flat.MatchConfiguration() """ @@ -52,10 +51,10 @@ class Hivemind: _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: @@ -79,6 +78,9 @@ def __init__(self, default_agent_id: Optional[str] = None): self._handle_controllable_team_info ) self._game_interface.packet_handlers.append(self._handle_packet) + self._game_interface.rendering_status_handlers.append( + self.rendering_status_update + ) self.renderer = Renderer(self._game_interface) @@ -91,13 +93,21 @@ def _try_initialize(self): ): return - # Search match settings for our spawn ids - for spawn_id in self.spawn_ids: + # Search match configuration for our spawn ids + for player_id in self.player_ids: for player in self.match_config.player_configurations: - if player.spawn_id == spawn_id: - self.names.append(player.name) - self.loggers.append(get_logger(player.name)) - break + match player.variety: + 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() @@ -111,11 +121,12 @@ def _try_initialize(self): exit() self._initialized_bot = True - self._game_interface.send_init_complete() + self._game_interface.send_msg(flat.InitComplete()) def _handle_match_config(self, match_config: flat.MatchConfiguration): self.match_config = match_config self._has_match_settings = True + self._try_initialize() def _handle_field_info(self, field_info: flat.FieldInfo): @@ -128,7 +139,7 @@ def _handle_controllable_team_info( ): self.team = player_mappings.team for controllable in player_mappings.controllables: - self.spawn_ids.append(controllable.spawn_id) + self.player_ids.append(controllable.identifier) self.indices.append(controllable.index) self._has_player_mapping = True @@ -166,7 +177,7 @@ def _packet_processor(self, packet: flat.GamePacket): ", ".join(map(str, self.indices)), ) player_input = flat.PlayerInput(index, controller) - self._game_interface.send_player_input(player_input) + self._game_interface.send_msg(player_input) def _run(self): running = True @@ -214,6 +225,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.renderer.can_render` if appropriate. + """ + if update.is_bot and update.index in self.indices: + self.renderer.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: int | None = 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 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. + - `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, @@ -228,20 +269,20 @@ def handle_match_comm( index: int, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ 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( self, index: int, content: bytes, - display: Optional[str] = None, + display: str | None = None, team_only: bool = False, ): """ @@ -251,7 +292,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, @@ -265,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] = [], ): """ @@ -275,7 +316,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,11 +325,11 @@ 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): """ - 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 af2cea5..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 @@ -33,6 +32,12 @@ def __init__( self.rlbot_interface: SocketRelay = SocketRelay("") self.rlbot_interface.packet_handlers.append(self._packet_reporter) + def __enter__(self) -> "MatchManager": + 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. @@ -70,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. @@ -88,6 +93,42 @@ def connect( rlbot_server_port=rlbot_server_port or self.rlbot_server_port, ) + 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: int | None = 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 { flat.MatchPhase.Inactive, @@ -102,7 +143,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. """ @@ -111,17 +152,17 @@ 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.run(background_thread=True) 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: @@ -130,7 +171,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() @@ -142,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] = [], ): """ @@ -152,7 +193,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): """ @@ -161,8 +202,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]: diff --git a/rlbot/managers/rendering.py b/rlbot/managers/rendering.py index 7dcee97..8eef087 100644 --- a/rlbot/managers/rendering.py +++ b/rlbot/managers/rendering.py @@ -1,5 +1,6 @@ +import math from collections.abc import Callable, Sequence -from typing import Optional +from contextlib import contextmanager from rlbot import flat from rlbot.interface import SocketRelay @@ -44,25 +45,67 @@ 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() - _group_id: Optional[int] = None + _group_id: int | None = None _current_renders: list[flat.RenderMessage] = [] + _default_color = white + + _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 + self._send_msg: Callable[[flat.RenderGroup | flat.RemoveRenderGroup], None] = ( + game_interface.send_msg ) - self._remove_render_group: Callable[[int], None] = ( - 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 + + 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) + @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: """ @@ -80,6 +123,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. @@ -90,6 +157,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) @@ -106,7 +174,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 @@ -116,7 +184,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): @@ -125,7 +193,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): @@ -158,29 +226,33 @@ 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, @@ -194,7 +266,7 @@ def draw_string_3d( text, _get_anchor(anchor), scale, - foreground, + foreground or self._default_color, background, h_align, v_align, @@ -207,7 +279,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, @@ -215,15 +287,16 @@ 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, + foreground or self._default_color, background, h_align, v_align, @@ -236,22 +309,23 @@ 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, ): """ 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, - color, + x * self._screen_width_factor, + y * self._screen_height_factor, + width * self._screen_width_factor, + height * self._screen_height_factor, + color or self._default_color, h_align, v_align, ) @@ -262,21 +336,22 @@ 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, ): """ 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, - color, + width * self._screen_width_factor, + height * self._screen_height_factor, + color or self._default_color, h_align, v_align, ) diff --git a/rlbot/managers/script.py b/rlbot/managers/script.py index 613b242..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: @@ -59,6 +58,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) @@ -72,6 +74,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: @@ -84,22 +97,14 @@ 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 - - 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( - "Script with agent id '%s' did not find itself in the match settings", - self._game_interface.agent_id, - ) + self._has_match_settings = True + self.can_render = ( + match_config.enable_rendering == flat.DebugRendering.OnByDefault + ) self._try_initialize() @@ -123,6 +128,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): @@ -180,22 +187,50 @@ 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.renderer.can_render = update.status + + def update_rendering_status( + self, + status: bool, + index: int | None = 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 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. + - `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, team: int, content: bytes, - display: Optional[str], + display: str | None, team_only: bool, ): """ 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( - 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. @@ -204,7 +239,7 @@ def send_match_comm( - `display`: The message to be displayed in the game in "quick chat", or `None` to display nothing. - `team_only`: If True, only your team will receive the message. For scripts, this means other scripts. """ - self._game_interface.send_match_comm( + self._game_interface.send_msg( flat.MatchComm( self.index, 2, @@ -218,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] = [], ): """ @@ -228,7 +263,7 @@ def set_game_state( """ game_state = fill_desired_game_state(balls, cars, match_info, commands) - self._game_interface.send_game_state(game_state) + self._game_interface.send_msg(game_state) def set_loadout(self, loadout: flat.PlayerLoadout, index: int): """ @@ -236,11 +271,11 @@ 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): """ - 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/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 502cbce..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 @@ -14,9 +13,10 @@ if CURRENT_OS != "Windows": import shlex + 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 @@ -87,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/utils/maps.py b/rlbot/utils/maps.py index 56a544a..98f14b2 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", @@ -82,6 +81,9 @@ "DriftWoods": "Woods_P", "Neotokyo_Arcade": "NeoTokyo_Arcade_P", "FuturaGarden": "UF_Day_P", + "DFHStadium_Anniversary": "stadium_10a_p", + "Holyfield": "Labs_Holyfield_Space_P", + "DriftWoods_Night": "woods_night_p", } STANDARD_MAPS = [ @@ -135,4 +137,7 @@ "DriftWoods", "Neotokyo_Arcade", "FuturaGarden", + "DFHStadium_Anniversary", + "DriftWoods_Night", + "NeoTokyo_Comic", ] diff --git a/rlbot/version.py b/rlbot/version.py index f8b01fd..57b560f 100644 --- a/rlbot/version.py +++ b/rlbot/version.py @@ -1 +1 @@ -__version__ = "2.0.0-beta.41" +__version__ = "2.0.0-beta.49" 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/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 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/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/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/many_match.py b/tests/many_match.py new file mode 100644 index 0000000..8cc093b --- /dev/null +++ b/tests/many_match.py @@ -0,0 +1,85 @@ +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_and_run( + wants_match_communications=True, + wants_ball_predictions=False, + close_between_matches=False, + background_thread=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() 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/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/bot.py b/tests/nexto/bot.py index 2bbcc16..a57ac28 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 @@ -372,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/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 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..5da1149 100644 --- a/tests/psy.toml +++ b/tests/psy.toml @@ -4,12 +4,11 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" [[cars]] team = 0 -config_file = "psy/bot.toml" type = "Psyonix" skill = "Beginner" diff --git a/tests/render_test.toml b/tests/render_test.toml index 79e0fd1..c3421a5 100644 --- a/tests/render_test.toml +++ b/tests/render_test.toml @@ -3,9 +3,9 @@ launcher = "Steam" [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" -enable_rendering = true +enable_rendering = "OffByDefault" [[cars]] type = "Human" diff --git a/tests/render_test/render.py b/tests/render_test/render.py index ddc8916..3582d67 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 @@ -22,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: @@ -30,6 +33,17 @@ def handle_packet(self, packet: flat.GamePacket): self.do_render(radius) + 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() @@ -67,9 +81,7 @@ def do_render(self, radius: float): CarAnchor(0, Vector3(200, 0, 0)), 0.02, 0.02, self.renderer.blue ) - self.renderer.draw_rect_2d( - 0.75, 0.75, 0.1, 0.1, Color(150, 30, 100), centered=False - ) + self.renderer.draw_rect_2d(0.75, 0.75, 0.1, 0.1, Color(150, 30, 100)) self.renderer.draw_rect_2d(0.75, 0.75, 0.1, 0.1, self.renderer.black) for hkey, h in { "left": flat.TextHAlign.Left, diff --git a/tests/rlbot.toml b/tests/rlbot.toml index 437453f..8436ce4 100644 --- a/tests/rlbot.toml +++ b/tests/rlbot.toml @@ -4,7 +4,7 @@ launcher = "Steam" auto_start_agents = true [match] -game_mode = "Soccer" +game_mode = "Soccar" game_map_upk = "Stadium_P" skip_replays = false start_without_countdown = false diff --git a/tests/run_forever.py b/tests/run_forever.py index f5b5661..830a85a 100644 --- a/tests/run_forever.py +++ b/tests/run_forever.py @@ -13,17 +13,16 @@ if __name__ == "__main__": match_manager = MatchManager(RLBOT_SERVER_FOLDER) - match_manager.ensure_server_started() current_map = -1 - blue_bot = load_player_config(BOT_PATH, flat.CustomBot(), 0) - orange_bot = load_player_config(BOT_PATH, flat.CustomBot(), 1) + blue_bot = load_player_config(BOT_PATH, 0) + orange_bot = load_player_config(BOT_PATH, 1) match_settings = flat.MatchConfiguration( launcher=flat.Launcher.Steam, auto_start_agents=True, - game_mode=flat.GameMode.Soccer, + game_mode=flat.GameMode.Soccar, enable_state_setting=True, existing_match_behavior=flat.ExistingMatchBehavior.Restart, skip_replays=True, diff --git a/tests/run_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..8353683 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 / "rlbot.toml" RLBOT_SERVER_FOLDER = DIR / "../" if __name__ == "__main__": @@ -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() 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() 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"