Skip to content

Migration

VirxEC edited this page Nov 4, 2024 · 13 revisions

RLBot v4 to v5

Legend: ✨=new, πŸ› =reworked/changed, β›”=removed

πŸ›  .cfg -> .toml

Instead of using .cfg files with a format that's only parsed properly by Python, RLBot v5 now uses the standard .toml format. This changes makes it easy to grab your language's off-the-shelf TOML parser to read the configuration file and get the same data that RLBot would.

To see how TOML works, check out the TOML spec.

Bot & script config files

There's a new naming convention for the toml files:

  • Config files that define bots MUST either be named bot.toml or end in .bot.toml. Here are examples of valid names:
    • bot.toml
    • Necto.bot.toml
    • atba.bot.toml
  • Config files that define scripts MUST either be named script.toml or end in .script.toml. Here are examples of valid names:
    • script.toml
    • tron.script.toml
    • SCB.script.toml

It should also be noted that whatever prefixes the .bot.toml/.script.toml file name will not be used by anything in RLBot.

Example of a bot.toml file that runs in a virtual environment:

[settings]
agent_id = "rolv-soren/necto"
name = "Necto"
loadout_file = "loadout.toml"
run_command = ".\\venv\\Scripts\\python bot.py"
run_command_linux = "./venv/bin/python bot.py"
logo_file = "necto_logo.png"

[details]
description = "Necto is the official RLGym community bot, trained using PPO with workers run by people all around the world."
fun_fact = "Necto uses an attention mechanism, commonly used for text understanding, to support any number of players"
source_link = "https://github.com/Rolv-Arild/Necto"
developer = "Rolv, Soren, and several contributors"
language = "rlgym"
tags = ["1v1", "teamplay"]

Example of a script.toml file that runs in the global Python environment:

[settings]
agent_id = "rlbot-community/script-example"
name = "Script Example"
run_command = "python render.py"
run_command_linux = "python3 render.py"
logo_file = "logo.png"

[details]
description = "Script example"
fun_fact = "This is just an example TOML!"
source_link = ""
developer = "The RLBot community"
language = "python"
tags = []
  • [settings] - Used by both RLBot & the GUI
    • agent_id - The static, unique id that is associated with this bot.
      • Preferred format is "author/bot-name"
    • name - The name of the bot/script.
    • loadout_file - The path to the loadout file for the bot.
    • run_command - The command to run the bot/script on Windows.
    • run_command_linux - The command to run the bot/script on Linux.
    • logo_file - The path to the logo file for the bot/script.
  • [details] - Used only by the GUI
    • description - A description of the bot/script.
    • fun_fact - A fun fact about the bot/script.
    • source_link - A link to the source code of the bot/script.
    • developer - The developer(s) of the bot/script.
    • language - The language the bot/script is written in.
    • tags - A list of tags that describe the bot/script. Possible tags:
      • 1v1
      • teamplay
      • goalie - Only add this tag if your bot only plays as a goalie; this directly contrasts with the teamplay tag!
      • hoops
      • dropshot
      • snow-day
      • spike-rush
      • heatseaker
      • memebot

Loadout config files

There's fewer differences between the .toml and .cfg files for loadouts - all of they keys have stayed the same, only the headers are differently named.

  • [Bot Loadout] -> [blue_loadout]
  • [Bot Loadout Orange] -> [orange_loadout]
  • [Bot Paint Blue] -> [blue_loadout.paint]
  • [Bot Paint Orange] -> [orange_loadout.paint]

Example of a loadout.toml file:

[blue_loadout]
# Primary Color selection
team_color_id = 29
# Secondary Color selection
custom_color_id = 0
# Car type (Octane, Merc, etc)
car_id = 23
# Type of decal
decal_id = 6083
# Wheel selection
wheels_id = 1580
# Boost selection
boost_id = 35
# Antenna Selection
antenna_id = 0
# Hat Selection
hat_id = 0
# Paint Type (for first color)
paint_finish_id = 0
# Paint Type (for secondary color)
custom_finish_id = 0
# Engine Audio Selection
engine_audio_id = 6919
# Car trail Selection
trails_id = 3220
# Goal Explosion Selection
goal_explosion_id = 4118

[orange_loadout]
team_color_id = 69
custom_color_id = 0
car_id = 23
decal_id = 6083
wheels_id = 1580
boost_id = 35
antenna_id = 0
hat_id = 0
paint_finish_id = 1681
custom_finish_id = 1681
engine_audio_id = 5635
trails_id = 3220
goal_explosion_id = 4118

