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() { } }