diff --git a/RLBot/Agents/AgentBaseManager.cs b/RLBot/Agents/AgentBaseManager.cs
new file mode 100644
index 0000000..fc25d94
--- /dev/null
+++ b/RLBot/Agents/AgentBaseManager.cs
@@ -0,0 +1,174 @@
+using Microsoft.Extensions.Logging;
+using RLBot.Flat;
+using RLBot.Util;
+
+namespace RLBot.Manager;
+
+///
+/// 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.
+///
+///
+///
+///
+///
+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= 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;
+ }
+
+ /// Initialize the agents of this process if the all required information has been received.
+ 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;
+ }
+
+ ///
+ /// Called when all required information is ready.
+ /// Bot managers should send initial loadouts before returning.
+ ///
+ 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;
+
+ ///
+ /// Run the agent manager. This will connect to RLBot and start reading packages. Blocking.
+ ///
+ /// Whether this process wants to receive match comms.
+ /// Whether this process wants to receive ball prediction.
+ 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();
+ }
+ }
+
+ ///
+ /// Process the latest game packet and ball prediction.
+ /// See and .
+ /// Ball prediction may be null, if ball prediction was not requested.
+ ///
+ protected abstract void ProcessPacket();
+
+ ///
+ /// Invoked when the agent shuts down.
+ ///
+ protected abstract void Retire();
+}
diff --git a/RLBot/Agents/BotFactory.cs b/RLBot/Agents/BotFactory.cs
new file mode 100644
index 0000000..c77f230
--- /dev/null
+++ b/RLBot/Agents/BotFactory.cs
@@ -0,0 +1,3 @@
+using RLBot.Flat;
+
+namespace RLBot.Manager;
diff --git a/RLBot/Agents/HivemindManager.cs b/RLBot/Agents/HivemindManager.cs
new file mode 100644
index 0000000..8b0f5a0
--- /dev/null
+++ b/RLBot/Agents/HivemindManager.cs
@@ -0,0 +1,136 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+///
+/// 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.
+///
+/// An rlbot connection interface
+/// 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".
+/// A factory for creating the hivemind instance once all required information has arrived.
+///
+///
+public class HivemindManager(
+ RLBotInterface rlbot,
+ string? defaultAgentId,
+ HivemindFactory hivemindFactory
+) : AgentBaseManager(rlbot, defaultAgentId)
+{
+ private IHivemind? _hivemind;
+ private List _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? 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
+ );
+ }
+ }
+}
diff --git a/RLBot/Agents/IBot.cs b/RLBot/Agents/IBot.cs
new file mode 100644
index 0000000..bb1c52a
--- /dev/null
+++ b/RLBot/Agents/IBot.cs
@@ -0,0 +1,48 @@
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+/// An interface for simple bots.
+///
+///
+public interface IBot
+{
+ ///
+ /// 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 (state-setting must be enabled).
+ ///
+ PlayerLoadoutT? GetInitialLoadout();
+
+ ///
+ /// Process the latest game packet and return this bot's next action.
+ ///
+ /// The latest game packet.
+ /// The latest ball prediction. May be null if ball prediction was not requested.
+ /// The bots next action.
+ ControllerStateT? GetOutput(GamePacketT packet, BallPredictionT? ballPrediction);
+
+ ///
+ /// Process an incoming match-comm message.
+ /// See the index and team field to determine the sender.
+ ///
+ /// A match-comm message
+ void OnMatchCommReceived(MatchCommT msg);
+
+ ///
+ /// Invoked when the bot is shut down. Use this to dispose of resources.
+ ///
+ void OnRetire();
+}
+
+/// A delegate for methods that can create instances.
+///
+///
+public delegate IBot BotFactory(
+ RLBotInterface rlbot,
+ int index,
+ uint team,
+ string name,
+ string agentId,
+ MatchConfigurationT matchConfig,
+ FieldInfoT fieldInfo
+);
diff --git a/RLBot/Agents/IHivemind.cs b/RLBot/Agents/IHivemind.cs
new file mode 100644
index 0000000..db43df2
--- /dev/null
+++ b/RLBot/Agents/IHivemind.cs
@@ -0,0 +1,52 @@
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+///
+/// An interface for a hivemind, a single process responsible for multiple cars.
+///
+///
+public interface IHivemind
+{
+ ///
+ /// Programmatically define the hiveminds initial loadout by mapping bot indexes to the desired loadout.
+ /// Return null to use the default loadout specified in the bot.toml file.
+ /// To change loadout mid-match, use (state-setting must be enabled).
+ ///
+ IDictionary? GetInitialLoadouts();
+
+ ///
+ /// Process the latest game packet and return the next action for each member of the hivemind.
+ ///
+ /// The latest game packet.
+ /// The latest ball prediction. May be null if ball prediction was not requested.
+ /// The bots' next actions.
+ IDictionary? GetOutputs(
+ GamePacketT packet,
+ BallPredictionT? ballPred
+ );
+
+ ///
+ /// Process an incoming match-comm message.
+ /// See the index and team field to determine the sender.
+ ///
+ /// A match-comm message
+ void OnMatchCommReceived(MatchCommT msg);
+
+ ///
+ /// Invoked when the hivemind is shut down. Use this to dispose of resources.
+ ///
+ void OnRetire();
+}
+
+/// A delegate for methods that can create instances.
+///
+public delegate IHivemind HivemindFactory(
+ RLBotInterface rlbot,
+ List indices,
+ uint team,
+ Dictionary names,
+ string agentId,
+ MatchConfigurationT matchConfig,
+ FieldInfoT fieldInfo
+);
diff --git a/RLBot/Agents/IScript.cs b/RLBot/Agents/IScript.cs
new file mode 100644
index 0000000..a24baa4
--- /dev/null
+++ b/RLBot/Agents/IScript.cs
@@ -0,0 +1,12 @@
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+public interface IScript
+{
+ void ProcessPacket(GamePacketT gamePacket, BallPredictionT? ballPrediction);
+
+ void OnMatchCommReceived(MatchCommT msg);
+
+ void OnRetire();
+}
diff --git a/RLBot/Agents/MultiThreadBotManager.cs b/RLBot/Agents/MultiThreadBotManager.cs
new file mode 100644
index 0000000..3fb4042
--- /dev/null
+++ b/RLBot/Agents/MultiThreadBotManager.cs
@@ -0,0 +1,179 @@
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+///
+/// A simple bot manager than runs each bot on a different thread.
+/// Ideal for simple hivemind bots that occasionally coordinate and share computation, but run mostly in parallel.
+/// Be mindful with shared resources and static variables when using this manager.
+/// The manager handles the bots' life-cycle including initialization, packet reading loop, and retirement on disconnect.
+///
+/// An rlbot connection interface
+/// 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".
+/// A bot factory for creating instances of the bot once all required information has arrived.
+///
+///
+public class MultiThreadBotManager(
+ RLBotInterface rlbot,
+ string? defaultAgentId,
+ BotFactory botFactory
+) : AgentBaseManager(rlbot, defaultAgentId)
+{
+ private class BoxedBool(bool val)
+ {
+ public bool Value { get; set; } = val;
+ }
+
+ private record struct GameTickData(GamePacketT Packet, BallPredictionT? BallPred);
+
+ private record BotInfo(
+ IBot Bot,
+ string Name,
+ int Index,
+ Thread Thread,
+ BoxedBool Running,
+ BlockingCollection Queue,
+ ConcurrentQueue MatchComms
+ );
+
+ private readonly List _botInfos = new();
+
+ protected override void Initialize()
+ {
+ Debug.Assert(_botInfos.Count == 0);
+
+ // Initialize bots and start threads
+ var playerConfs = MatchConfig.PlayerConfigurations;
+ var team = TeamInfo.Team;
+ foreach (var agent in TeamInfo.Controllables)
+ {
+ var index = (int)agent.Index;
+ var name = playerConfs[index].Variety.AsCustomBot().Name;
+ var bot = botFactory(Rlbot, index, team, name, AgentId, MatchConfig, FieldInfo);
+ var queue = new BlockingCollection(
+ new ConcurrentQueue()
+ );
+ var matchComms = new ConcurrentQueue();
+ var info = new BotInfo(
+ bot,
+ name,
+ index,
+ new Thread(BotLoop),
+ new BoxedBool(true),
+ queue,
+ matchComms
+ );
+ _botInfos.Add(info);
+ info.Thread.Start(info);
+ }
+
+ // Initial loadouts
+ foreach (var info in _botInfos)
+ {
+ var loadout = info.Bot.GetInitialLoadout();
+ if (loadout != null)
+ {
+ Rlbot.SendSetLoadout(
+ new SetLoadoutT { Index = (uint)info.Index, Loadout = loadout }
+ );
+ }
+ }
+ }
+
+ private void BotLoop(object? botInfo)
+ {
+ var info = (BotInfo)botInfo!;
+ try
+ {
+ while (info.Running.Value)
+ {
+ // Match comms
+ while (info.MatchComms.TryDequeue(out var msg))
+ {
+ try
+ {
+ info.Bot.OnMatchCommReceived(msg);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing match comms: {}",
+ info.Name,
+ e
+ );
+ return;
+ }
+ }
+
+ // Game packet (temp blocking)
+ if (!info.Queue.TryTake(out var data, 200))
+ {
+ continue;
+ }
+
+ ControllerStateT? ctrl = null;
+ try
+ {
+ ctrl = info.Bot.GetOutput(data.Packet, data.BallPred);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing game packet: {}",
+ info.Name,
+ e
+ );
+ return;
+ }
+
+ if (ctrl != null)
+ {
+ Rlbot.SendPlayerInput(
+ new PlayerInputT
+ {
+ PlayerIndex = (uint)info.Index,
+ ControllerState = ctrl,
+ }
+ );
+ }
+ }
+ }
+ finally
+ {
+ info.Bot.OnRetire();
+ }
+ }
+
+ protected override void ProcessPacket()
+ {
+ foreach (var info in _botInfos)
+ {
+ // Take previous packet if bot-loop did not already. Ensures that there is only one item in queue.
+ info.Queue.TryTake(out _);
+ info.Queue.Add(new GameTickData(_latestPacket!, _latestPrediction));
+ }
+ }
+
+ protected override void HandleMatchComm(MatchCommT msg)
+ {
+ foreach (var info in _botInfos)
+ {
+ info.MatchComms.Enqueue(msg);
+ }
+ }
+
+ protected override void Retire()
+ {
+ foreach (var info in _botInfos)
+ {
+ info.Running.Value = false;
+ }
+ foreach (var info in _botInfos)
+ {
+ info.Thread.Join();
+ }
+ }
+}
diff --git a/RLBot/Agents/ScriptManager.cs b/RLBot/Agents/ScriptManager.cs
new file mode 100644
index 0000000..5b8f074
--- /dev/null
+++ b/RLBot/Agents/ScriptManager.cs
@@ -0,0 +1,90 @@
+using Microsoft.Extensions.Logging;
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+/// A delegate for methods that can create instances.
+///
+public delegate IScript ScriptFactory(
+ RLBotInterface rlbot,
+ int index,
+ string agentId,
+ MatchConfigurationT matchConfig,
+ FieldInfoT fieldInfo
+);
+
+///
+/// A simple manager for scripts. Scripts observe the match and potentially uses debug rendering, state-setting, or match comms.
+/// The manager handles the script's life-cycle including initialization, packet reading loop, and retirement on disconnect.
+///
+/// An rlbot connection interface
+/// A unique id for this type of script. Should match the agent id in your script.toml file and typically has the form "devname/scriptname/version".
+/// A factory for creating the script instance once all required information has arrived.
+public class ScriptManager(
+ RLBotInterface rlbot,
+ string defaultAgentId,
+ ScriptFactory scriptFactory
+) : AgentBaseManager(rlbot, defaultAgentId)
+{
+ private IScript? _script;
+ private int _index;
+ private string _name = "Unknown Script";
+
+ protected override void Initialize()
+ {
+ var agent = TeamInfo.Controllables[0];
+ _index = (int)agent.Index;
+ _name = MatchConfig.ScriptConfigurations[_index].Name;
+ _script = scriptFactory(Rlbot, _index, AgentId, MatchConfig, FieldInfo);
+ }
+
+ protected override void ProcessPacket()
+ {
+ if (_script == null)
+ return;
+ try
+ {
+ _script.ProcessPacket(_latestPacket!, _latestPrediction);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing game packet: {}",
+ _name,
+ e
+ );
+ }
+ }
+
+ protected override void HandleMatchComm(MatchCommT msg)
+ {
+ if (_script == null)
+ return;
+ try
+ {
+ _script.OnMatchCommReceived(msg);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing match comms: {}",
+ _name,
+ e
+ );
+ }
+ }
+
+ protected override void Retire()
+ {
+ if (_script == null)
+ return;
+ try
+ {
+ _script.OnRetire();
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("{} encountered an error while retiring: {}", _name, e);
+ }
+ }
+}
diff --git a/RLBot/Agents/SingleThreadBotManager.cs b/RLBot/Agents/SingleThreadBotManager.cs
new file mode 100644
index 0000000..dfeaa89
--- /dev/null
+++ b/RLBot/Agents/SingleThreadBotManager.cs
@@ -0,0 +1,119 @@
+using Microsoft.Extensions.Logging;
+using RLBot.Flat;
+
+namespace RLBot.Manager;
+
+///
+/// A simple bot manager than runs everything on a single thread.
+/// Ideal for standard, non-hivemind bots. Hiveminds are supported too, but consider other managers.
+/// The manager handles the bot(s)'s life-cycle including initialization, packet reading loop, and retirement on disconnect.
+///
+/// An rlbot connection interface
+/// 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".
+/// A bot factory for creating instances of the bot once all required information has arrived.
+///
+///
+public class SingleThreadBotManager(
+ RLBotInterface rlbot,
+ string defaultAgentId,
+ BotFactory botFactory
+) : AgentBaseManager(rlbot, defaultAgentId)
+{
+ private record BotInfo(IBot Bot, string Name, int Index);
+
+ private readonly List _botInfos = new();
+
+ protected override void Initialize()
+ {
+ // Create bots
+ var playerConfigs = MatchConfig.PlayerConfigurations;
+ var team = TeamInfo.Team;
+ _botInfos.Clear();
+ foreach (var agent in TeamInfo.Controllables)
+ {
+ var index = (int)agent.Index;
+ var name = playerConfigs[index].Variety.AsCustomBot().Name;
+ var bot = botFactory(Rlbot, index, team, name, AgentId, MatchConfig, FieldInfo);
+ var process = new BotInfo(bot, name, index);
+ _botInfos.Add(process);
+ }
+
+ // Set loadouts
+ foreach (var botInfo in _botInfos)
+ {
+ var loadout = botInfo.Bot.GetInitialLoadout();
+ if (loadout != null)
+ {
+ Rlbot.SendSetLoadout(
+ new SetLoadoutT { Index = (uint)botInfo.Index, Loadout = loadout }
+ );
+ }
+ }
+ }
+
+ protected override void ProcessPacket()
+ {
+ foreach (var botInfo in _botInfos)
+ {
+ try
+ {
+ var ctrl = botInfo.Bot.GetOutput(_latestPacket!, _latestPrediction);
+ if (ctrl != null)
+ {
+ Rlbot.SendPlayerInput(
+ new PlayerInputT
+ {
+ PlayerIndex = (uint)botInfo.Index,
+ ControllerState = ctrl,
+ }
+ );
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing game packet: {}",
+ botInfo.Name,
+ e
+ );
+ return;
+ }
+ }
+ }
+
+ protected override void HandleMatchComm(MatchCommT msg)
+ {
+ foreach (var botInfo in _botInfos)
+ {
+ try
+ {
+ botInfo.Bot.OnMatchCommReceived(msg);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(
+ "{} encountered an error while processing match comms: {}",
+ botInfo.Name,
+ e
+ );
+ return;
+ }
+ }
+ }
+
+ protected override void Retire()
+ {
+ foreach (var botInfo in _botInfos)
+ {
+ try
+ {
+ botInfo.Bot.OnRetire();
+ }
+ catch (Exception e)
+ {
+ Logger.LogError("{} encountered an error while retiring: {}", botInfo.Name, e);
+ return;
+ }
+ }
+ }
+}
diff --git a/RLBot/Util/GameStateExt.cs b/RLBot/Common/GameStateExt.cs
similarity index 100%
rename from RLBot/Util/GameStateExt.cs
rename to RLBot/Common/GameStateExt.cs
diff --git a/RLBot/Util/RenderAnchorExtensions.cs b/RLBot/Common/RenderAnchorExtensions.cs
similarity index 100%
rename from RLBot/Util/RenderAnchorExtensions.cs
rename to RLBot/Common/RenderAnchorExtensions.cs
diff --git a/RLBot/Manager/Renderer.cs b/RLBot/Common/Renderer.cs
similarity index 86%
rename from RLBot/Manager/Renderer.cs
rename to RLBot/Common/Renderer.cs
index a6893c2..f4a7517 100644
--- a/RLBot/Manager/Renderer.cs
+++ b/RLBot/Common/Renderer.cs
@@ -18,6 +18,9 @@ public class Renderer
private readonly RLBotInterface _rlbotInterface;
+ private float _screenWidthScale = 1f;
+ private float _screenHeightScale = 1f;
+
/// True if rendering is enabled. Note the Renderer will still send render messages if
/// draw methods are called, but RLBot will likely ignore the messages.
public bool CanRender { get; private set; } = true;
@@ -40,11 +43,28 @@ public class Renderer
/// All active render groups. Render groups persist until overriden or cleared.
public HashSet.Enumerator ActiveGroupIds => _activeGroupIds.GetEnumerator();
- public Renderer(RLBotInterface rlbotInterface)
+ public Renderer(
+ RLBotInterface rlbotInterface,
+ int screenWidth = 1920,
+ int screenHeight = 1080
+ )
{
_rlbotInterface = rlbotInterface;
- _rlbotInterface.OnMatchConfigCallback += m => CanRender = m.EnableRendering == DebugRendering.OnByDefault;
+ _rlbotInterface.OnMatchConfigCallback += m =>
+ CanRender = m.EnableRendering == DebugRendering.OnByDefault;
_rlbotInterface.OnRenderingStatusCallback += s => CanRender = s.Status;
+ AssumeScreenSize(screenWidth, screenHeight);
+ }
+
+ ///
+ /// Set the presumed screen size when scaling 2D rendering to screens-space coordinates.
+ ///
+ ///
+ ///
+ public void AssumeScreenSize(int width, int height)
+ {
+ _screenWidthScale = 1f / width;
+ _screenHeightScale = 1f / height;
}
///
@@ -114,6 +134,7 @@ public void ClearAll()
///
/// Add a render message to the current render group.
/// It is typically more convenient to use the other draw methods.
+ /// 2D positions and sizes must be in screen-space, e.g. x=0.1 is 10% of screen width.
///
/// The render message to add
public void Draw(RenderMessageT msg)
@@ -200,12 +221,12 @@ public void DrawPolyLine3D(IEnumerable points, Color? color = null) =>
DrawPolyLine3D(points.Select(v => v.ToFlatBuf()), color);
///
- /// Draw text in 2D space. x and y uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height.
+ /// Draw text in 2D space. x and y uses pixel coordinates based on the declared screen size.
/// The characters of the font are 20 pixels wide and 10 pixels tall when scale is 1.
///
/// The text to draw.
- /// x position of text in screen-space coordinates.
- /// y position of text in screen-space coordinates.
+ /// x position of text in pixel coordinates.
+ /// y position of text in pixel coordinates.
/// Scale of text.
/// Color of text. Uses if null.
/// Color of background for the text. Uses transparent if null.
@@ -229,8 +250,8 @@ public void DrawText2D(
new String2DT
{
Text = text,
- X = x,
- Y = y,
+ X = x * _screenWidthScale,
+ Y = y * _screenHeightScale,
Scale = scale,
Foreground = (foreground ?? Color).ToFlatBuf(),
Background = (background ?? Color.Transparent).ToFlatBuf(),
@@ -243,11 +264,11 @@ public void DrawText2D(
}
///
- /// Draw text in 2D space. Position uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height.
+ /// Draw text in 2D space. Position uses pixel coordinates based on the declared screen size.
/// The characters of the font are 20 pixels wide and 10 pixels tall when scale is 1.
///
/// The text to draw.
- /// Position of text in screen-space coordinates.
+ /// Position of text in pixel coordinates.
/// Scale of text.
/// Color of text. Uses if null.
/// Color of background for the text. Uses transparent if null.
@@ -345,12 +366,12 @@ public void DrawText3D(
///
/// Draw a rectangle in 2D space.
- /// X, y, width, and height uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height.
+ /// X, y, width, and height uses pixel coordinates based on the declared screen size.
///
- /// x position of the rectangle in screen-space coordinates.
- /// y position of the rectangle in screen-space coordinates.
- /// Width of the rectangle as a fraction of screen-space width.
- /// Height of the rectangle as a fraction of screen-space height.
+ /// x position of the rectangle in pixel coordinates.
+ /// y position of the rectangle in pixel coordinates.
+ /// Width of the rectangle in pixels.
+ /// Height of the rectangle in pixels.
/// Color of the rectangle. Uses if null.
/// Whether the rectangle should be centered at (x,y), or if (x,y) is the top left corner.
public void DrawRect2D(
@@ -368,10 +389,10 @@ public void DrawRect2D(
Variety = RenderTypeUnion.FromRect2D(
new Rect2DT
{
- X = x,
- Y = y,
- Width = width,
- Height = height,
+ X = x * _screenWidthScale,
+ Y = y * _screenHeightScale,
+ Width = width * _screenWidthScale,
+ Height = height * _screenHeightScale,
Color = (color ?? Color).ToFlatBuf(),
}
),
@@ -381,10 +402,10 @@ public void DrawRect2D(
///
/// Draw a rectangle in 2D space.
- /// X, y, width, and height uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height.
+ /// X, y, width, and height uses pixel coordinates based on the declared screen size.
///
- /// Position of the rectangle in screen-space coordinates.
- /// Size of the rectangle using fractions of screen-space size.
+ /// Position of the rectangle in pixel coordinates.
+ /// Size of the rectangle in pixels.
/// Color of the rectangle. Uses if null.
/// Whether the rectangle should be centered at (x,y), or if (x,y) is the top left corner.
public void DrawRect2D(
@@ -396,7 +417,7 @@ public void DrawRect2D(
///
/// Draw a rectangle in 3D space.
- /// Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height.
+ /// Width and height uses pixel coordinates based on the declared screen size.
/// The size does not change based on distance to the camera.
///
/// The anchor to draw the rectangle at.
@@ -417,8 +438,8 @@ public void DrawRect3D(
new Rect3DT
{
Anchor = anchor,
- Width = width,
- Height = height,
+ Width = width * _screenWidthScale,
+ Height = height * _screenHeightScale,
Color = (color ?? Color).ToFlatBuf(),
}
),
@@ -428,7 +449,7 @@ public void DrawRect3D(
///
/// Draw a rectangle in 3D space.
- /// Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height.
+ /// Width and height uses pixel coordinates based on the declared screen size.
/// The size does not change based on distance to the camera.
///
/// The anchor to draw the rectangle at.
@@ -439,7 +460,7 @@ public void DrawRect3D(RenderAnchorT anchor, Vector2 size, Color? color = null)
///
/// Draw a rectangle in 3D space.
- /// Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height.
+ /// Width and height uses pixel coordinates based on the declared screen size.
/// The size does not change based on distance to the camera.
///
/// The position of the rectangle.
@@ -451,7 +472,7 @@ public void DrawRect3D(Vector3 position, float width, float height, Color? color
///
/// Draw a rectangle in 3D space.
- /// Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height.
+ /// Width and height uses pixel coordinates based on the declared screen size.
/// The size does not change based on distance to the camera.
///
/// The position of the rectangle.
diff --git a/RLBot/Util/SystemColorExtensions.cs b/RLBot/Common/SystemColorExtensions.cs
similarity index 99%
rename from RLBot/Util/SystemColorExtensions.cs
rename to RLBot/Common/SystemColorExtensions.cs
index 31036cf..be561c3 100644
--- a/RLBot/Util/SystemColorExtensions.cs
+++ b/RLBot/Common/SystemColorExtensions.cs
@@ -8,7 +8,7 @@ public static class SystemColorExtensions
/// Convert an RLBot Color to a System.Drawing.Color
public static System.Drawing.Color ToSysColor(this Color color) =>
System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
-
+
/// Convert a system color to a flatbuffer color.
public static ColorT ToFlatBuf(this System.Drawing.Color color) =>
new()
diff --git a/RLBot/Util/SystemVectorExtensions.cs b/RLBot/Common/SystemVectorExtensions.cs
similarity index 98%
rename from RLBot/Util/SystemVectorExtensions.cs
rename to RLBot/Common/SystemVectorExtensions.cs
index 583ab07..51a4e70 100644
--- a/RLBot/Util/SystemVectorExtensions.cs
+++ b/RLBot/Common/SystemVectorExtensions.cs
@@ -7,10 +7,10 @@ public static class SystemVectorExtensions
{
/// Convert an RLBot flatbuffer vector to a System.Numerics.Vector3
public static System.Numerics.Vector3 ToSysVec(this Vector3T v) => new(v.X, v.Y, v.Z);
-
+
/// Convert an RLBot flatbuffer vector to a System.Numerics.Vector2
public static System.Numerics.Vector2 ToSysVec(this Vector2T v) => new(v.X, v.Y);
-
+
/// Convert a system vector to a flatbuffer vector.
public static Vector2T ToFlatBuf(this System.Numerics.Vector2 v) =>
new() { X = v.X, Y = v.Y };
diff --git a/RLBot/GameState/DesiredBallStateBuilder.cs b/RLBot/GameState/DesiredBallStateBuilder.cs
index 69ddab7..be845bb 100644
--- a/RLBot/GameState/DesiredBallStateBuilder.cs
+++ b/RLBot/GameState/DesiredBallStateBuilder.cs
@@ -12,7 +12,7 @@ public DesiredBallStateBuilder(DesiredBallStateT ballState)
_ballState = ballState;
ballState.Physics ??= new DesiredPhysicsT();
}
-
+
///
/// Set the desired location of the ball.
///
@@ -22,11 +22,11 @@ public DesiredBallStateBuilder Location(Vector3 location)
{
X = new FloatT { Val = location.X },
Y = new FloatT { Val = location.Y },
- Z = new FloatT { Val = location.Z }
+ Z = new FloatT { Val = location.Z },
};
return this;
}
-
+
///
/// Set the desired location of the ball.
///
@@ -36,7 +36,7 @@ public DesiredBallStateBuilder Location(Flat.Vector3 location)
{
X = new FloatT { Val = location.X },
Y = new FloatT { Val = location.Y },
- Z = new FloatT { Val = location.Z }
+ Z = new FloatT { Val = location.Z },
};
return this;
}
@@ -50,7 +50,7 @@ public DesiredBallStateBuilder LocationX(float value)
_ballState.Physics.Location.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired location of the ball.
///
@@ -60,7 +60,7 @@ public DesiredBallStateBuilder LocationY(float value)
_ballState.Physics.Location.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired location of the ball.
///
@@ -70,7 +70,7 @@ public DesiredBallStateBuilder LocationZ(float value)
_ballState.Physics.Location.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired velocity of the ball.
///
@@ -80,7 +80,7 @@ public DesiredBallStateBuilder Velocity(Vector3 velocity)
{
X = new FloatT { Val = velocity.X },
Y = new FloatT { Val = velocity.Y },
- Z = new FloatT { Val = velocity.Z }
+ Z = new FloatT { Val = velocity.Z },
};
return this;
}
@@ -94,7 +94,7 @@ public DesiredBallStateBuilder VelocityX(float value)
_ballState.Physics.Velocity.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired velocity of the ball.
///
@@ -104,7 +104,7 @@ public DesiredBallStateBuilder VelocityY(float value)
_ballState.Physics.Velocity.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired velocity of the ball.
///
@@ -114,7 +114,7 @@ public DesiredBallStateBuilder VelocityZ(float value)
_ballState.Physics.Velocity.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired angular velocity of the ball.
///
@@ -124,7 +124,7 @@ public DesiredBallStateBuilder AngularVelocity(Vector3 angVel)
{
X = new FloatT { Val = angVel.X },
Y = new FloatT { Val = angVel.Y },
- Z = new FloatT { Val = angVel.Z }
+ Z = new FloatT { Val = angVel.Z },
};
return this;
}
@@ -138,7 +138,7 @@ public DesiredBallStateBuilder AngularVelocityX(float value)
_ballState.Physics.AngularVelocity.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired angular velocity of the ball.
///
@@ -148,7 +148,7 @@ public DesiredBallStateBuilder AngularVelocityY(float value)
_ballState.Physics.AngularVelocity.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired angular velocity of the ball.
///
@@ -158,7 +158,7 @@ public DesiredBallStateBuilder AngularVelocityZ(float value)
_ballState.Physics.AngularVelocity.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired rotation of the ball.
///
@@ -168,11 +168,11 @@ public DesiredBallStateBuilder Rotation(Vector3 rotation)
{
Pitch = new FloatT { Val = rotation.X },
Yaw = new FloatT { Val = rotation.Y },
- Roll = new FloatT { Val = rotation.Z }
+ Roll = new FloatT { Val = rotation.Z },
};
return this;
}
-
+
///
/// Set the desired rotation of the ball.
///
@@ -182,7 +182,7 @@ public DesiredBallStateBuilder Rotation(RotatorT rotation)
{
Pitch = new FloatT { Val = rotation.Pitch },
Yaw = new FloatT { Val = rotation.Yaw },
- Roll = new FloatT { Val = rotation.Roll }
+ Roll = new FloatT { Val = rotation.Roll },
};
return this;
}
@@ -196,7 +196,7 @@ public DesiredBallStateBuilder RotationPitch(float value)
_ballState.Physics.Rotation.Pitch = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the yaw value of the desired rotation of the ball.
///
@@ -206,7 +206,7 @@ public DesiredBallStateBuilder RotationYaw(float value)
_ballState.Physics.Rotation.Yaw = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the roll value of the desired rotation of the ball.
///
diff --git a/RLBot/GameState/DesiredCarStateBuilder.cs b/RLBot/GameState/DesiredCarStateBuilder.cs
index 88c0abb..b122a8b 100644
--- a/RLBot/GameState/DesiredCarStateBuilder.cs
+++ b/RLBot/GameState/DesiredCarStateBuilder.cs
@@ -14,7 +14,7 @@ public DesiredCarStateBuilder(DesiredCarStateT carState)
}
///
- /// Set the desired boost amount.
+ /// Set the desired boost amount.
///
public DesiredCarStateBuilder Boost(float value)
{
@@ -31,11 +31,11 @@ public DesiredCarStateBuilder Location(Vector3 location)
{
X = new FloatT { Val = location.X },
Y = new FloatT { Val = location.Y },
- Z = new FloatT { Val = location.Z }
+ Z = new FloatT { Val = location.Z },
};
return this;
}
-
+
///
/// Set the desired location of the car.
///
@@ -45,7 +45,7 @@ public DesiredCarStateBuilder Location(Flat.Vector3 location)
{
X = new FloatT { Val = location.X },
Y = new FloatT { Val = location.Y },
- Z = new FloatT { Val = location.Z }
+ Z = new FloatT { Val = location.Z },
};
return this;
}
@@ -59,7 +59,7 @@ public DesiredCarStateBuilder LocationX(float value)
_carState.Physics.Location.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired location of the car.
///
@@ -69,7 +69,7 @@ public DesiredCarStateBuilder LocationY(float value)
_carState.Physics.Location.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired location of the car.
///
@@ -79,7 +79,7 @@ public DesiredCarStateBuilder LocationZ(float value)
_carState.Physics.Location.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired velocity of the car.
///
@@ -89,7 +89,7 @@ public DesiredCarStateBuilder Velocity(Vector3 velocity)
{
X = new FloatT { Val = velocity.X },
Y = new FloatT { Val = velocity.Y },
- Z = new FloatT { Val = velocity.Z }
+ Z = new FloatT { Val = velocity.Z },
};
return this;
}
@@ -103,7 +103,7 @@ public DesiredCarStateBuilder VelocityX(float value)
_carState.Physics.Velocity.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired velocity of the car.
///
@@ -113,7 +113,7 @@ public DesiredCarStateBuilder VelocityY(float value)
_carState.Physics.Velocity.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired velocity of the car.
///
@@ -123,7 +123,7 @@ public DesiredCarStateBuilder VelocityZ(float value)
_carState.Physics.Velocity.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired angular velocity of the car.
///
@@ -133,7 +133,7 @@ public DesiredCarStateBuilder AngularVelocity(Vector3 angVel)
{
X = new FloatT { Val = angVel.X },
Y = new FloatT { Val = angVel.Y },
- Z = new FloatT { Val = angVel.Z }
+ Z = new FloatT { Val = angVel.Z },
};
return this;
}
@@ -147,7 +147,7 @@ public DesiredCarStateBuilder AngularVelocityX(float value)
_carState.Physics.AngularVelocity.X = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Y value of the desired angular velocity of the car.
///
@@ -157,7 +157,7 @@ public DesiredCarStateBuilder AngularVelocityY(float value)
_carState.Physics.AngularVelocity.Y = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the Z value of the desired angular velocity of the car.
///
@@ -167,7 +167,7 @@ public DesiredCarStateBuilder AngularVelocityZ(float value)
_carState.Physics.AngularVelocity.Z = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the desired rotation of the car.
///
@@ -177,11 +177,11 @@ public DesiredCarStateBuilder Rotation(Vector3 rotation)
{
Pitch = new FloatT { Val = rotation.X },
Yaw = new FloatT { Val = rotation.Y },
- Roll = new FloatT { Val = rotation.Z }
+ Roll = new FloatT { Val = rotation.Z },
};
return this;
}
-
+
///
/// Set the desired rotation of the car.
///
@@ -191,7 +191,7 @@ public DesiredCarStateBuilder Rotation(RotatorT rotation)
{
Pitch = new FloatT { Val = rotation.Pitch },
Yaw = new FloatT { Val = rotation.Yaw },
- Roll = new FloatT { Val = rotation.Roll }
+ Roll = new FloatT { Val = rotation.Roll },
};
return this;
}
@@ -205,7 +205,7 @@ public DesiredCarStateBuilder RotationPitch(float value)
_carState.Physics.Rotation.Pitch = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the yaw value of the desired rotation of the car.
///
@@ -215,7 +215,7 @@ public DesiredCarStateBuilder RotationYaw(float value)
_carState.Physics.Rotation.Yaw = new FloatT { Val = value };
return this;
}
-
+
///
/// Set the roll value of the desired rotation of the car.
///
diff --git a/RLBot/GameState/DesiredGameStateBuilder.cs b/RLBot/GameState/DesiredGameStateBuilder.cs
index 9ac1394..6044225 100644
--- a/RLBot/GameState/DesiredGameStateBuilder.cs
+++ b/RLBot/GameState/DesiredGameStateBuilder.cs
@@ -14,7 +14,9 @@ public struct DesiredGameStateBuilder(RLBotInterface rlbotInterface)
///
/// Modify the desired match info.
///
- public DesiredGameStateBuilder MatchInfo(Func build)
+ public DesiredGameStateBuilder MatchInfo(
+ Func build
+ )
{
var builder = new DesiredMatchInfoBuilder(_state.MatchInfo ?? new DesiredMatchInfoT());
build(builder);
@@ -24,7 +26,10 @@ public DesiredGameStateBuilder MatchInfo(Func
/// Modify the desired car state at the given index.
///
- public DesiredGameStateBuilder Car(int index, Func build)
+ public DesiredGameStateBuilder Car(
+ int index,
+ Func build
+ )
{
while (_state.CarStates.Count <= index)
{
@@ -39,8 +44,10 @@ public DesiredGameStateBuilder Car(int index, Func
/// Modify the desired cars at the given indices.
///
- public DesiredGameStateBuilder Cars(IEnumerable indices,
- Func build)
+ public DesiredGameStateBuilder Cars(
+ IEnumerable indices,
+ Func build
+ )
{
foreach (var index in indices)
{
@@ -54,11 +61,14 @@ public DesiredGameStateBuilder Cars(IEnumerable indices,
}
return this;
}
-
+
///
/// Modify the desired ball state at the given index.
///
- public DesiredGameStateBuilder Ball(int index, Func build)
+ public DesiredGameStateBuilder Ball(
+ int index,
+ Func build
+ )
{
while (_state.BallStates.Count <= index)
{
@@ -71,10 +81,12 @@ public DesiredGameStateBuilder Ball(int index, Func
- /// Modify the desired balls at the given indices.
+ /// Modify the desired balls at the given indices.
///
- public DesiredGameStateBuilder Balls(IEnumerable indices,
- Func build)
+ public DesiredGameStateBuilder Balls(
+ IEnumerable indices,
+ Func build
+ )
{
foreach (var index in indices)
{
diff --git a/RLBot/GameState/DesiredMatchInfoBuilder.cs b/RLBot/GameState/DesiredMatchInfoBuilder.cs
index 52c3af0..601eb64 100644
--- a/RLBot/GameState/DesiredMatchInfoBuilder.cs
+++ b/RLBot/GameState/DesiredMatchInfoBuilder.cs
@@ -5,7 +5,7 @@ namespace RLBot.GameState;
public readonly struct DesiredMatchInfoBuilder(DesiredMatchInfoT matchInfo)
{
///
- /// Set the world gravity z.
+ /// Set the world gravity z.
///
public DesiredMatchInfoBuilder GravityZ(float value)
{
diff --git a/RLBot/Manager/Bot.cs b/RLBot/Manager/Bot.cs
deleted file mode 100644
index bc648be..0000000
--- a/RLBot/Manager/Bot.cs
+++ /dev/null
@@ -1,267 +0,0 @@
-using Microsoft.Extensions.Logging;
-using RLBot.Flat;
-using RLBot.GameState;
-using RLBot.Util;
-
-namespace RLBot.Manager;
-
-public abstract class Bot
-{
- public Logging Logger = new("Bot", LogLevel.Information);
-
- public int Team { get; private set; } = -1;
- public int Index { get; private set; } = -1;
- public string Name { get; private set; } = "UnknownBot";
- public int PlayerId { get; private set; } = -1;
-
- public MatchConfigurationT MatchConfig { get; private set; } = new();
- public FieldInfoT FieldInfo { get; private set; } = new();
- public BallPredictionT BallPrediction { get; private set; } = new();
-
- public readonly Renderer Renderer;
-
- private bool _initializedBot = false;
- private bool _hasMatchConfig = false;
- private bool _hasFieldInfo = false;
- private bool _hasPlayerMapping = false;
-
- private readonly RLBotInterface _rlbotInterface;
- private GamePacketT? _latestPacket;
- private BallPredictionT _latestPrediction = new();
-
- public Bot(string? defaultAgentId = null)
- {
- string? agentId =
- Environment.GetEnvironmentVariable("RLBOT_AGENT_ID") ?? defaultAgentId;
-
- if (agentId 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= 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."
- );
- }
-
- _rlbotInterface = new RLBotInterface(agentId, logger: Logger);
- _rlbotInterface.OnMatchConfigCallback += HandleMatchConfig;
- _rlbotInterface.OnFieldInfoCallback += HandleFieldInfo;
- _rlbotInterface.OnMatchCommunicationCallback += HandleMatchCommunication;
- _rlbotInterface.OnBallPredictionCallback += HandleBallPrediction;
- _rlbotInterface.OnControllableTeamInfoCallback += HandleControllableTeamInfo;
- _rlbotInterface.OnGamePacketCallback += HandleGamePacket;
-
- Renderer = new Renderer(_rlbotInterface);
- }
-
- private void TryInitialize()
- {
- if (_initializedBot || !_hasMatchConfig || !_hasFieldInfo || !_hasPlayerMapping)
- return;
-
- foreach (PlayerConfigurationT player in MatchConfig.PlayerConfigurations)
- {
- if (player.PlayerId == PlayerId)
- {
- Name = player.Variety.AsCustomBot().Name;
- Logger = new Logging(Name, LogLevel.Information);
- break;
- }
- }
-
- try
- {
- Initialize();
- }
- catch (Exception e)
- {
- Logger.LogCritical(
- "Bot {0} failed to initialize due the following error: {1}",
- Name,
- e
- );
- return;
- }
-
- _initializedBot = true;
- _rlbotInterface.SendInitComplete();
- }
-
- public virtual void Initialize() { }
-
- private void HandleMatchConfig(MatchConfigurationT matchConfig)
- {
- MatchConfig = matchConfig;
- _hasMatchConfig = true;
- TryInitialize();
- }
-
- private void HandleFieldInfo(FieldInfoT fieldInfo)
- {
- FieldInfo = fieldInfo;
- _hasFieldInfo = true;
- TryInitialize();
- }
-
- private void HandleMatchCommunication(MatchCommT matchComm) =>
- HandleMatchComm(
- (int)matchComm.Index,
- (int)matchComm.Team,
- matchComm.Content,
- matchComm.Display,
- matchComm.TeamOnly
- );
-
- public virtual void HandleMatchComm(
- int Index,
- int Team,
- List Content,
- string? Display,
- bool teamOnly
- ) { }
-
- public void SendMatchComm(
- int Index,
- int Team,
- List Content,
- string? Display = null,
- bool teamOnly = false
- )
- {
- _rlbotInterface.SendMatchComm(
- new MatchCommT
- {
- Index = (uint)Index,
- Team = (uint)Team,
- Content = Content,
- Display = Display,
- TeamOnly = teamOnly,
- }
- );
- }
-
- private void HandleBallPrediction(BallPredictionT ballPrediction) =>
- _latestPrediction = ballPrediction;
-
- private void HandleControllableTeamInfo(ControllableTeamInfoT controllableTeamInfo)
- {
- Team = (int)controllableTeamInfo.Team;
- var controllable = controllableTeamInfo.Controllables[0];
- Index = (int)controllable.Index;
- PlayerId = controllable.Identifier;
- _hasPlayerMapping = true;
-
- TryInitialize();
- }
-
- private void HandleGamePacket(GamePacketT gamePacket) => _latestPacket = gamePacket;
-
- private void ProcessPacket(GamePacketT packet)
- {
- if (packet.Players.Count <= Index)
- {
- return;
- }
-
- BallPrediction = _latestPrediction;
- ControllerStateT controller;
-
- try
- {
- controller = GetOutput(packet);
- }
- catch (Exception e)
- {
- Logger.LogError(
- "Bot {0} encountered an error while processing game packet: {1}",
- Name,
- e
- );
- return;
- }
-
- var playerInput = new PlayerInputT
- {
- PlayerIndex = (uint)Index,
- ControllerState = controller,
- };
- _rlbotInterface.SendPlayerInput(playerInput);
- }
-
- public void Run(bool wantsMatchCommunications = true, bool wantsBallPredictions = true)
- {
- int rlbotServerPort = int.Parse(
- Environment.GetEnvironmentVariable("RLBOT_SERVER_PORT") ?? RLBotInterface.DEFAULT_RLBOT_SERVER_PORT.ToString()
- );
-
- try
- {
- _rlbotInterface.Connect(
- wantsMatchCommunications,
- wantsBallPredictions,
- rlbotServerPort: rlbotServerPort
- );
-
- while (true)
- {
- var res = _rlbotInterface.HandleIncomingMessages(
- 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);
- _latestPacket = null;
- }
- continue;
- }
- }
- }
- catch (Exception e)
- {
- Logger.LogCritical("An error occured while running the bot:\n{0}", e);
- return;
- }
- finally
- {
- Retire();
- }
- }
-
- public void SetLoadout(SetLoadoutT setLoadout) =>
- _rlbotInterface.SendSetLoadout(setLoadout);
-
- public void SetGameState(
- Dictionary? balls = null,
- Dictionary? cars = null,
- DesiredMatchInfoT? matchInfo = null,
- List? commands = null
- )
- {
- var gameState = GameStateExt.FillDesiredGameState(balls, cars, matchInfo, commands);
- _rlbotInterface.SendGameState(gameState);
- }
-
- ///
- /// Modify the current game state using a builder pattern.
- ///
- public DesiredGameStateBuilder GameStateBuilder()
- {
- return new DesiredGameStateBuilder(_rlbotInterface);
- }
-
- public virtual void Retire() { }
-
- public abstract ControllerStateT GetOutput(GamePacketT packet);
-}
diff --git a/RLBot/Manager/Match.cs b/RLBot/Manager/Match.cs
deleted file mode 100644
index da7ca20..0000000
--- a/RLBot/Manager/Match.cs
+++ /dev/null
@@ -1,164 +0,0 @@
-using System.Diagnostics;
-using Microsoft.Extensions.Logging;
-using RLBot.Flat;
-using RLBot.GameState;
-using RLBot.Util;
-
-namespace RLBot.Manager;
-
-public class Match
-{
- private readonly Logging _logger = new Logging("Match", LogLevel.Information);
-
- public GamePacketT? Packet { get; private set; }
- private Process? _rlbotServerProcess;
- private int _rlbotServerPort = RLBotInterface.DEFAULT_RLBOT_SERVER_PORT;
- private bool _initialized = false;
-
- private string? _mainExecutablePath;
- private string _mainExecutableName = OsConstants.MainExecutableName;
-
- private RLBotInterface _rlbotInterface;
-
- public Match(
- string? mainExecutablePath = null,
- string? mainExecutableName = null
- )
- {
- _mainExecutablePath = mainExecutablePath;
- if (mainExecutableName != null)
- _mainExecutableName = mainExecutableName;
-
- _rlbotInterface = new RLBotInterface("", logger: _logger);
- _rlbotInterface.OnGamePacketCallback += PacketReporter;
- }
-
- public void EnsureServerStarted()
- {
- _logger.LogWarning("The C# RLBot Interface cannot ensure the RLBot Server is running. The feature has yet to be implemented.");
-
- // self.rlbot_server_process, self.rlbot_server_port = gateway.find_server_process(
- // self.main_executable_name
- // )
-
- if (_rlbotServerProcess != null)
- {
- _logger.LogInformation("Already have {0} running!", _mainExecutableName);
- return;
- }
-
- if (_mainExecutablePath == null)
- _mainExecutablePath = Directory.GetCurrentDirectory();
-
- // rlbot_server_process, self.rlbot_server_port = gateway.launch(
- // self.main_executable_path,
- // self.main_executable_name,
- // )
- // self.rlbot_server_process = psutil.Process(rlbot_server_process.pid)
-
- // self.logger.info(
- // "Started %s with process id %s",
- // self.main_executable_name,
- // self.rlbot_server_process.pid,
- // )
- }
-
- private void PacketReporter(GamePacketT packet) => Packet = packet;
-
- public void Connect(
- bool wantsMatchCommunications,
- bool wantsBallPredictions,
- bool closeAfterMatch = true,
- int rlbotServerPort = RLBotInterface.DEFAULT_RLBOT_SERVER_PORT
- ) =>
- _rlbotInterface.Connect(
- wantsMatchCommunications,
- wantsBallPredictions,
- closeAfterMatch,
- rlbotServerPort
- );
-
- public void WaitForFirstPacket()
- {
- while (
- Packet == null
- || Packet.MatchInfo.MatchPhase == MatchPhase.Inactive
- || Packet.MatchInfo.MatchPhase == MatchPhase.Ended
- )
- Thread.Sleep(100);
- }
-
- public void StartMatch(MatchConfigurationT config, bool waitForStart = true)
- {
- EnsureGameConnection();
-
- _rlbotInterface.StartMatch(config);
-
- if (!_initialized)
- {
- _rlbotInterface.SendInitComplete();
- _initialized = true;
- }
-
- if (waitForStart)
- {
- WaitForFirstPacket();
- _logger.LogInformation("Match has started.");
- }
- }
-
- public void StartMatch(string configPath, bool waitForStart = true)
- {
- EnsureGameConnection();
-
- _rlbotInterface.StartMatch(configPath);
-
- if (!_initialized)
- {
- _rlbotInterface.SendInitComplete();
- _initialized = true;
- }
-
- if (waitForStart)
- {
- WaitForFirstPacket();
- _logger.LogInformation("Match has started.");
- }
- }
-
- private void EnsureGameConnection()
- {
- if (!_rlbotInterface.IsConnected)
- {
- _rlbotInterface.Connect(
- wantsMatchCommunications: false,
- wantsBallPredictions: false,
- closeBetweenMatches: false
- );
- _rlbotInterface.Run(backgroundThread: true);
- }
- }
-
- ///
- /// Modify the current game state using a builder pattern.
- ///
- public DesiredGameStateBuilder GameStateBuilder()
- {
- return new DesiredGameStateBuilder(_rlbotInterface);
- }
-
- public void SetGameState(
- Dictionary? balls = null,
- Dictionary? cars = null,
- DesiredMatchInfoT? matchInfo = null,
- List? commands = null
- )
- {
- var gameState = GameStateExt.FillDesiredGameState(balls, cars, matchInfo, commands);
- _rlbotInterface.SendGameState(gameState);
- }
-
- public void Disconnect() => _rlbotInterface.Disconnect();
-
- public void StopMatch() => _rlbotInterface.StopMatch();
-}
diff --git a/RLBot/Manager/Script.cs b/RLBot/Manager/Script.cs
deleted file mode 100644
index 431827c..0000000
--- a/RLBot/Manager/Script.cs
+++ /dev/null
@@ -1,243 +0,0 @@
-using Microsoft.Extensions.Logging;
-using RLBot.Flat;
-using RLBot.GameState;
-using RLBot.Util;
-
-namespace RLBot.Manager;
-
-public abstract class Script
-{
- public Logging Logger = new("Script", LogLevel.Information);
-
- public int Index { get; private set; }
- public string Name { get; private set; } = "UnknownScript";
-
- public MatchConfigurationT MatchConfig { get; private set; } = new();
- public FieldInfoT FieldInfo { get; private set; } = new();
- public BallPredictionT BallPrediction { get; private set; } = new();
-
- public readonly Renderer Renderer;
-
- private bool _initialized = false;
- private bool _hasMatchSettings = false;
- private bool _hasFieldInfo = false;
-
- private readonly RLBotInterface _rlbotInterface;
- private GamePacketT? _latestPacket;
- private BallPredictionT _latestPrediction = new();
-
- public Script(string? defaultAgentId = null)
- {
- string? agentId =
- Environment.GetEnvironmentVariable("RLBOT_AGENT_ID") ?? defaultAgentId;
-
- if (agentId is null)
- {
- Logger.LogCritical(
- "Environment variable RLBOT_AGENT_ID is not set and no default agent id is passed to "
- + "the constructor of the script. If you are starting your script manually, please set it "
- + "manually, e.g. `RLBOT_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."
- );
- }
-
- _rlbotInterface = new RLBotInterface(agentId, logger: Logger);
- _rlbotInterface.OnMatchConfigCallback += HandleMatchConfig;
- _rlbotInterface.OnFieldInfoCallback += HandleFieldInfo;
- _rlbotInterface.OnMatchCommunicationCallback += HandleMatchCommunication;
- _rlbotInterface.OnBallPredictionCallback += HandleBallPrediction;
- _rlbotInterface.OnGamePacketCallback += HandleGamePacket;
-
- Renderer = new Renderer(_rlbotInterface);
- }
-
- private void TryInitialize()
- {
- if (_initialized || !_hasMatchSettings || !_hasFieldInfo)
- return;
-
- try
- {
- Initialize();
- }
- catch (Exception e)
- {
- Logger.LogCritical(
- "Script {0} failed to initialize due the following error: {1}",
- Name,
- e
- );
- throw new Exception("Failed to initialize script.", e);
- }
-
- _initialized = true;
- _rlbotInterface.SendInitComplete();
- }
-
- public virtual void Initialize() { }
-
- private void HandleMatchConfig(MatchConfigurationT matchConfig)
- {
- MatchConfig = matchConfig;
-
- for (int i = 0; i < matchConfig.ScriptConfigurations.Count; i++)
- {
- var script = matchConfig.ScriptConfigurations[i];
- if (script.AgentId == _rlbotInterface.AgentId)
- {
- Index = i;
- Name = script.Name;
- _hasMatchSettings = true;
- }
- }
-
- if (!_hasMatchSettings)
- {
- Logger.LogWarning("Script with agent id '{}' did not find itself in the match settings", _rlbotInterface.AgentId);
- }
-
- TryInitialize();
- }
-
- private void HandleFieldInfo(FieldInfoT fieldInfo)
- {
- FieldInfo = fieldInfo;
- _hasFieldInfo = true;
- TryInitialize();
- }
-
- private void HandleMatchCommunication(MatchCommT matchComm) =>
- HandleMatchComm(
- (int)matchComm.Index,
- (int)matchComm.Team,
- matchComm.Content,
- matchComm.Display,
- matchComm.TeamOnly
- );
-
- public virtual void HandleMatchComm(
- int Index,
- int Team,
- List Content,
- string? Display,
- bool teamOnly
- ) { }
-
- public void SendMatchComm(
- int Index,
- int Team,
- List Content,
- string? Display = null,
- bool teamOnly = false
- )
- {
- _rlbotInterface.SendMatchComm(
- new MatchCommT
- {
- Index = (uint)Index,
- Team = (uint)Team,
- Content = Content,
- Display = Display,
- TeamOnly = teamOnly,
- }
- );
- }
-
- private void HandleBallPrediction(BallPredictionT ballPrediction) =>
- _latestPrediction = ballPrediction;
-
- private void HandleGamePacket(GamePacketT gamePacket) => _latestPacket = gamePacket;
-
- private void ProcessPacket(GamePacketT packet)
- {
- BallPrediction = _latestPrediction;
-
- try
- {
- HandlePacket(packet);
- }
- catch (Exception e)
- {
- Logger.LogError(
- "Script {0} encountered an error while processing game packet: {1}",
- Name,
- e
- );
- }
- }
-
- public void Run(bool wantsMatchCommunications = true, bool wantsBallPredictions = true)
- {
- int rlbotServerPort = int.Parse(
- Environment.GetEnvironmentVariable("RLBOT_SERVER_PORT") ?? RLBotInterface.DEFAULT_RLBOT_SERVER_PORT.ToString()
- );
-
- try
- {
- _rlbotInterface.Connect(
- wantsMatchCommunications,
- wantsBallPredictions,
- rlbotServerPort: rlbotServerPort
- );
-
- while (true)
- {
- var res = _rlbotInterface.HandleIncomingMessages(
- 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);
- _latestPacket = null;
- }
- continue;
- }
- }
- }
- catch (Exception e)
- {
- Logger.LogCritical("An error occured while running the bot:\n{0}", e);
- }
- finally
- {
- Retire();
- }
- }
-
- public void SetLoadout(SetLoadoutT setLoadout) =>
- _rlbotInterface.SendSetLoadout(setLoadout);
-
- ///
- /// Modify the current game state using a builder pattern.
- ///
- public DesiredGameStateBuilder GameStateBuilder()
- {
- return new DesiredGameStateBuilder(_rlbotInterface);
- }
-
- public void SetGameState(
- Dictionary? balls = null,
- Dictionary? cars = null,
- DesiredMatchInfoT? matchInfo = null,
- List? commands = null
- )
- {
- var gameState = GameStateExt.FillDesiredGameState(balls, cars, matchInfo, commands);
- _rlbotInterface.SendGameState(gameState);
- }
-
- public virtual void Retire() { }
-
- public abstract void HandlePacket(GamePacketT packet);
-}
diff --git a/RLBot/RLBotInterface.cs b/RLBot/RLBotInterface.cs
index 88fef50..4214cf5 100644
--- a/RLBot/RLBotInterface.cs
+++ b/RLBot/RLBotInterface.cs
@@ -3,6 +3,7 @@
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using RLBot.Flat;
+using RLBot.GameState;
using RLBot.Util;
namespace RLBot;
@@ -11,16 +12,19 @@ public class RLBotInterface
{
public const int DEFAULT_RLBOT_SERVER_PORT = 23234;
- public bool IsConnected { get; private set; } = false;
- private bool _running = false;
+ private readonly Logging _logger = new Logging(
+ nameof(RLBotInterface),
+ LogLevel.Information
+ );
+ public bool IsConnected { get; private set; } = false;
private readonly int _connectionTimeout;
- private readonly Logging _logger;
private readonly TcpClient _client = new();
private SpecStreamReader? _socketSpecReader;
private SpecStreamWriter? _socketSpecWriter;
- public readonly string AgentId;
+ public bool IsRunning { get; private set; } = false;
+
public event Action OnConnectCallback = delegate { };
public event Action OnGamePacketCallback = delegate { };
public event Action OnFieldInfoCallback = delegate { };
@@ -31,20 +35,9 @@ public class RLBotInterface
public event Action OnRenderingStatusCallback = delegate { };
public event Action OnAnyMessageCallback = delegate { };
- public RLBotInterface(string agentId, int connectionTimeout = 120, Logging? logger = null)
+ public RLBotInterface(int connectionTimeout = 120)
{
- AgentId = agentId;
_connectionTimeout = connectionTimeout;
-
- if (logger is null)
- {
- _logger = new Logging("Interface", LogLevel.Information);
- }
- else
- {
- _logger = logger;
- }
-
_client.NoDelay = true;
}
@@ -83,6 +76,25 @@ public void SendGameState(DesiredGameStateT gameState)
SendFlatBuffer(InterfaceMessageUnion.FromDesiredGameState(gameState));
}
+ public void SendGameState(
+ Dictionary? balls = null,
+ Dictionary? cars = null,
+ DesiredMatchInfoT? matchInfo = null,
+ List? commands = null
+ )
+ {
+ var gameState = GameStateExt.FillDesiredGameState(balls, cars, matchInfo, commands);
+ SendGameState(gameState);
+ }
+
+ ///
+ /// Modify the current game state using a builder pattern.
+ ///
+ public DesiredGameStateBuilder GameStateBuilder()
+ {
+ return new DesiredGameStateBuilder(this);
+ }
+
public void SendRenderGroup(RenderGroupT renderGroup)
{
SendFlatBuffer(InterfaceMessageUnion.FromRenderGroup(renderGroup));
@@ -112,18 +124,40 @@ public void StartMatch(string matchConfigPath)
var attr = File.GetAttributes(matchConfigPath);
if (attr.HasFlag(FileAttributes.Directory))
throw new ArgumentException(
- $"Expected path to file, but it is a directory: {matchConfigPath}"
+ $"Expected path to file, but found a directory: {matchConfigPath}"
);
var startCommand = new StartCommandT { ConfigPath = matchConfigPath };
SendFlatBuffer(InterfaceMessageUnion.FromStartCommand(startCommand));
+ SendInitComplete();
+ }
+
+ public void ConnectAsMatchHost(
+ bool wantsBallPredictions = false,
+ bool wantsMatchCommunications = false
+ )
+ {
+ Connect("", wantsBallPredictions, wantsMatchCommunications, true);
+ }
+
+ public void Connect(
+ string agentId,
+ bool wantsMatchCommunications,
+ bool wantsBallPredictions,
+ bool outliveMatches
+ )
+ {
+ var portRaw = Environment.GetEnvironmentVariable("RLBOT_SERVER_PORT");
+ var port = portRaw == null ? DEFAULT_RLBOT_SERVER_PORT : int.Parse(portRaw);
+ Connect(agentId, wantsMatchCommunications, wantsBallPredictions, outliveMatches, port);
}
public void Connect(
+ string agentId,
bool wantsMatchCommunications,
bool wantsBallPredictions,
- bool closeBetweenMatches = true,
- int rlbotServerPort = DEFAULT_RLBOT_SERVER_PORT
+ bool outliveMatches,
+ int rlbotServerPort
)
{
if (IsConnected)
@@ -160,7 +194,8 @@ public void Connect(
{
nextWarning *= 2;
_logger.LogWarning(
- "Connection is being refused/aborted. Trying again ..."
+ "Failing to connect to RLBot on port {}. Trying again ...",
+ rlbotServerPort
);
}
}
@@ -168,20 +203,15 @@ public void Connect(
if (!IsConnected)
{
- throw new SocketException(
- (int)SocketError.ConnectionRefused,
- "Connection was refused/aborted repeatedly! "
- + "Ensure that Rocket League and the RLBotServer is running. "
- + "Try calling `ensure_server_started()` before connecting."
+ throw new TimeoutException(
+ "Failed to establish connection. Ensure that the RLBotServer is running."
);
}
}
- catch (TimeoutException e)
+ catch (Exception e)
{
throw new TimeoutException(
- "Took too long to connect to the RLBot! "
- + "Ensure that Rocket League and the RLBotServer is running."
- + "Try calling `ensure_server_started()` before connecting.",
+ "Failed to establish connection. Ensure that the RLBotServer is running.",
e
);
}
@@ -200,17 +230,17 @@ public void Connect(
localIpEndPoint!.Port
);
- OnConnectCallback();
-
var connectionSettings = new ConnectionSettingsT
{
- AgentId = AgentId,
+ AgentId = agentId,
WantsBallPredictions = wantsBallPredictions,
WantsComms = wantsMatchCommunications,
- CloseBetweenMatches = closeBetweenMatches,
+ CloseBetweenMatches = !outliveMatches,
};
-
+
SendFlatBuffer(InterfaceMessageUnion.FromConnectionSettings(connectionSettings));
+
+ OnConnectCallback();
}
public void Run(bool backgroundThread = false)
@@ -220,7 +250,7 @@ public void Run(bool backgroundThread = false)
throw new Exception("Connection has not been established");
}
- if (_running)
+ if (IsRunning)
{
throw new Exception("Message handling is already running");
}
@@ -231,15 +261,20 @@ public void Run(bool backgroundThread = false)
}
else
{
- _running = true;
- while (_running && IsConnected)
- _running =
- HandleIncomingMessages(blocking: true) != MsgHandlingResult.Terminated;
+ IsRunning = true;
+ while (IsRunning && IsConnected)
+ IsRunning =
+ HandleNextIncomingMessage(blocking: true) != MsgHandlingResult.Terminated;
- _running = false;
+ IsRunning = false;
}
}
+ public void StopRunning()
+ {
+ IsRunning = false;
+ }
+
public enum MsgHandlingResult
{
Terminated,
@@ -247,7 +282,7 @@ public enum MsgHandlingResult
MoreMsgsQueued,
}
- public MsgHandlingResult HandleIncomingMessages(bool blocking = false)
+ public MsgHandlingResult HandleNextIncomingMessage(bool blocking = false)
{
if (!IsConnected)
{
@@ -262,14 +297,14 @@ public MsgHandlingResult HandleIncomingMessages(bool blocking = false)
try
{
- return HandleIncomingMessage(packet)
+ return HandleMessage(packet)
? MsgHandlingResult.MoreMsgsQueued
: MsgHandlingResult.Terminated;
}
catch (Exception e)
{
_logger.LogError(
- "Unexpected error while handling message of type {0}: {1}",
+ "Unexpected error while handling message of type {}: {}",
packet.Message.Type,
e
);
@@ -287,7 +322,7 @@ public MsgHandlingResult HandleIncomingMessages(bool blocking = false)
}
}
- private bool HandleIncomingMessage(CorePacketT packet)
+ private bool HandleMessage(CorePacketT packet)
{
OnAnyMessageCallback(packet);
@@ -317,7 +352,8 @@ private bool HandleIncomingMessage(CorePacketT packet)
OnBallPredictionCallback(ballPrediction);
break;
case CoreMessage.ControllableTeamInfo:
- ControllableTeamInfoT controllableTeamInfo = packet.Message.AsControllableTeamInfo();
+ ControllableTeamInfoT controllableTeamInfo =
+ packet.Message.AsControllableTeamInfo();
OnControllableTeamInfoCallback(controllableTeamInfo);
break;
case CoreMessage.RenderingStatus:
@@ -325,7 +361,10 @@ private bool HandleIncomingMessage(CorePacketT packet)
OnRenderingStatusCallback(renderingStatus);
break;
default:
- _logger.LogWarning("Received message of unknown type: {0}", packet.Message.Type);
+ _logger.LogWarning(
+ "Received message of unknown type: {0}",
+ packet.Message.Type
+ );
break;
}
@@ -340,11 +379,13 @@ public void Disconnect()
return;
}
- _socketSpecWriter!.Write(InterfaceMessageUnion.FromDisconnectSignal(new DisconnectSignalT()));
+ _socketSpecWriter!.Write(
+ InterfaceMessageUnion.FromDisconnectSignal(new DisconnectSignalT())
+ );
_socketSpecWriter.Send();
var timeout = 5.0;
- while (_running && timeout > 0)
+ while (IsRunning && timeout > 0)
{
Thread.Sleep(100);
timeout -= 0.1;
@@ -353,11 +394,11 @@ public void Disconnect()
if (timeout <= 0)
{
_logger.LogCritical("RLBot is not responding to our disconnect request!?");
- _running = false;
+ IsRunning = false;
}
Debug.Assert(
- !_running,
+ !IsRunning,
"Disconnect request or timeout should have set _running to False"
);
diff --git a/RLBot/Util/DataType.cs b/RLBot/Util/DataType.cs
deleted file mode 100644
index 1aab876..0000000
--- a/RLBot/Util/DataType.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace RLBot.Util;
-
-/**
- * https://wiki.rlbot.org/framework/sockets-specification/#data-types
- */
-public enum DataType : ushort
-{
- None,
-
- ///
- /// Arrives at a high rate according to https://wiki.rlbot.org/botmaking/tick-rate/ except
- /// "desired tick rate" is not relevant here
- ///
- GamePacket,
-
- ///
- /// Sent once when a match starts, or when you first connect.
- ///
- FieldInfo,
-
- ///
- /// Sent once when a match starts, or when you first connect.
- ///
- StartCommand,
- MatchSettings,
- PlayerInput,
- DesiredGameState,
- RenderGroup,
- RemoveRenderGroup,
- MatchComms,
- BallPrediction,
-
- ///
- /// Clients must send this after connecting to the socket.
- ///
- ConnectionSettings,
-
- ///
- /// used to end a match and shut down bots (optionally the server as well)
- ///
- StopCommand,
-
- ///
- /// Use to dynamically set the loadout of a bot
- ///
- SetLoadout,
-
- ///
- /// Indicates that a connection is ready to receive `GameTickPacket`s
- ///
- InitComplete,
- ControllableTeamInfo,
-}
diff --git a/RLBot/Util/SocketSpecStreamReader.cs b/RLBot/Util/SocketSpecStreamReader.cs
index 30d9887..09f63fc 100644
--- a/RLBot/Util/SocketSpecStreamReader.cs
+++ b/RLBot/Util/SocketSpecStreamReader.cs
@@ -27,7 +27,7 @@ public CorePacket ReadOne()
_bufferedStream.ReadExactly(_ushortReader);
payloadSize = ReadBigEndian(_ushortReader);
_bufferedStream.ReadExactly(_payloadReader, 0, payloadSize);
-
+
ByteBuffer byteBuffer = new(_payloadReader, 0);
return CorePacket.GetRootAsCorePacket(byteBuffer);
}
diff --git a/RLBot/Util/SocketSpecStreamWriter.cs b/RLBot/Util/SocketSpecStreamWriter.cs
index 28c6cc9..f2ff806 100644
--- a/RLBot/Util/SocketSpecStreamWriter.cs
+++ b/RLBot/Util/SocketSpecStreamWriter.cs
@@ -12,6 +12,7 @@ class SpecStreamWriter(Stream stream)
{
private static readonly ILogger Logger = Logging.GetLogger("SpecStreamWriter");
+ private readonly object _lock = new();
private readonly FlatBufferBuilder _messageBuilder = new(1 << 12);
private readonly byte[] _messageBuffer = new byte[2 + ushort.MaxValue];
@@ -23,34 +24,37 @@ private static void WriteBigEndian(ushort value, byte[] buffer)
internal void Write(InterfaceMessageUnion message)
{
- var packet = new InterfacePacketT { Message = message };
-
- _messageBuilder.Clear();
- _messageBuilder.Finish(InterfacePacket.Pack(_messageBuilder, packet).Value);
- ArraySegment payload = _messageBuilder.DataBuffer.ToArraySegment(
- _messageBuilder.DataBuffer.Position,
- _messageBuilder.DataBuffer.Length - _messageBuilder.DataBuffer.Position
- );
-
- if (payload.Count > ushort.MaxValue)
+ lock (_lock)
{
- // Can't send if the message size is bigger than our header can describe.
- Logger.LogError(
- $"Cannot send message because size of {payload.Count} cannot be described by a ushort."
+ var packet = new InterfacePacketT { Message = message };
+
+ _messageBuilder.Clear();
+ _messageBuilder.Finish(InterfacePacket.Pack(_messageBuilder, packet).Value);
+ ArraySegment payload = _messageBuilder.DataBuffer.ToArraySegment(
+ _messageBuilder.DataBuffer.Position,
+ _messageBuilder.DataBuffer.Length - _messageBuilder.DataBuffer.Position
);
- return;
- }
- if (payload.Count == 0 || payload.Array == null)
- {
- Logger.LogWarning("Cannot send an empty message.");
- return;
+ if (payload.Count > ushort.MaxValue)
+ {
+ // Can't send if the message size is bigger than our header can describe.
+ Logger.LogError(
+ $"Cannot send message because size of {payload.Count} cannot be described by a ushort."
+ );
+ return;
+ }
+
+ if (payload.Count == 0 || payload.Array == null)
+ {
+ Logger.LogWarning("Cannot send an empty message.");
+ return;
+ }
+
+ WriteBigEndian((ushort)payload.Count, _messageBuffer);
+ Array.Copy(payload.Array, payload.Offset, _messageBuffer, 2, payload.Count);
+
+ stream.Write(_messageBuffer, 0, 2 + payload.Count);
}
-
- WriteBigEndian((ushort)payload.Count, _messageBuffer);
- Array.Copy(payload.Array, payload.Offset, _messageBuffer, 2, payload.Count);
-
- stream.Write(_messageBuffer, 0, 2 + payload.Count);
}
internal void Send() => stream.Flush();
diff --git a/Tests/Atba/Atba.cs b/Tests/Atba/Atba.cs
index 57d8e94..05a9da9 100644
--- a/Tests/Atba/Atba.cs
+++ b/Tests/Atba/Atba.cs
@@ -1,36 +1,68 @@
using Microsoft.Extensions.Logging;
using MyBot.Math;
+using RLBot;
using RLBot.Flat;
using RLBot.Manager;
using RLBot.Util;
-using Vector3 = System.Numerics.Vector3;
-using Color = System.Drawing.Color;
-Atba bot = new();
-bot.Run();
+RLBotInterface rlbot = new();
+SingleThreadBotManager manager = new(
+ rlbot,
+ "test/csharp_atba",
+ (rlbot, index, team, name, agentId, matchConfig, fieldInfo) =>
+ new Atba(rlbot, index, team, name, agentId, matchConfig, fieldInfo)
+);
+manager.Run();
-class Atba : Bot
+internal class Atba : IBot
{
- public Atba()
- : base("test/csharp_atba") { }
+ private readonly Logging _logger = new Logging(nameof(Atba), LogLevel.Information);
- public override void Initialize()
+ public readonly RLBotInterface Rlbot;
+ public readonly int Index;
+ public readonly uint Team;
+ public readonly string Name;
+ public readonly string AgentId;
+ public readonly MatchConfigurationT MatchConfig;
+ public readonly FieldInfoT FieldInfo;
+
+ public readonly Renderer Renderer;
+
+ public Atba(
+ RLBotInterface rlbot,
+ int index,
+ uint team,
+ string name,
+ string agentId,
+ MatchConfigurationT matchConfig,
+ FieldInfoT fieldInfo
+ )
{
- Logger.LogInformation("Initializing agent!");
+ Rlbot = rlbot;
+ Index = index;
+ Team = team;
+ Name = name;
+ AgentId = agentId;
+ MatchConfig = matchConfig;
+ FieldInfo = fieldInfo;
+ Renderer = new(rlbot);
+
+ _logger.LogInformation("Initializing agent!");
- int numBoostPads = FieldInfo.BoostPads.Count;
- Logger.LogInformation($"There are {numBoostPads} boost pads on the field.");
+ int numBoostPads = fieldInfo.BoostPads.Count;
+ _logger.LogInformation($"There are {numBoostPads} boost pads on the field.");
}
- public override ControllerStateT GetOutput(GamePacketT packet)
+ public PlayerLoadoutT? GetInitialLoadout()
+ {
+ return null; // Use the loadout declared in bot.toml
+ }
+
+ public ControllerStateT GetOutput(GamePacketT packet, BallPredictionT? ballPrediction)
{
ControllerStateT controller = new();
- if (
- packet.MatchInfo.MatchPhase != MatchPhase.Active
- && packet.MatchInfo.MatchPhase != MatchPhase.Kickoff
- || packet.Balls.Count == 0
- )
+ if (packet.Balls.Count == 0)
return controller;
Vec2 ballLocation = new(packet.Balls[0].Physics.Location);
@@ -46,11 +78,18 @@ public override ControllerStateT GetOutput(GamePacketT packet)
controller.Throttle = 1;
controller.Jump = packet.MatchInfo.LastSpectated == Index;
-
+
Renderer.Begin();
- Renderer.DrawLine3D(myCar.Physics.Location.ToSysVec(), packet.Balls[0].Physics.Location.ToSysVec(), Color.White);
+ Renderer.DrawLine3D(
+ myCar.Physics.Location.ToSysVec(),
+ packet.Balls[0].Physics.Location.ToSysVec()
+ );
Renderer.End();
return controller;
}
+
+ public void OnMatchCommReceived(MatchCommT msg) { }
+
+ public void OnRetire() { }
}
diff --git a/Tests/RunMatch/Program.cs b/Tests/RunMatch/Program.cs
index 951c34e..de91959 100644
--- a/Tests/RunMatch/Program.cs
+++ b/Tests/RunMatch/Program.cs
@@ -1,4 +1,4 @@
-using RLBot.Manager;
+using RLBot;
if (args.Length == 0)
{
@@ -6,14 +6,14 @@
return;
}
-string matchConfigPath = args[0];
-Match matchManager = new();
-matchManager.StartMatch(matchConfigPath);
+RLBotInterface rlbot = new RLBotInterface();
+rlbot.ConnectAsMatchHost();
+rlbot.StartMatch(args[0]);
-// wait
+// Wait
Console.WriteLine("\nPress enter to end the match: ");
Console.ReadLine();
-// end the match and disconnect
-matchManager.StopMatch();
-matchManager.Disconnect();
+// End the match and disconnect
+rlbot.StopMatch();
+rlbot.Disconnect();
diff --git a/Tests/TestScript/TestScript.cs b/Tests/TestScript/TestScript.cs
index 3e1328e..0afc91d 100644
--- a/Tests/TestScript/TestScript.cs
+++ b/Tests/TestScript/TestScript.cs
@@ -1,39 +1,75 @@
using Microsoft.Extensions.Logging;
+using RLBot;
using RLBot.Flat;
-using Vector3 = System.Numerics.Vector3;
using RLBot.Manager;
+using RLBot.Util;
+using Vector3 = System.Numerics.Vector3;
-TestScript script = new TestScript();
-script.Run();
+var manager = new ScriptManager(
+ new RLBotInterface(),
+ "test/csharp_script",
+ (rlbot, index, agentId, matchConfig, fieldInfo) =>
+ new TestScript(rlbot, index, agentId, matchConfig, fieldInfo)
+);
+manager.Run();
-class TestScript : Script
+class TestScript : IScript
{
+ private readonly Logging _logger = new Logging(nameof(TestScript), LogLevel.Information);
+
+ public readonly RLBotInterface Rlbot;
+ public readonly int Index;
+ public readonly string AgentId;
+ public readonly MatchConfigurationT MatchConfig;
+ public readonly FieldInfoT FieldInfo;
+
+ public readonly Renderer Renderer;
+
private float _next = 10f;
- public TestScript()
- : base("test/csharp_script")
+ public TestScript(
+ RLBotInterface rlbot,
+ int index,
+ string agentId,
+ MatchConfigurationT matchConfig,
+ FieldInfoT fieldInfo
+ )
{
- }
+ Rlbot = rlbot;
+ Index = index;
+ AgentId = agentId;
+ MatchConfig = matchConfig;
+ FieldInfo = fieldInfo;
+ Renderer = new Renderer(rlbot);
- public override void Initialize()
- {
- Logger.LogInformation("Test script initialized!");
+ _logger.LogInformation("Test script initialized!");
}
- public override void HandlePacket(GamePacketT packet)
+ public void ProcessPacket(GamePacketT packet, BallPredictionT? ballPred)
{
- if (packet.MatchInfo.SecondsElapsed < _next || packet.Balls.Count == 0)
+ if (
+ packet.MatchInfo.SecondsElapsed < _next
+ || packet.Balls.Count == 0
+ || packet.Players.Count == 0
+ )
return;
- // State setting
- GameStateBuilder()
- .Balls(Enumerable.Range(0, packet.Balls.Count), (i, c) => c
- .Location(Vector3.UnitZ * 93)
- .VelocityZ(packet.Balls[i].Physics.Velocity.Z + 1000f)
+ // Test state setting
+ Rlbot
+ .GameStateBuilder()
+ .Balls(
+ Enumerable.Range(0, packet.Balls.Count),
+ (i, c) =>
+ c.Location(Vector3.UnitZ * 93)
+ .VelocityZ(packet.Balls[i].Physics.Velocity.Z + 1000f)
)
.Car(1, c => c.Boost(100).RotationYaw(0))
.BuildAndSend();
_next = packet.MatchInfo.SecondsElapsed + 10f;
}
+
+ public void OnMatchCommReceived(MatchCommT msg) { }
+
+ public void OnRetire() { }
}