[blue_loadout.paint]
car_paint_id = 12
decal_paint_id = 12
wheels_paint_id = 12
boost_paint_id = 12
antenna_paint_id = 0
hat_paint_id = 0
trails_paint_id = 12
goal_explosion_paint_id = 12

[orange_loadout.paint]
car_paint_id = 12
decal_paint_id = 12
wheels_paint_id = 12
boost_paint_id = 12
antenna_paint_id = 0
hat_paint_id = 0
trails_paint_id = 12
goal_explosion_paint_id = 12

Automated conversion from v4 cfg to v5 toml

Alternatively, to automate the conversion between the two formats, you can use the following Python script:

from configparser import RawConfigParser
from pathlib import Path
from typing import Any

import toml


def cfg_to_dict(path: Path) -> dict[str, dict[str, str]]:
    config = RawConfigParser()
    config.read(path)

    dict_config: dict[str, dict[str, str]] = {}
    for section in config.sections():
        dict_config[section] = {}
        for key, value in config.items(section):
            dict_config[section][key] = value

    return dict_config


def write_to_toml(path: Path, config: dict[str, dict[str, Any]]):
    path.parent.mkdir(parents=True, exist_ok=True)
    with open(path, "w") as f:
        toml.dump(config, f)


class Loadout:
    def __init__(self, path: Path):
        self.cfg_dict = cfg_to_dict(path)

    def _convert_dict_section(self, old_name: str) -> dict[str, int]:
        return {
            key: int(value)
            for key, value in self.cfg_dict[old_name].items()
            if key not in {"primary_color_lookup", "secondary_color_lookup"}
        }

    def convert_to_toml(self) -> dict[str, dict[str, Any]]:
        toml_dict: dict[str, dict[str, Any]] = {}

        for section in self.cfg_dict.keys():
            if section == "Bot Loadout":
                toml_dict["blue_loadout"] = self._convert_dict_section(section)
            elif section == "Bot Loadout Orange":
                toml_dict["orange_loadout"] = self._convert_dict_section(section)
            elif section == "Bot Paint Blue":
                toml_dict["blue_loadout"]["paint"] = self._convert_dict_section(section)
            elif section == "Bot Paint Orange":
                toml_dict["orange_loadout"]["paint"] = self._convert_dict_section(
                    section
                )

        return toml_dict

    def write_to_toml(self, path: Path):
        write_to_toml(path, self.convert_to_toml())


class Bot:
    def __init__(self, path: Path):
        self.parent_path = path.parent
        self.cfg_dict = cfg_to_dict(path)

    def _convert_settings(self) -> dict[str, str]:
        settings: dict[str, str] = {}

        use_virtual_environment = False
        python_file = ""

        for key, value in self.cfg_dict["Locations"].items():
            if key == "looks_config":
                key = "loadout_file"
                value = value.replace(".cfg", ".toml")
            elif key == "use_virtual_environment":
                use_virtual_environment = True
                continue
            elif key == "maximum_tick_rate_preference":
                assert int(value) == 120, "Only 120 tick rate is supported"
                continue
            elif key == "python_file":
                python_file = value
                continue
            elif key in {
                "requirements_file",
                "supports_early_start",
                "supports_standalone",
            }:
                continue
            settings[key] = value

        if use_virtual_environment:
            settings["run_command"] = ".\\venv\\Scripts\\python " + python_file
            settings["run_command_linux"] = "./venv/bin/python " + python_file
        else:
            settings["run_command"] = "python " + python_file
            settings["run_command_linux"] = "python3 " + python_file

        return settings

    def _convert_details(self) -> dict[str, str | list[str]]:
        details: dict[str, str | list[str]] = {}

        for key, value in self.cfg_dict["Details"].items():
            if key == "tags":
                details[key] = [tag.strip() for tag in value.split(",")]
                continue
            details[key] = value

        return details

    def convert_to_toml(self) -> dict[str, dict[str, Any]]:
        toml_dict: 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"]["author"]}/{toml_dict["settings"]["name"]}"

        return toml_dict

    def write_to_toml(self, bot_path: Path):
        config = self.convert_to_toml()
        write_to_toml(bot_path, config)

        old_loadout = config["settings"]["loadout_file"]
        Loadout(self.parent_path / old_loadout.replace(".toml", ".cfg")).write_to_toml(
            bot_path.parent / old_loadout
        )

This script can then be used like this:

from pathlib import Path

bot = Bot(Path("necto.cfg"))
bot.write_to_toml("necto.bot.toml")

πŸ›  main.py

