Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions RLBot/Agents/AgentBaseManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
using Microsoft.Extensions.Logging;
using RLBot.Flat;
using RLBot.Util;

namespace RLBot.Manager;

/// <summary>
/// The AgentBaseManager class is an abstract base class for managing agents in RLBot and
/// implements the common initialization and packet reading behavior. That is, the
/// AgentBaseManager will wait for the match configuration, field information, and team information
/// before initializing the agent(s) of this process. Once initialization is complete,
/// the AgentBaseManager handles messages from RLBot discarding any outdated game packets,
/// finally passing the latest packet and ball prediction to the implementer.
/// </summary>
/// <seealso cref="SingleThreadBotManager"/>
/// <seealso cref="MultiThreadBotManager"/>
/// <seealso cref="HivemindManager"/>
/// <seealso cref="ScriptManager"/>
public abstract class AgentBaseManager
{
protected readonly Logging Logger = new(nameof(AgentBaseManager), LogLevel.Information);

public RLBotInterface Rlbot { get; }
public string AgentId { get; }

public ControllableTeamInfoT TeamInfo { get; private set; } = new();
public MatchConfigurationT MatchConfig { get; private set; } = new();
public FieldInfoT FieldInfo { get; private set; } = new();
public bool IsInitialized { get; private set; } = false;

public bool HasMatchConfig { get; private set; } = false;
public bool HasFieldInfo { get; private set; } = false;
public bool HasTeamInfo { get; private set; } = false;

protected GamePacketT? _latestPacket;
protected BallPredictionT? _latestPrediction;

public AgentBaseManager(RLBotInterface rlbot, string? defaultAgentId = null)
{
var aid = Environment.GetEnvironmentVariable("RLBOT_AGENT_ID") ?? defaultAgentId;

if (aid is null)
{
Logger.LogCritical(
"Environment variable RLBOT_AGENT_ID is not set and no default agent id is passed to "
+ "the constructor of the bot. If you are starting your bot manually, please set it "
+ "manually, e.g. `RLBOT_AGENT_ID=<agent_id> dotnet run`"
);

throw new Exception(
"Environment variable RLBOT_AGENT_ID is not set and no default agent id is passed to the constructor of the bot."
);
}

AgentId = aid;

Rlbot = rlbot;
Rlbot.OnMatchConfigCallback += HandleMatchConfig;
Rlbot.OnFieldInfoCallback += HandleFieldInfo;
Rlbot.OnMatchCommunicationCallback += HandleMatchComm;
Rlbot.OnBallPredictionCallback += HandleBallPrediction;
Rlbot.OnControllableTeamInfoCallback += HandleControllableTeamInfo;
Rlbot.OnGamePacketCallback += HandleGamePacket;
}

/// <summary>Initialize the agents of this process if the all required information has been received.</summary>
private void TryInitialize()
{
if (IsInitialized || !HasMatchConfig || !HasFieldInfo || !HasTeamInfo)
return;

try
{
Logger.LogDebug("Initializing agent: {}", AgentId);
Initialize();
}
catch (Exception e)
{
Logger.LogCritical("Failed to initialize agent: {}.\n{}", AgentId, e);
return;
}

Rlbot.SendInitComplete();
IsInitialized = true;
}

/// <summary>
/// Called when all required information is ready.
/// Bot managers should send initial loadouts before returning.
/// </summary>
protected abstract void Initialize();

private void HandleMatchConfig(MatchConfigurationT matchConfig)
{
MatchConfig = matchConfig;
HasMatchConfig = true;
TryInitialize();
}

private void HandleFieldInfo(FieldInfoT fieldInfo)
{
FieldInfo = fieldInfo;
HasFieldInfo = true;
TryInitialize();
}

protected abstract void HandleMatchComm(MatchCommT msg);

private void HandleBallPrediction(BallPredictionT ballPrediction) =>
_latestPrediction = ballPrediction;

private void HandleControllableTeamInfo(ControllableTeamInfoT controllableTeamInfo)
{
TeamInfo = controllableTeamInfo;
HasTeamInfo = true;
TryInitialize();
}

private void HandleGamePacket(GamePacketT gamePacket) => _latestPacket = gamePacket;

/// <summary>
/// Run the agent manager. This will connect to RLBot and start reading packages. Blocking.
/// </summary>
/// <param name="wantsMatchCommunications">Whether this process wants to receive match comms.</param>
/// <param name="wantsBallPredictions">Whether this process wants to receive ball prediction.</param>
public void Run(bool wantsMatchCommunications = true, bool wantsBallPredictions = true)
{
try
{
Rlbot.Connect(AgentId, wantsMatchCommunications, wantsBallPredictions, false);

while (true)
{
var res = Rlbot.HandleNextIncomingMessage(blocking: _latestPacket is null);

switch (res)
{
case RLBotInterface.MsgHandlingResult.Terminated:
return;
case RLBotInterface.MsgHandlingResult.MoreMsgsQueued:
continue;
case RLBotInterface.MsgHandlingResult.NoIncomingMsgs:
if (_latestPacket is not null)
{
ProcessPacket();
_latestPacket = null;
}
continue;
}
}
}
catch (Exception e)
{
Logger.LogCritical("An error occured while running the bot:\n{0}", e);
return;
}
finally
{
Retire();
}
}

/// <summary>
/// Process the latest game packet and ball prediction.
/// See <see cref="_latestPacket"/> and <see cref="_latestPrediction"/>.
/// Ball prediction may be null, if ball prediction was not requested.
/// </summary>
protected abstract void ProcessPacket();

/// <summary>
/// Invoked when the agent shuts down.
/// </summary>
protected abstract void Retire();
}
3 changes: 3 additions & 0 deletions RLBot/Agents/BotFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using RLBot.Flat;

