-
Notifications
You must be signed in to change notification settings - Fork 3
Migration
Legend: β¨=new, π =reworked/changed, β=removed
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.
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]
name = "Necto"
loadout_config = "loadout.toml"
run_command = ".\\venv\\Scripts\\python bot.py"
run_command_linux = "./venv/bin/python bot.py"
logo_file = "necto_logo.png"
agent_id = "rolv-soren/necto"
[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]
name = "Script Example"
run_command = "python render.py"
run_command_linux = "python3 render.py"
logo_file = "logo.png"
agent_id = "rlbot-community/script-example"
[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-
name
- The name of the bot/script. -
loadout_config
- 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. -
agent_id
- The static, unique id that is associated with this bot.- Preferred format is
"author/bot-name"
- Preferred format is
-
-
[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
-
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
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_config"
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()
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_config"]
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")
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()
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
andself.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 theGameState
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
andcars
, theint
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, wherecontent
is abytes
object that contains the message, anddisplay
is an optionalstr
that was owned in-game as a v4 QuickChat used to:from rlbot.flat import * # ... def handle_match_communication( 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)
-
- β
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.
- The length of the list
- β
num_boosts
andnum_goals
have been removed due to them not being needed anymore.- The length of the lists
boost_pads
andgoals
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.
- The length of the lists
-
β¨
auto_start_bots
- Whether or not RLBot should automatically start the bots & scripts. If set toFalse
, 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!
- No
-
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
- Usegame_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
- β¨
- β¨
-
π
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.
- Will be
- β¨
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 whilejump
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 tohas_double_jumped
, this isTrue
orFalse
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 byair_state == AirState.OnGround
- β¨
- π
-
π
game_boosts
->boost_pads
-
π
game_teams
->teams
-
β
num_cars
,num_boosts
, andnum_teams
have been removed due to them not being needed anymore. -
The length of the lists
players
,boost_pad_states
, andteams
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 inPlayerInfo
- β
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 bygame_state_type
- β
is_round_active
,is_kickoff_pause
, andis_match_ended
-
is_round_active
is directly replaced bygame_state_type == GameStateType.Active
-
is_kickoff_pause
is directly replaced bygame_state_type == GameStateType.Kickoff
-
is_match_ended
is directly replaced bygame_state_type == GameStateType.Ended
-
- β¨
game_state_type
- The current state of the game. The options are:-
GameStateType.Inactive
- The game is not running. -
GameStateType.Countdown
- The 3.. 2.. 1.. countdown before the a kickoff. -
GameStateType.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. -
GameStateType.Active
- Normal game play. -
GameStateType.GoalScored
- A goal has been scored and the goal animation is playing. -
GameStateType.Replay
- The goal replay is playing. -
GameStateType.Paused
- The game is paused. -
GameStateType.Ended
- The match finished and is on the end screen.
-
- β