In RLBot v5, every bot is now more similar to v4's StandaloneBot. This means that your bot MUST run it's run method at the bottom of the file, or else IT WILL NOT RUN.

Here is a minimal example of a bot in RLBot v5:

from rlbot.flat import *
from rlbot.managers import Bot


class MyBot(Bot):
    def initialize(self):
        """
        Called for all heaver initialization that needs to happen.
        Field info and match settings are fully loaded at this point, and won't return garbage data.

        `self.index` and `self.team` are also available.
        """

        # A little known feature of even v4,
        # bots have a `logger` attribute that can be used to log messages.
        self.logger.info("Setting default values that require more match info!")

    def get_output(self, packet: GameTickPacket) -> ControllerState:
        """
        Where all the logic of your bot gets its input and returns its output.
        """

        return ControllerState()


if __name__ == "__main__":
    # This is the entry point for the bot.
    MyBot().run()

πŸ›  BaseAgent/StandaloneBot -> Bot

There have been several changes to how you access data like field info, ball prediction, etc.

  • πŸ›  def __init__(self, name: str, team: int, index: int) -> def __init__(self)

    • These variables are no longer known at this time.

    • NOTE: This method is similar-ish to the "early start" of v4, but you probably shouldn't use it. If you want to define default variables, the following is more Pythonic:

      class MyBot(Bot):
          my_variable = None
          counter = 1

      These variables can then be accessed like normal via self.my_variable and self.counter.

      If you need to perform more complex initialization, it's recommended you use initialize instead.

  • πŸ›  initialize - This method blocks the match from starting until it's finished running. All heavy initialization should be done here, as the purpose of the match not starting is to ensure that all bots had time to boot up and are ready to go.

  • πŸ›  self.get_field_info() -> self.field_info - Example:

    from rlbot.flat import *
    from rlbot.managers import Bot
    
    
    class MyBot(Bot):
        def initialize(self):
            # Log a yellow warning if the number of boost pads is not 34 pads in a standard map!
            if len(self.field_info.boost_pads) != 34:
                self.logger.warning(
                    "The standard number of boost pads is 34, but this map has %d:%s",
                    len(self.field_info.boost_pads),
                    "\n".join(map(str, self.field_info.boost_pads)),
                )
    
        def get_output(self, packet: GameTickPacket) -> ControllerState:
            return ControllerState()
    
    
    if __name__ == "__main__":
        MyBot().run()
  • πŸ›  self.get_match_settings() -> self.match_settings - Example:

    from rlbot.flat import *
    from rlbot.managers import Bot
    
    
    class MyBot(Bot):
        gravity = -650
    
        def initialize(self):
            match self.match_settings.mutator_settings.gravity_option:
                case GravityOption.Low:
                    self.gravity /= 2
                case GravityOption.High:
                    self.gravity *= 1.75
                case GravityOption.Super_High:
                    self.gravity *= 5
                case GravityOption.Reverse:
                    self.gravity *= -1
    
        def get_output(self, packet: GameTickPacket) -> ControllerState:
            return ControllerState()
    
    
    if __name__ == "__main__":
        MyBot().run()
  • πŸ›  self.get_ball_prediction_struct() -> self.ball_prediction

  • β›” def is_hot_reload_enabled(self) -> bool - This method is no longer needed, and as such as been removed.

  • πŸ›  def set_game_state(self, game_state: GameState) - This method has been replaced by the following, where the GameState wrapper has been removed:

    def set_game_state(
        self,
        balls: dict[int, DesiredBallState] = {},
        cars: dict[int, DesiredCarState] = {},
        game_info: Optional[DesiredGameInfoState] = None,
        commands: list[ConsoleCommand] = [],
    )

    For balls and cars, the int key is the index of the ball or car you want to modify.

    Example usage, where the game speed is set to 2x:

    from rlbot.flat import DesiredGameInfoState
    
    # ...
    
    self.set_game_state(
        game_info=DesiredGameInfoState(game_speed=2)
    )
  • πŸ›  QuickChats & MatchComms have been combined into one

    • def handle_quick_chat(self, index: int, team: int, quick_chat: QuickChats) - This method has been replaced by the following, where content is a bytes object that contains the message, and display is an optional str that was owned in-game as a v4 QuickChat used to:

      from rlbot.flat import *
      
      # ...
      
      def handle_match_comm(
          self,
          index: int,
          team: int,
          content: bytes,
          display: Optional[str],
          team_only: bool,
      ):
          # Be careful with `content`, it can be anything! Make sure to validate it before using it.
      
          self.logger.info(f"Received match communication from index {index}! {display}")
    • def send_quick_chat(self, team_only: bool, quick_chat: QuickChats) -> def send_match_comm(self, content: bytes, display: Optional[str] = None, team_only: bool = False) - Example:

      from rlbot.flat import *
      
      # ...
      
      # Print "What a save!" in the chat for all players to see
      # Note: `b""` is an empty byte string, which is the equivalent of `None` for bytes.
      # We're using it here to show that we don't need to send any extra data with the message.
      self.send_match_comm(b"", "What a save!")
      
      # Send "15 bot_has_ball" to only our team, with no message to print to the screen
      self.send_match_comm(b"15 bot_has_ball", team_only=True)