namespace RLBot.Manager;
136 changes: 136 additions & 0 deletions RLBot/Agents/HivemindManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using RLBot.Flat;

namespace RLBot.Manager;

/// <summary>
/// A manager for hivemind bots.
/// A hivemind is a single process responsible for multiple cars.
/// This manager allows the user to customize the orchestration of the entire swarm.
/// The manager handles the hivemind's life-cycle including initialization, packet reading loop, and retirement on disconnect.
/// </summary>
/// <param name="rlbot">An rlbot connection interface</param>
/// <param name="defaultAgentId">A unique id for this type of bot. Should match the agent id in your bot.toml file and typically has the form "devname/botname/version".</param>
/// <param name="hivemindFactory">A factory for creating the hivemind instance once all required information has arrived.</param>
/// <seealso cref="SingleThreadBotManager"/>
/// <seealso cref="MultiThreadBotManager"/>
public class HivemindManager(
RLBotInterface rlbot,
string? defaultAgentId,
HivemindFactory hivemindFactory
) : AgentBaseManager(rlbot, defaultAgentId)
{
private IHivemind? _hivemind;
private List<int> _indices = new();
private uint _team;

protected override void Initialize()
{
Debug.Assert(_hivemind == null && _indices.Count == 0);

var playerConfs = MatchConfig.PlayerConfigurations;
_team = TeamInfo.Team;
_indices = TeamInfo.Controllables.Select(a => (int)a.Index).ToList();
var names = _indices.ToDictionary(
i => i,
i => playerConfs[i].Variety.AsCustomBot().Name
);

_hivemind = hivemindFactory(
Rlbot,
_indices,
_team,
names,
AgentId,
MatchConfig,
FieldInfo
);

var loadouts = _hivemind.GetInitialLoadouts();
if (loadouts != null)
{
foreach (var loadout in loadouts)
{
Rlbot.SendSetLoadout(
new SetLoadoutT { Index = (uint)loadout.Key, Loadout = loadout.Value }
);
}
}
}

protected override void ProcessPacket()
{
if (_hivemind == null)
return;

IDictionary<int, ControllerStateT>? controllers = null;
try
{
controllers = _hivemind.GetOutputs(_latestPacket!, _latestPrediction);
}
catch (Exception e)
{
Logger.LogError(
"Hivemind '{}' (team {}) encountered an error while processing game packet: {}",
AgentId,
_team,
e
);
}

if (controllers != null)
{
foreach (var ctrl in controllers)
{
Rlbot.SendPlayerInput(
new PlayerInputT
{
PlayerIndex = (uint)ctrl.Key,
ControllerState = ctrl.Value,
}
);
}
}
}

protected override void HandleMatchComm(MatchCommT msg)
{
if (_hivemind == null)
return;

try
{
_hivemind.OnMatchCommReceived(msg);
}
catch (Exception e)
{
Logger.LogError(
"Hivemind '{}' (team {}) encountered an error while processing match comms: {}",
AgentId,
_team,
e
);
}
}

protected override void Retire()
{
if (_hivemind == null)
return;

try
{
_hivemind.OnRetire();
}
catch (Exception e)
{
Logger.LogError(
"Hivemind '{}' (team {}) encountered an error while retiring: {}",
AgentId,
_team,
e
);
}
}
}
48 changes: 48 additions & 0 deletions RLBot/Agents/IBot.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using RLBot.Flat;

namespace RLBot.Manager;

/// <summary>An interface for simple bots.</summary>
/// <seealso cref="SingleThreadBotManager"/>
/// <seealso cref="MultiThreadBotManager"/>
public interface IBot
{
/// <summary>
/// Defines the bot's initial loadout. Return null to use the default loadout specified in the bot.toml file.
/// To change loadout mid-match, use <see cref="RLBotInterface.SendSetLoadout"/> (state-setting must be enabled).
/// </summary>
PlayerLoadoutT? GetInitialLoadout();

/// <summary>
/// Process the latest game packet and return this bot's next action.
/// </summary>
/// <param name="packet">The latest game packet.</param>
/// <param name="ballPrediction">The latest ball prediction. May be null if ball prediction was not requested.</param>
/// <returns>The bots next action.</returns>
ControllerStateT? GetOutput(GamePacketT packet, BallPredictionT? ballPrediction);

/// <summary>
/// Process an incoming match-comm message.
/// See the index and team field to determine the sender.
/// </summary>
/// <param name="msg">A match-comm message</param>
void OnMatchCommReceived(MatchCommT msg);

/// <summary>
/// Invoked when the bot is shut down. Use this to dispose of resources.
/// </summary>
void OnRetire();
}

/// <summary>A delegate for methods that can create <see cref="IBot"/> instances.</summary>
/// <seealso cref="SingleThreadBotManager"/>
/// <seealso cref="MultiThreadBotManager"/>
public delegate IBot BotFactory(
RLBotInterface rlbot,
int index,
uint team,
string name,
string agentId,
MatchConfigurationT matchConfig,
FieldInfoT fieldInfo
);
Loading