πŸ›  BallPrediction

  • β›” num_slices has been removed due to it not being needed anymore.
    • The length of the list slices is now limited to the number of slices in the prediction. Iterating over the list until the end is now the proper way to access all the data.

πŸ›  FieldInfoPacket -> FieldInfo

  • β›” num_boosts and num_goals have been removed due to them not being needed anymore.
    • The length of the lists boost_pads and goals are now limited to the number of boosts and goals on the field. Iterating over the lists until the end is now the proper way to access all the data.

πŸ›  MatchSettings

  • ✨ auto_start_bots - Whether or not RLBot should automatically start the bots & scripts. If set to False, they will not start until the user manually starts them - HOWEVER, the match will start IMMEDIATELY and will not wait for the bots to connect!

  • ✨ script_configurations - The scripts that are running in the match.

  • ✨ freeplay - Whether or not to start the match in Freeplay instead of a normal Exhibition match. May be useful for testing purposes.

  • ✨ launcher & game_path - How RLBot should start the game. The options are:

    • Launcher.Steam - Start the game through Steam.
      • game_path does nothing
    • Launcher.Epic - Windows only - Start the game through the Epic Games Store.
      • No game_path required anymore!
    • Launcher.Custom - Start the game through a custom method. Currently:
      • game_path = "" - The game will not be started by RLBot.
      • game_path = "legendary" - Start the game through the Epic Games Store via the Legendary launcher. Required to use EGS on Linux.
  • β›” game_map - Use game_map_upk instead. If you don't know the file names:

    import random
    
    from rlbot.utils.maps import GAME_MAP_TO_UPK, STANDARD_MAPS
    
    # grab random map name from the list STANDARD_MAPS
    random_map = random.choice(STANDARD_MAPS)
    
    # convert the map name to the upk file name
    game_map_upk = GAME_MAP_TO_UPK[random_map]

    See tests/run_forever.py for a more complete example on running a match with a random map.

  • πŸ›  mutator_settings - New options!

    Interested in what mutators go with what games modes? Check out this list!

    • ✨ multi_ball
      • MultiBall.One
      • MultiBall.Two
      • MultiBall.Four
      • MultiBall.Six
    • ✨ max_time_option
      • MaxTimeOption.Default
      • MaxTimeOption.Eleven_Minutes
    • ✨ game_event_option
      • GameEventOption.Default
      • GameEventOption.Haunted
      • GameEventOption.Rugby
    • ✨ audio_option
      • AudioOption.Default
      • AudioOption.Haunted
    • πŸ›  max_score
      • ✨ MaxScore.Seven
    • πŸ›  ball_type_option
      • ✨ BallTypeOption.Beachball
      • ✨ BallTypeOption.Anniversary
      • ✨ BallTypeOption.Haunted
      • ✨ BallTypeOption.Ekin
      • ✨ BallTypeOption.SpookyCube
    • πŸ›  ball_weight_option
      • ✨ BallWeightOption.Curve_Ball
      • ✨ BallWeightOption.Beach_Ball_Curve
      • ✨ BallWeightOption.Magnus_FutBall
    • πŸ›  ball_size_option
      • ✨ BallSizeOption.Medium
    • πŸ›  ball_size_option
      • ✨ BallBouncinessOption.LowishBounciness
    • πŸ›  rumble_option
      • ✨ RumbleOption.Haunted_Ball_Beam
      • ✨ RumbleOption.Tactical
      • ✨ RumbleOption.BatmanRumble
    • πŸ›  boost_strength_option
      • ✨ BoostStrengthOption.Five
    • πŸ›  gravity_option
      • ✨ GravityOption.Reverse

πŸ›  GameTickPacket

  • πŸ›  game_cars -> players

    • πŸ›  PlayerInfo
      • ✨ latest_touch - The last time the player touched the ball.
        • Will be None if the player has not touched the ball since the last kickoff.
        • Contains the ball_index of the ball that was touched.
      • ✨ air_state - The current state of the car in the air. Possible values are:
        • AirState.OnGround - All 4 wheels are touching the ground.
        • AirState.Jumping - Lasts while jump is being held by the player
        • AirState.DoubleJumping - Lasts for ~13 ticks.
        • AirState.Dodging - Lasts for the duration of the torque applied by the dodge, or ~79 ticks.
        • AirState.InAir - The car is in the air, but not in any of the other states.
      • ✨ dodge_timeout - A countdown, in seconds, until the dodge expires. Starts counting down after the car releases jump. -1 otherwise.
      • ✨ has_dodged - A compliment to has_double_jumped, this is True or False depending on if the player has dodged since it last touched the ground.
      • ✨ dodge_elapsed - The time, in seconds, since the car last started dodging. Continues counting until the car lands on the ground.
      • ✨ dodge_dir - The (-pitch, yaw + roll) of the car when on the first frame of it's dodge.
      • ✨ last_input - The last controller input the player used.
      • ✨ last_spectated - If the player was the last one to be watched by a spectator
      • ✨ accolades - A list of the accolades (as strings) the player earned in the previous tick. Here are some examples of different accolades:
        • Win, Loss, TimePlayed
        • Shot, Assist, Center, Clear, PoolShot
        • Goal, AerialGoal, BicycleGoal, BulletGoal, BackwardsGoal, LongGoal, OvertimeGoal, TurtleGoal
        • AerialHit, BicycleHit, BulletHit, JuggleHit, FirstTouch, BallHit
        • Save, EpicSave, FreezeSave
        • HatTrick, Savior, Playmaker, MVP
        • FastestGoal, SlowestGoal, FurthestGoal, OwnGoal
        • MostBallTouches, FewestBallTouches, MostBoostPickups, FewestBoostPickups, BoostPickups
        • CarTouches, Demolition, Demolish
        • LowFive, HighFive
      • πŸ›  is_demolished -> demolished_timeout - -1 if the player is not demolished, otherwise the time remaining until the player respawns.
      • β›” has_wheel_contact - this is directly replaced by air_state == AirState.OnGround
  • πŸ›  game_boosts -> boost_pads

  • πŸ›  game_teams -> teams

  • β›” num_cars, num_boosts, and num_teams have been removed due to them not being needed anymore.

    • The length of the lists players, boost_pads, and teams are now limited to the number of cars and boosts in the match. Iterating over the lists until the end is now the proper way to access all the data.
  • πŸ›  ball -> balls - balls is now a list, and RLBot officially supports multiple balls in a single match. However, there are also times in a normal game where this list has 0 items - it's recommended that this is checked for near the start of your logic, so your bot doesn't accidentally throw hundreds of errors.

    def get_output(self, packet: GameTickPacket) -> ControllerState:
        if len(packet.balls) == 0:
            return ControllerState()
    
        # Your logic here
        # ...
  • πŸ›  BallInfo

    • β›” latest_touch has been removed due to it now being tracked in PlayerInfo
    • β›” drop_shot_info has been removed due to RLBot not getting any dropshot data.
    • πŸ›  collision_shape -> shape
      • Old collision_shape type:

        class CollisionShape:
            type: ShapeType
            box: BoxShape
            sphere: SphereShape
            cylinder: CylinderShape
      • New shape type:

        class CollisionShape:
            item: Optional[BoxShape | SphereShape | CylinderShape]
  • πŸ›  game_info - Several arguments have been replaced by game_status

    • β›” is_round_active, is_kickoff_pause, and is_match_ended
      • is_round_active is directly replaced by game_status == GameStatus.Active
      • is_kickoff_pause is directly replaced by game_status == GameStatus.Kickoff
      • is_match_ended is directly replaced by game_status == GameStatus.Ended
    • ✨ game_status - The current state of the game. The options are:
      • GameStatus.Inactive - The game is not running.
      • GameStatus.Countdown - The 3.. 2.. 1.. countdown before the a kickoff.
      • GameStatus.Kickoff - After the countdown, but before the game timer starts counting down again. Usually the timer resumes after the ball gets hit by a car.
      • GameStatus.Active - Normal game play.
      • GameStatus.GoalScored - A goal has been scored and the goal animation is playing.
      • GameStatus.Replay - The goal replay is playing.
      • GameStatus.Paused - The game is paused.
      • GameStatus.Ended - The match finished and is on the end screen.
Clone this wiki locally