diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 487b227..3eb4fd5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,13 @@ name: Build -on: [ workflow_dispatch, push ] +on: + push: + branches: + - "*" + tags: + - "v*.*.*" + pull_request: + workflow_dispatch: jobs: build-windows: @@ -38,3 +45,23 @@ jobs: with: name: RLBotServer-ubuntu path: ./**/publish/RLBotServer + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [build-linux, build-windows] + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v4 + with: + merge-multiple: "true" + - name: Publish to GitHub Releases + uses: softprops/action-gh-release@v2 + with: + files: ./**/publish/RLBotServer* + generate_release_notes: true + body: | + Pre-built binaries that allows bots to interface with Rocket League via the RLBot v5 spec diff --git a/README.md b/README.md index d3d480c..f4fc543 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,13 @@ compiled binaries at `RLBotCS\bin\Release\net8.0`. ## Deployment -New deployment method is still TBD. +1. Ensure all changes are on the `master` branch +1. Create a new tag with the next version number - e.x. `git tag v0.1.0 -m "Core v0.1.0"` + - Preferably sign the tag too - `git tag -s v0.1.0 -m "Core v0.1.0"` +1. Push the tag - `git push --tags` +1. Wait for the Github Actions to build the release and upload it to the release page! + +Further deployment steps for automatic updates are still in progress. ## Maintenance @@ -35,11 +41,11 @@ This project uses the CSharpier formatter. You can run it with `dotnet csharpier ### Flatbuffers The Core project uses flatbuffers, which involves generating C# code based on a specification -file called `rlbot.fbs`. Find this in `RLBotCS/FlatBuffer` after it's been generated. +file called `rlbot.fbs`. Find this in `./FlatBuffer` after it's been generated. The [flatbuffers-schema](https://github.com/RLBot/flatbuffers-schema) submodule should be kept update to date: -- `cd RLBotCS/flatbuffers-schema` +- `cd flatbuffers-schema` - `git checkout main` - `git fetch` - `git pull` @@ -52,3 +58,10 @@ The `Bridge.dll` file in `RLBotCS/lib` is built from a _closed-source_ repositor It is maintained by RLBot developers who have signed an agreement with Psyonix to keep it private. The dll file is platform-independent and works for building the project on both Windows and Linux. + +### rl_ball_sym + +The native binaries that live in `RLBotCS/lib/rl_ball_sym` generate the ball prediction that core then distributes to bots & scripts that request it. +The `dll`/`so` are dynamically loaded at run time while developing core, and the `a`/`lib` files are statically linked during publishing. + +All source code and releases for building the dlls can be found at but the core of the code is a library that's published for anyone's use at . diff --git a/RLBotCS/Conversion/FlatToCommand.cs b/RLBotCS/Conversion/FlatToCommand.cs index 24b838b..d405863 100644 --- a/RLBotCS/Conversion/FlatToCommand.cs +++ b/RLBotCS/Conversion/FlatToCommand.cs @@ -18,6 +18,7 @@ private static string MapGameMode(GameMode gameMode) => GameMode.Rumble => "?game=TAGame.GameInfo_Items_TA", GameMode.Heatseeker => "?game=TAGame.GameInfo_GodBall_TA", GameMode.Gridiron => "?game=TAGame.GameInfo_Football_TA", + GameMode.Knockout => "?game=TAGame.GameInfo_KnockOut_TA", _ => throw new ArgumentOutOfRangeException(nameof(gameMode), gameMode, null) }; @@ -101,6 +102,8 @@ private static string MapBallType(BallTypeOption option) => BallTypeOption.Beachball => "Ball_BeachBall", BallTypeOption.Anniversary => "Ball_Anniversary", BallTypeOption.Haunted => "Ball_Haunted", + BallTypeOption.Ekin => "Ball_Ekin", + BallTypeOption.SpookyCube => "Ball_SpookyCube", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; @@ -113,6 +116,7 @@ private static string MapBallWeight(BallWeightOption option) => BallWeightOption.Super_Light => "SuperLightBall", BallWeightOption.Curve_Ball => "MagnusBall", BallWeightOption.Beach_Ball_Curve => "MagnusBeachBall", + BallWeightOption.Magnus_FutBall => "MagnusFutBallTest", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; @@ -121,6 +125,7 @@ private static string MapBallSize(BallSizeOption option) => { BallSizeOption.Default => "", BallSizeOption.Small => "SmallBall", + BallSizeOption.Medium => "MediumBall", BallSizeOption.Large => "BigBall", BallSizeOption.Gigantic => "GiantBall", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) @@ -133,6 +138,7 @@ private static string MapBallBounciness(BallBouncinessOption option) => BallBouncinessOption.Low => "LowBounciness", BallBouncinessOption.High => "HighBounciness", BallBouncinessOption.Super_High => "SuperBounciness", + BallBouncinessOption.LowishBounciness => "LowishBounciness", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; @@ -160,6 +166,7 @@ private static string MapRumble(RumbleOption option) => RumbleOption.Spike_Rush => "ItemsModeRugby", RumbleOption.Haunted_Ball_Beam => "ItemsModeHauntedBallBeam", RumbleOption.Tactical => "ItemsModeSelection", + RumbleOption.BatmanRumble => "ItemsMode_BM", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; @@ -169,6 +176,7 @@ private static string MapBoostStrength(BoostStrengthOption option) => BoostStrengthOption.One => "", BoostStrengthOption.OneAndAHalf => "BoostMultiplier1_5x", BoostStrengthOption.Two => "BoostMultiplier2x", + BoostStrengthOption.Five => "BoostMultiplier5x", BoostStrengthOption.Ten => "BoostMultiplier10x", _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; @@ -205,6 +213,31 @@ private static string MapRespawnTime(RespawnTimeOption option) => _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) }; + private static string MapMaxTime(MaxTimeOption option) => + option switch + { + MaxTimeOption.Default => "", + MaxTimeOption.Eleven_Minutes => "MaxTime11Minutes", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + private static string MapGameEvent(GameEventOption option) => + option switch + { + GameEventOption.Default => "", + GameEventOption.Rugby => "RugbyGameEventRules", + GameEventOption.Haunted => "HauntedGameEventRules", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + + private static string MapAudio(AudioOption option) => + option switch + { + AudioOption.Default => "", + AudioOption.Haunted => "HauntedAudio", + _ => throw new ArgumentOutOfRangeException(nameof(option), option, null) + }; + private static string GetOption(string option) { if (option != "") @@ -232,6 +265,9 @@ public static string MakeOpenCommand(MatchSettingsT matchSettings) // Parse game mode command += MapGameMode(matchSettings.GameMode); + if (matchSettings.SkipReplays) + command += "?noreplay"; + // Whether to or not to skip the kickoff countdown if (!matchSettings.InstantStart) command += "?Playtest"; @@ -262,6 +298,9 @@ public static string MakeOpenCommand(MatchSettingsT matchSettings) command += GetOption(MapGravity(mutatorSettings.GravityOption)); command += GetOption(MapDemolish(mutatorSettings.DemolishOption)); command += GetOption(MapRespawnTime(mutatorSettings.RespawnTimeOption)); + command += GetOption(MapMaxTime(mutatorSettings.MaxTimeOption)); + command += GetOption(MapGameEvent(mutatorSettings.GameEventOption)); + command += GetOption(MapAudio(mutatorSettings.AudioOption)); return command; } diff --git a/RLBotCS/Conversion/GameStateToFlat.cs b/RLBotCS/Conversion/GameStateToFlat.cs index 5673cb3..9b69e35 100644 --- a/RLBotCS/Conversion/GameStateToFlat.cs +++ b/RLBotCS/Conversion/GameStateToFlat.cs @@ -1,4 +1,5 @@ using Bridge.Models.Message; +using Bridge.Packet; using Bridge.State; using rlbot.flat; using CollisionShapeUnion = rlbot.flat.CollisionShapeUnion; @@ -51,17 +52,6 @@ public static GameTickPacketT ToFlatBuffers(this GameState gameState) AngularVelocity = ball.Physics.AngularVelocity.ToVector3T() }; - TouchT lastTouch = - new() - { - PlayerName = ball.LatestTouch.PlayerName, - PlayerIndex = ball.LatestTouch.PlayerIndex, - Team = ball.LatestTouch.Team, - GameSeconds = ball.LatestTouch.TimeSeconds, - Location = ball.LatestTouch.HitLocation.ToVector3T(), - Normal = ball.LatestTouch.HitNormal.ToVector3T() - }; - CollisionShapeUnion collisionShape = ball.Shape switch { ICollisionShape.Box boxShape @@ -91,14 +81,7 @@ ICollisionShape.Cylinder cylinderShape ) }; - balls.Add( - new() - { - Physics = ballPhysics, - LatestTouch = lastTouch, - Shape = collisionShape - } - ); + balls.Add(new() { Physics = ballPhysics, Shape = collisionShape }); } rlbot.flat.GameStateType gameStateType = gameState.GameStateType switch @@ -134,7 +117,9 @@ ICollisionShape.Cylinder cylinderShape ]; List boostStates = gameState - .BoostPads.Values.Select( + .BoostPads.Values.OrderBy(boost => boost.SpawnPosition.Y) + .ThenBy(boost => boost.SpawnPosition.X) + .Select( boost => new BoostPadStateT { IsActive = boost.IsActive, Timer = boost.Timer } ) .ToList(); @@ -152,6 +137,16 @@ ICollisionShape.Cylinder cylinderShape _ => AirState.OnGround }; + TouchT? lastTouch = null; + if (car.LastTouch is BallTouch touch) + lastTouch = new() + { + GameSeconds = touch.TimeSeconds, + Location = touch.HitLocation.ToVector3T(), + Normal = touch.HitNormal.ToVector3T(), + BallIndex = touch.BallIndex, + }; + players.Add( new() { @@ -162,6 +157,7 @@ ICollisionShape.Cylinder cylinderShape Velocity = car.Physics.Velocity.ToVector3T(), AngularVelocity = car.Physics.AngularVelocity.ToVector3T() }, + LastestTouch = lastTouch, AirState = airState, DodgeTimeout = car.DodgeTimeout, DemolishedTimeout = car.DemolishedTimeout, @@ -210,7 +206,7 @@ ICollisionShape.Cylinder cylinderShape Balls = balls, GameInfo = gameInfo, Teams = teams, - BoostPadStates = boostStates, + BoostPads = boostStates, Players = players }; } diff --git a/RLBotCS/Conversion/PsyonixLoadouts.cs b/RLBotCS/Conversion/PsyonixLoadouts.cs index 2a1137a..637a19b 100644 --- a/RLBotCS/Conversion/PsyonixLoadouts.cs +++ b/RLBotCS/Conversion/PsyonixLoadouts.cs @@ -6,7 +6,7 @@ namespace RLBotCS.Conversion; internal static class PsyonixLoadouts { private static Random _random = new(); - + /// Unused loadout names, used to avoid spawning multiple of the same Psyonix bot private static List Unused = new(); @@ -14,10 +14,22 @@ public static void Reset() { Unused.Clear(); } - + + public static PlayerLoadoutT? GetFromName(string name, int team) + { + int index = Unused.FindIndex(n => n.Contains(name)); + if (index == -1) + return null; + + var loadout = DefaultLoadouts[Unused[index]][team]; + Unused.RemoveAt(index); + return loadout; + } + public static (string, PlayerLoadoutT) GetNext(int team) { - if (Unused.Count == 0) Unused = DefaultLoadouts.Keys.OrderBy(_ => _random.Next()).ToList(); + if (Unused.Count == 0) + Unused = DefaultLoadouts.Keys.OrderBy(_ => _random.Next()).ToList(); var fullName = Unused.Last(); Unused.RemoveAt(Unused.Count - 1); var loadout = DefaultLoadouts[fullName][team]; diff --git a/RLBotCS/Main.cs b/RLBotCS/Main.cs index b329dbb..29627a1 100644 --- a/RLBotCS/Main.cs +++ b/RLBotCS/Main.cs @@ -16,7 +16,8 @@ if (rlbotSocketsPort < 0) { throw new FormatException(); - } else if (rlbotSocketsPort > 65535) + } + else if (rlbotSocketsPort > 65535) { throw new OverflowException(); } diff --git a/RLBotCS/ManagerTools/BallPredictor.cs b/RLBotCS/ManagerTools/BallPredictor.cs index 2397549..9ef1713 100644 --- a/RLBotCS/ManagerTools/BallPredictor.cs +++ b/RLBotCS/ManagerTools/BallPredictor.cs @@ -1,5 +1,4 @@ using System.Runtime.InteropServices; -using Bridge.Packet; using rlbot.flat; namespace RLBotCS.ManagerTools; @@ -62,7 +61,7 @@ public static partial class BallPredictor [LibraryImport("rl_ball_sym", EntryPoint = "free_ball_slices")] private static unsafe partial void FreeBallSlices(BallSlice* slices, ushort ticks); - private static Vec3 ToVec3(Bridge.Models.Phys.Vector3 vec) => new(vec.X, vec.Y, vec.Z); + private static Vec3 ToVec3(Vector3T vec) => new(vec.X, vec.Y, vec.Z); private static Vector3T ToVector3T(Vec3 vec) => new() @@ -111,7 +110,8 @@ public static PredictionMode GetMode(MatchSettingsT matchSettings) public static BallPredictionT Generate( PredictionMode mode, float currentTime, - Ball currentBall + BallInfoT currentBall, + (TouchT, uint)? lastTouch ) { BallSlice ball = @@ -131,20 +131,23 @@ Ball currentBall if (mode == PredictionMode.Heatseeker) { - if (currentBall.LatestTouch.TimeSeconds < float.Epsilon) + if (lastTouch is (TouchT, uint) lastestTouch) { - // A goal happened, we're in kickoff - ResetHeatseekerTarget(); - } - else if (currentTime - currentBall.LatestTouch.TimeSeconds < 0.1) - { - // Target goal is the opposite of the last touch - SetHeatseekerTarget(currentBall.LatestTouch.Team == 1 ? (byte)0 : (byte)1); + if (currentTime - lastestTouch.Item1.GameSeconds < 0.1) + { + // Target goal is the opposite of the last touch + SetHeatseekerTarget(lastestTouch.Item2 == 1 ? (byte)0 : (byte)1); + } + else if (GetHeatseekerTargetY() == 0 || MathF.Abs(ball.Location.Y) >= 4820) + { + // We're very likely to hit a wall that will redirect the ball towards the other goal + SetHeatseekerTarget(ball.LinearVelocity.Y < 0 ? (byte)1 : (byte)0); + } } - else if (GetHeatseekerTargetY() == 0 || MathF.Abs(ball.Location.Y) >= 4820) + else { - // We're very likely to hit a wall that will redirect the ball towards the other goal - SetHeatseekerTarget(ball.LinearVelocity.Y < 0 ? (byte)1 : (byte)0); + // A goal happened, we're in kickoff + ResetHeatseekerTarget(); } } diff --git a/RLBotCS/ManagerTools/CircularBuffer.cs b/RLBotCS/ManagerTools/CircularBuffer.cs new file mode 100644 index 0000000..e7042b0 --- /dev/null +++ b/RLBotCS/ManagerTools/CircularBuffer.cs @@ -0,0 +1,31 @@ +namespace RLBotCS.ManagerTools; + +public class CircularBuffer +{ + private int _startIndex; + private int _currentIndex; + private readonly T[] _buffer; + + public int Count => (_currentIndex - _startIndex + _buffer.Length) % _buffer.Length; + + public CircularBuffer(int capacity) + { + _buffer = new T[capacity]; + } + + public void AddLast(T item) + { + _buffer[_currentIndex] = item; + + // continuously overwrite the oldest item once full + _currentIndex = (_currentIndex + 1) % _buffer.Length; + if (_currentIndex == _startIndex) + _startIndex = (_startIndex + 1) % _buffer.Length; + } + + public IEnumerable Iter() + { + for (int i = _startIndex; i != _currentIndex; i = (i + 1) % _buffer.Length) + yield return _buffer[i]; + } +} diff --git a/RLBotCS/ManagerTools/ConfigParser.cs b/RLBotCS/ManagerTools/ConfigParser.cs index e32d733..7eb41e7 100644 --- a/RLBotCS/ManagerTools/ConfigParser.cs +++ b/RLBotCS/ManagerTools/ConfigParser.cs @@ -191,30 +191,29 @@ List missingValues private static string GetRunCommand(TomlTable runnableSettings, List missingValues) { - string? runCommandWindows = ParseString( - runnableSettings, - "run_command", - null, - missingValues - ); - string? runCommandLinux = ParseString( - runnableSettings, - "run_command_linux", - null, - missingValues - ); + string runCommandWindows = + ParseString(runnableSettings, "run_command", null, missingValues) ?? ""; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return runCommandWindows ?? ""; + return runCommandWindows; + + string runCommandLinux = + ParseString(runnableSettings, "run_command_linux", null, missingValues) ?? ""; - if (runCommandLinux != null) + if (runCommandLinux != "") return runCommandLinux; - // TODO: - // We're currently on Linux but there's no Linux-specific run command - // Try running the Windows command under Wine instead - Logger.LogError("No Linux-specific run command found for script!"); - return runCommandWindows ?? ""; + if (runCommandWindows != "") + { + // TODO: + // We're currently on Linux but there's no Linux-specific run command + // Try running the Windows command under Wine instead + Logger.LogError("No Linux-specific run command found for script!"); + return runCommandWindows; + } + + // No run command found + return ""; } private static ScriptConfigurationT GetScriptConfig( @@ -237,6 +236,7 @@ List missingValues ScriptConfigurationT scriptConfig = new() { + Name = ParseString(scriptSettings, "name", "Unnamed Script", missingValues), Location = CombinePaths( tomlParent, ParseString(scriptSettings, "location", "", missingValues) @@ -272,9 +272,15 @@ List missingValues PlayerClassUnion.FromPsyonix( new PsyonixT { - BotSkill = ParseFloat(table, "skill", 1.0f, missingValues) + BotSkill = ParseEnum( + table, + "skill", + PsyonixSkill.AllStar, + missingValues + ) } ), + matchConfigPath, missingValues ), PlayerClass.PartyMember @@ -297,21 +303,54 @@ List missingValues }; private static PlayerConfigurationT GetPsyonixConfig( - TomlTable table, + TomlTable playerTable, PlayerClassUnion classUnion, + string matchConfigPath, List missingValues ) { - var team = ParseUint(table, "team", 0, missingValues); - var (name, loadout) = PsyonixLoadouts.GetNext((int)team); + string? matchConfigParent = Path.GetDirectoryName(matchConfigPath); + + string? playerTomlPath = CombinePaths( + matchConfigParent, + ParseString(playerTable, "config", null, missingValues) + ); + TomlTable playerToml = GetTable(playerTomlPath); + string? tomlParent = Path.GetDirectoryName(playerTomlPath); + + TomlTable playerSettings = ParseTable(playerToml, "settings", missingValues); + + var team = ParseUint(playerTable, "team", 0, missingValues); + string? name = ParseString(playerSettings, "name", null, missingValues); + PlayerLoadoutT? loadout = GetPlayerLoadout(playerSettings, tomlParent, missingValues); + + if (name == null) + { + (name, loadout) = PsyonixLoadouts.GetNext((int)team); + } + else if (PsyonixLoadouts.GetFromName(name, (int)team) is PlayerLoadoutT newLoadout) + { + loadout = newLoadout; + } + + var runCommand = GetRunCommand(playerSettings, missingValues); + var location = ""; + + if (runCommand != "") + { + location = CombinePaths( + tomlParent, + ParseString(playerSettings, "location", "", missingValues) + ); + } return new() { Variety = classUnion, Team = team, Name = name, - Location = "", - RunCommand = "", + Location = location, + RunCommand = runCommand, Loadout = loadout, }; } @@ -324,7 +363,7 @@ List missingValues { string? loadoutTomlPath = CombinePaths( tomlParent, - ParseString(playerTable, "looks_config", null, missingValues) + ParseString(playerTable, "loadout_config", null, missingValues) ); if (loadoutTomlPath == null) @@ -508,6 +547,19 @@ List missingValues RespawnTimeOption.Three_Seconds, missingValues ), + MaxTimeOption = ParseEnum( + mutatorTable, + "max_time", + MaxTimeOption.Default, + missingValues + ), + GameEventOption = ParseEnum( + mutatorTable, + "game_event", + GameEventOption.Default, + missingValues + ), + AudioOption = ParseEnum(mutatorTable, "audio", AudioOption.Default, missingValues), }; public static MatchSettingsT GetMatchSettings(string path) @@ -562,12 +614,7 @@ public static MatchSettingsT GetMatchSettings(string path) true, missingValues["rlbot"] ), - GamePath = ParseString( - rlbotTable, - "rocket_league_exe_path", - "", - missingValues["rlbot"] - ), + GamePath = ParseString(rlbotTable, "game_path", "", missingValues["rlbot"]), GameMode = ParseEnum( matchTable, "game_mode", diff --git a/RLBotCS/ManagerTools/LaunchManager.cs b/RLBotCS/ManagerTools/LaunchManager.cs index 9dc08ab..d606781 100644 --- a/RLBotCS/ManagerTools/LaunchManager.cs +++ b/RLBotCS/ManagerTools/LaunchManager.cs @@ -18,6 +18,20 @@ internal static class LaunchManager private static readonly ILogger Logger = Logging.GetLogger("LaunchManager"); + public static string? GetGameArgsAndKill() + { + Process[] candidates = Process.GetProcessesByName("RocketLeague"); + + foreach (var candidate in candidates) + { + string args = GetProcessArgs(candidate); + candidate.Kill(); + return args; + } + + return null; + } + public static int FindUsableGamePort(int rlbotSocketsPort) { Process[] candidates = Process.GetProcessesByName("RocketLeague"); @@ -25,7 +39,7 @@ public static int FindUsableGamePort(int rlbotSocketsPort) // Search cmd line args for port foreach (var candidate in candidates) { - string[] args = GetProcessArgs(candidate); + string[] args = GetProcessArgs(candidate).Split(" "); foreach (var arg in args) if (arg.Contains("RLBot_ControllerURL")) { @@ -55,16 +69,16 @@ public static int FindUsableGamePort(int rlbotSocketsPort) return DefaultGamePort; } - private static string[] GetProcessArgs(Process process) + private static string GetProcessArgs(Process process) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return process.StartInfo.Arguments.Split(' '); + return process.StartInfo.Arguments; using WmiConnection con = new WmiConnection(); WmiQuery objects = con.CreateQuery( $"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {process.Id}" ); - return objects.SingleOrDefault()?["CommandLine"]?.ToString()?.Split(" ") ?? []; + return objects.SingleOrDefault()?["CommandLine"]?.ToString() ?? ""; } private static string[] GetIdealArgs(int gamePort) => @@ -89,6 +103,36 @@ private static List ParseCommand(string command) return parts; } + private static Process RunCommandInShell(string command) + { + Process process = new(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = $"/c {command}"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + process.StartInfo.FileName = "/bin/sh"; + process.StartInfo.Arguments = $"-c \"{command}\""; + } + else + throw new PlatformNotSupportedException( + "RLBot is not supported on non-Windows/Linux platforms" + ); + + return process; + } + + private static void LaunchGameViaLegendary() + { + Process legendary = RunCommandInShell( + "legendary launch Sugar -rlbot RLBot_ControllerURL=127.0.0.1:23233 RLBot_PacketSendRate=240 -nomovie" + ); + legendary.Start(); + } + public static void LaunchBots( Dictionary> processGroups, int rlbotSocketsPort @@ -100,28 +144,21 @@ int rlbotSocketsPort if (mainPlayer.RunCommand == "") continue; - Process botProcess = new(); + Process botProcess = RunCommandInShell(mainPlayer.RunCommand); if (mainPlayer.Location != "") botProcess.StartInfo.WorkingDirectory = mainPlayer.Location; + List spawnIds = processGroup.Select(player => player.SpawnId).ToList(); + botProcess.StartInfo.EnvironmentVariables["RLBOT_SPAWN_IDS"] = string.Join( + ',', + spawnIds + ); + botProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = + rlbotSocketsPort.ToString(); + try { - var commandParts = ParseCommand(mainPlayer.RunCommand); - botProcess.StartInfo.FileName = Path.Join( - mainPlayer.Location, - commandParts[0] - ); - botProcess.StartInfo.Arguments = string.Join(' ', commandParts.Skip(1)); - - List spawnIds = processGroup.Select(player => player.SpawnId).ToList(); - botProcess.StartInfo.EnvironmentVariables["RLBOT_SPAWN_IDS"] = string.Join( - ',', - spawnIds - ); - botProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = - rlbotSocketsPort.ToString(); - botProcess.Start(); } catch (Exception e) @@ -141,20 +178,18 @@ int rlbotSocketsPort if (script.RunCommand == "") continue; - Process scriptProcess = new(); + Process scriptProcess = RunCommandInShell(script.RunCommand); if (script.Location != "") scriptProcess.StartInfo.WorkingDirectory = script.Location; + scriptProcess.StartInfo.EnvironmentVariables["RLBOT_SPAWN_IDS"] = + script.SpawnId.ToString(); + scriptProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = + rlbotSocketsPort.ToString(); + try { - var commandParts = ParseCommand(script.RunCommand); - scriptProcess.StartInfo.FileName = Path.Join(script.Location, commandParts[0]); - scriptProcess.StartInfo.Arguments = string.Join(' ', commandParts.Skip(1)); - - scriptProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = - rlbotSocketsPort.ToString(); - scriptProcess.Start(); } catch (Exception e) @@ -164,10 +199,14 @@ int rlbotSocketsPort } } - public static void LaunchRocketLeague(rlbot.flat.Launcher launcher, int gamePort) + public static void LaunchRocketLeague( + rlbot.flat.Launcher launcherPref, + string gamePath, + int gamePort + ) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - switch (launcher) + switch (launcherPref) { case rlbot.flat.Launcher.Steam: string steamPath = GetWindowsSteamPath(); @@ -183,12 +222,76 @@ public static void LaunchRocketLeague(rlbot.flat.Launcher launcher, int gamePort rocketLeague.Start(); break; case rlbot.flat.Launcher.Epic: - throw new NotSupportedException("Epic Games not supported."); + // we need a hack to launch the game properly + + // start the game + Process launcher = new(); + launcher.StartInfo.FileName = "cmd.exe"; + launcher.StartInfo.Arguments = + "/c start \"\" \"com.epicgames.launcher://apps/9773aa1aa54f4f7b80e44bef04986cea%3A530145df28a24424923f5828cc9031a1%3ASugar?action=launch&silent=true\""; + launcher.Start(); + + Console.WriteLine("Waiting for Rocket League path details..."); + + // get the game path & login args, the quickly kill the game + // todo: add max number of retries + string? args = null; + while (args is null) + { + Thread.Sleep(1000); + args = GetGameArgsAndKill(); + } + + if (args is null) + throw new Exception("Failed to get Rocket League args"); + + string directGamePath = ParseCommand(args)[0]; + Logger.LogInformation($"Found Rocket League @ \"{directGamePath}\""); + + // append RLBot args + args = args.Replace(directGamePath, ""); + args = args.Replace("\"\"", ""); + string idealArgs = string.Join(" ", GetIdealArgs(gamePort)); + // rlbot args need to be first or the game might ignore them :( + string modifiedArgs = $"\"{directGamePath}\" {idealArgs} {args}"; + + // wait for the game to fully close + while (IsRocketLeagueRunning()) + Thread.Sleep(500); + + // relaunch the game with the new args + Process epicRocketLeague = new(); + epicRocketLeague.StartInfo.FileName = "cmd.exe"; + epicRocketLeague.StartInfo.Arguments = $"/c \"{modifiedArgs}\""; + + // prevent the game from printing to the console + epicRocketLeague.StartInfo.UseShellExecute = false; + epicRocketLeague.StartInfo.RedirectStandardOutput = true; + epicRocketLeague.StartInfo.RedirectStandardError = true; + epicRocketLeague.Start(); + + Logger.LogInformation( + $"Starting RocketLeague.exe directly with {idealArgs}" + ); + + // if we don't read the output, the game will hang + new Thread(() => + { + epicRocketLeague.StandardOutput.ReadToEnd(); + }).Start(); + + break; case rlbot.flat.Launcher.Custom: + if (gamePath.ToLower() == "legendary") + { + LaunchGameViaLegendary(); + return; + } + throw new NotSupportedException("Unexpected launcher. Use Steam."); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - switch (launcher) + switch (launcherPref) { case rlbot.flat.Launcher.Steam: string args = string.Join("%20", GetIdealArgs(gamePort)); @@ -204,8 +307,14 @@ public static void LaunchRocketLeague(rlbot.flat.Launcher launcher, int gamePort rocketLeague.Start(); break; case rlbot.flat.Launcher.Epic: - throw new NotSupportedException("Epic Games not supported."); + throw new NotSupportedException("Epic Games not supported on Linux."); case rlbot.flat.Launcher.Custom: + if (gamePath.ToLower() == "legendary") + { + LaunchGameViaLegendary(); + return; + } + throw new NotSupportedException("Unexpected launcher. Use Steam."); } else diff --git a/RLBotCS/ManagerTools/MatchStarter.cs b/RLBotCS/ManagerTools/MatchStarter.cs index 53daf0f..18ea8d0 100644 --- a/RLBotCS/ManagerTools/MatchStarter.cs +++ b/RLBotCS/ManagerTools/MatchStarter.cs @@ -23,26 +23,24 @@ int rlbotSocketsPort private bool _communicationStarted; private bool _hasEverLoadedMap; - private bool _needsSpawnBots; + private bool _needsSpawnCars; - public bool MatchEnded = false; + public bool HasSpawnedMap; + public bool MatchEnded; public MatchSettingsT? GetMatchSettings() => _deferredMatchSettings ?? _matchSettings; public void SetNullMatchSettings() { - _matchSettings = null; - _deferredMatchSettings = null; + if (!_needsSpawnCars) + _matchSettings = null; } public void StartCommunication() { _communicationStarted = true; - if (_deferredMatchSettings != null) - { - MakeMatch(_deferredMatchSettings); - _deferredMatchSettings = null; - } + if (_deferredMatchSettings is MatchSettingsT matchSettings) + MakeMatch(matchSettings); } public void StartMatch(MatchSettingsT matchSettings) @@ -50,7 +48,11 @@ public void StartMatch(MatchSettingsT matchSettings) if (!LaunchManager.IsRocketLeagueRunning()) { _communicationStarted = false; - LaunchManager.LaunchRocketLeague(matchSettings.Launcher, gamePort); + LaunchManager.LaunchRocketLeague( + matchSettings.Launcher, + matchSettings.GamePath, + gamePort + ); } if (!_communicationStarted) @@ -63,18 +65,25 @@ public void StartMatch(MatchSettingsT matchSettings) MakeMatch(matchSettings); } - public void MapSpawned() + public void MapSpawned(string MapName) { - if (_matchSettings != null) + Logger.LogInformation("Got map info for " + MapName); + _hasEverLoadedMap = true; + HasSpawnedMap = true; + + if (!_needsSpawnCars) + return; + + if (_deferredMatchSettings is MatchSettingsT matchSettings) { - bridge.TryWrite(new SetMutators(_matchSettings.MutatorSettings)); + bridge.TryWrite(new SetMutators(matchSettings.MutatorSettings)); - if (_needsSpawnBots) - { - SpawnCars(_matchSettings); - bridge.TryWrite(new FlushMatchCommands()); - _needsSpawnBots = false; - } + bool spawned = SpawnCars(matchSettings); + if (!spawned) + return; + + _matchSettings = matchSettings; + _deferredMatchSettings = null; } } @@ -104,6 +113,9 @@ private void MakeMatch(MatchSettingsT matchSettings) playerConfig.Location ??= ""; playerConfig.RunCommand ??= ""; + if (playerConfig.Variety.Type != PlayerClass.RLBot) + continue; + if (playerConfig.Hivemind) { // only add one process per team @@ -132,16 +144,45 @@ private void MakeMatch(MatchSettingsT matchSettings) } } + foreach (var scriptConfig in matchSettings.ScriptConfigurations) + { + // De-duplicating similar names, Overwrites original value + string scriptName = scriptConfig.Name ?? ""; + if (playerNames.TryGetValue(scriptName, out int value)) + { + playerNames[scriptName] = ++value; + scriptConfig.Name = scriptName + $" ({value})"; + } + else + { + playerNames[scriptName] = 0; + scriptConfig.Name = scriptName; + } + + if (scriptConfig.SpawnId == 0) + scriptConfig.SpawnId = scriptConfig.Name.GetHashCode(); + + scriptConfig.Location ??= ""; + scriptConfig.RunCommand ??= ""; + } + matchSettings.GamePath ??= ""; matchSettings.GameMapUpk ??= ""; _connectionReadies = 0; - _expectedConnections = 0; + _expectedConnections = matchSettings.ScriptConfigurations.Count + processes.Count; + if (matchSettings.AutoStartBots) { LaunchManager.LaunchBots(processes, rlbotSocketsPort); LaunchManager.LaunchScripts(matchSettings.ScriptConfigurations, rlbotSocketsPort); } + else + { + Logger.LogWarning( + "AutoStartBots is disabled in match settings. Bots & scripts will not be started automatically!" + ); + } LoadMatch(matchSettings); } @@ -153,23 +194,24 @@ private void LoadMatch(MatchSettingsT matchSettings) var shouldSpawnNewMap = matchSettings.ExistingMatchBehavior switch { - ExistingMatchBehavior.Continue_And_Spawn => !_hasEverLoadedMap || MatchEnded, - ExistingMatchBehavior.Restart_If_Different => IsDifferentFromLast(matchSettings), + ExistingMatchBehavior.Continue_And_Spawn => !_hasEverLoadedMap, + ExistingMatchBehavior.Restart_If_Different + => MatchEnded || IsDifferentFromLast(matchSettings), _ => true }; + _needsSpawnCars = true; if (shouldSpawnNewMap) { - _needsSpawnBots = true; _hasEverLoadedMap = true; + HasSpawnedMap = false; + _matchSettings = null; + _deferredMatchSettings = matchSettings; bridge.TryWrite(new SpawnMap(matchSettings)); } else { - // No need to load a new map, just spawn the players. - SpawnCars(matchSettings); - // despawn old bots that aren't in the new match if (_matchSettings is MatchSettingsT lastMatchSettings) { @@ -183,14 +225,20 @@ private void LoadMatch(MatchSettingsT matchSettings) if (toDespawn.Count > 0) { + Logger.LogInformation( + "Despawning old players: " + string.Join(", ", toDespawn) + ); bridge.TryWrite(new RemoveOldPlayers(toDespawn)); } } + // No need to load a new map, just spawn the players. + SpawnCars(matchSettings, true); bridge.TryWrite(new FlushMatchCommands()); - } - _matchSettings = matchSettings; + _matchSettings = matchSettings; + _deferredMatchSettings = null; + } } private bool IsDifferentFromLast(MatchSettingsT matchSettings) @@ -243,12 +291,34 @@ private bool IsDifferentFromLast(MatchSettingsT matchSettings) || lastMutators.RespawnTimeOption != mutators.RespawnTimeOption; } - private void SpawnCars(MatchSettingsT matchSettings) + private bool SpawnCars(MatchSettingsT matchSettings, bool force = false) { + // ensure this function is only called once + // and only if the map has been spawned + if (!_needsSpawnCars || !HasSpawnedMap) + return false; + + bool doSpawning = + force + || !matchSettings.AutoStartBots + || _expectedConnections <= _connectionReadies; + Logger.LogInformation( + "Spawning cars: " + + _expectedConnections + + " expected connections, " + + _connectionReadies + + " connection readies, " + + (doSpawning ? "spawning" : "not spawning") + ); + + if (!doSpawning) + return false; + + _needsSpawnCars = false; + PlayerConfigurationT? humanConfig = null; int numPlayers = matchSettings.PlayerConfigurations.Count; int indexOffset = 0; - _expectedConnections = matchSettings.ScriptConfigurations.Count; for (int i = 0; i < numPlayers; i++) { @@ -263,7 +333,6 @@ private void SpawnCars(MatchSettingsT matchSettings) + " with spawn id " + playerConfig.SpawnId ); - _expectedConnections++; bridge.TryWrite( new SpawnBot( @@ -278,11 +347,12 @@ private void SpawnCars(MatchSettingsT matchSettings) case PlayerClass.Psyonix: var skillEnum = playerConfig.Variety.AsPsyonix().BotSkill switch { - < 0 => BotSkill.Intro, - < 0.5f => BotSkill.Easy, - < 1 => BotSkill.Medium, + PsyonixSkill.Beginner => BotSkill.Intro, + PsyonixSkill.Rookie => BotSkill.Easy, + PsyonixSkill.Pro => BotSkill.Medium, _ => BotSkill.Hard }; + bridge.TryWrite( new SpawnBot(playerConfig, skillEnum, (uint)(i - indexOffset), false) ); @@ -310,34 +380,79 @@ private void SpawnCars(MatchSettingsT matchSettings) } } + // If no human was requested for the match, + // then make the human spectate so we can start the match if (humanConfig is null) - { - // If no human was requested for the match, - // then make the human spectate so we can start the match bridge.TryWrite(new ConsoleCommand("spectate")); - } else - { bridge.TryWrite(new SpawnHuman(humanConfig, (uint)(numPlayers - indexOffset))); + + bridge.TryWrite(new MarkQueuingComplete()); + + return true; + } + + public void AddLoadout(PlayerLoadoutT loadout, int spawnId) + { + if (_matchSettings is null) + { + Logger.LogError("Match settings not loaded yet."); + return; } - // If bots still need to be spawned, pause the game - if ( - matchSettings.AutoStartBots - && _expectedConnections != 0 - && _expectedConnections > _connectionReadies - ) - bridge.TryWrite(new SetPaused(true)); + if (!_needsSpawnCars) + { + // todo: when the match is already running, + // respawn the car with the new loadout in the same position + Logger.LogError( + "Match already started, can't add loadout - feature has not implemented!" + ); + return; + } + + var player = _matchSettings.PlayerConfigurations.Find(p => p.SpawnId == spawnId); + if (player is null) + { + Logger.LogError($"Player with spawn id {spawnId} not found to add loadout to."); + return; + } + + if (player.Loadout is not null) + { + Logger.LogError( + $"Player \"{player.Name}\" with spawn id {spawnId} already has a loadout." + ); + return; + } + + player.Loadout = loadout; } public void IncrementConnectionReadies() { _connectionReadies++; - if (_connectionReadies == _expectedConnections) + Logger.LogInformation( + "Connection readies: " + + _connectionReadies + + " / " + + _expectedConnections + + "; needs spawn cars: " + + _needsSpawnCars + ); + + if ( + _deferredMatchSettings is MatchSettingsT matchSettings + && _connectionReadies >= _expectedConnections + && _needsSpawnCars + ) { - bridge.TryWrite(new SetPaused(false)); - bridge.TryWrite(new FlushMatchCommands()); + bool spawned = SpawnCars(matchSettings); + if (!spawned) + return; + + _matchSettings = matchSettings; + _deferredMatchSettings = null; } } } diff --git a/RLBotCS/ManagerTools/PerfMonitor.cs b/RLBotCS/ManagerTools/PerfMonitor.cs new file mode 100644 index 0000000..84c41b1 --- /dev/null +++ b/RLBotCS/ManagerTools/PerfMonitor.cs @@ -0,0 +1,100 @@ +using Bridge.State; +using rlbot.flat; + +namespace RLBotCS.ManagerTools; + +public class PerfMonitor +{ + private const int ClientId = 0; + private const int RenderGroupId = 1; + private const int _maxSamples = 120; + private const float _timeSkip = 0.5f; + + private static readonly ColorT TextColor = new ColorT() + { + A = 255, + R = 255, + G = 255, + B = 255, + }; + private static readonly ColorT BackColor = new ColorT() + { + A = 100, + R = 0, + G = 0, + B = 0, + }; + + private readonly CircularBuffer _rlbotSamples = new(_maxSamples); + private readonly SortedDictionary> _samples = new(); + private float time = 0; + + public void AddRLBotSample(float timeDiff) + { + _rlbotSamples.AddLast(timeDiff); + } + + public void AddSample(string name, bool gotInput) + { + if (!_samples.ContainsKey(name)) + { + _samples[name] = new(_maxSamples); + } + + _samples[name].AddLast(gotInput); + } + + public void RemoveBot(string name) + { + _samples.Remove(name); + } + + public void RenderSummary(Rendering rendering, GameState gameState, float deltaTime) + { + time += deltaTime; + if (time < _timeSkip) + return; + time = 0; + + float averageTimeDiff = _rlbotSamples.Count > 0 ? _rlbotSamples.Iter().Average() : 1; + float timeDiffPercentage = 1 / (120f * averageTimeDiff); + + string message = $"RLBot: {timeDiffPercentage * 100:0.0}%"; + bool shouldRender = timeDiffPercentage < 0.9999 || timeDiffPercentage > 1.0001; + + foreach (var (name, samples) in _samples) + { + int gotInputCount = samples.Iter().Count(sample => sample); + float gotInputPercentage = (float)gotInputCount / samples.Count; + + message += $"\n{name}: {gotInputPercentage * 100:0.0}%"; + + if (gotInputPercentage < 0.9999 || gotInputPercentage > 1.0001) + shouldRender = true; + } + + var renderText = new String2DT() + { + Y = 10f / 1920f, + X = 200f / 1080f, + Text = message, + Foreground = TextColor, + Background = BackColor, + Scale = 1, + HAlign = TextHAlign.Left, + VAlign = TextVAlign.Top, + }; + + var renderMessages = new List() + { + new RenderMessageT() { Variety = RenderTypeUnion.FromString2D(renderText), }, + }; + + if (shouldRender) + rendering.AddRenderGroup(ClientId, RenderGroupId, renderMessages, gameState); + else + rendering.RemoveRenderGroup(ClientId, RenderGroupId); + } + + public void ClearAll() => _samples.Clear(); +} diff --git a/RLBotCS/ManagerTools/Rendering.cs b/RLBotCS/ManagerTools/Rendering.cs index 12f4103..8c01749 100644 --- a/RLBotCS/ManagerTools/Rendering.cs +++ b/RLBotCS/ManagerTools/Rendering.cs @@ -125,8 +125,11 @@ public void ClearClientRenders(int clientId) _clientRenderTracker.Remove(clientId); } - public void ClearAllRenders() + public void ClearAllRenders(MatchCommandSender matchCommandSender) { + matchCommandSender.AddConsoleCommand("FlushPersistentDebugLines"); + matchCommandSender.Send(); + foreach (var clientRenders in _clientRenderTracker.Values) foreach (int renderId in clientRenders.Keys) foreach (ushort renderItem in clientRenders[renderId]) diff --git a/RLBotCS/Server/BridgeContext.cs b/RLBotCS/Server/BridgeContext.cs index 2d533fc..36ac49f 100644 --- a/RLBotCS/Server/BridgeContext.cs +++ b/RLBotCS/Server/BridgeContext.cs @@ -16,8 +16,8 @@ TcpMessenger messenger { public readonly ILogger Logger = Logging.GetLogger("BridgeHandler"); + public int ticksSkipped = 0; public GameState GameState = new(); - public bool LastMatchEnded { get; set; } public ChannelWriter Writer { get; } = writer; public ChannelReader Reader { get; } = reader; @@ -26,15 +26,18 @@ TcpMessenger messenger public PlayerInputSender PlayerInputSender { get; } = new(messenger); public Rendering RenderingMgmt { get; } = new(messenger); public QuickChat QuickChat { get; } = new(); + public PerfMonitor PerfMonitor { get; } = new(); public bool GotFirstMessage { get; set; } public bool MatchHasStarted { get; set; } public bool QueuedMatchCommands { get; set; } public bool DelayMatchCommandSend { get; set; } + public bool QueuingCommandsComplete { get; set; } public void QueueConsoleCommand(string command) { QueuedMatchCommands = true; + QueuingCommandsComplete = false; MatchCommandSender.AddConsoleCommand(command); } } diff --git a/RLBotCS/Server/BridgeHandler.cs b/RLBotCS/Server/BridgeHandler.cs index bee0a05..f674df8 100644 --- a/RLBotCS/Server/BridgeHandler.cs +++ b/RLBotCS/Server/BridgeHandler.cs @@ -2,6 +2,8 @@ using Bridge.Conversion; using Bridge.TCP; using Microsoft.Extensions.Logging; +using rlbot.flat; +using RLBotCS.Conversion; using RLBotCS.Server.FlatbuffersMessage; using GameStateType = Bridge.Models.Message.GameStateType; @@ -13,6 +15,7 @@ internal class BridgeHandler( TcpMessenger messenger ) { + private const int MAX_TICK_SKIP = 1; private readonly BridgeContext _context = new(writer, reader, messenger); private async Task HandleIncomingMessages() @@ -50,41 +53,78 @@ private async Task HandleServer() // reset the counter that lets us know if we're sending too many bytes messenger.ResetByteCount(); + float prevTime = _context.GameState.SecondsElapsed; _context.GameState = MessageHandler.CreateUpdatedState( messageClump, _context.GameState ); - _context.Writer.TryWrite(new DistributeGameState(_context.GameState)); + float deltaTime = _context.GameState.SecondsElapsed - prevTime; + bool timeAdvanced = deltaTime > 0.001; + if (timeAdvanced) + _context.ticksSkipped = 0; + else + _context.ticksSkipped++; + + if (timeAdvanced) + _context.PerfMonitor.AddRLBotSample(deltaTime); + + GameTickPacketT? packet = + timeAdvanced + || ( + _context.ticksSkipped > MAX_TICK_SKIP + && ( + _context.GameState.GameStateType == GameStateType.Replay + || _context.GameState.GameStateType == GameStateType.Paused + || _context.GameState.GameStateType == GameStateType.Ended + || _context.GameState.GameStateType == GameStateType.Inactive + ) + ) + ? _context.GameState.ToFlatBuffers() + : null; + _context.Writer.TryWrite(new DistributeGameState(_context.GameState, packet)); var matchStarted = MessageHandler.ReceivedMatchInfo(messageClump); if (matchStarted) { - _context.RenderingMgmt.ClearAllRenders(); + // _context.Logger.LogInformation("Map name: " + _context.GameState.MapName); + _context.RenderingMgmt.ClearAllRenders(_context.MatchCommandSender); _context.MatchHasStarted = true; - _context.Writer.TryWrite(new MapSpawned()); + _context.Writer.TryWrite(new MapSpawned(_context.GameState.MapName)); } if (_context.GameState.MatchEnded) { - if (!_context.LastMatchEnded) - { - _context.QuickChat.ClearChats(); - _context.RenderingMgmt.ClearAllRenders(); - _context.Writer.TryWrite(new StopMatch(false)); - } + // reset everything + _context.QuickChat.ClearChats(); + _context.PerfMonitor.ClearAll(); } - else + else if ( + _context.GameState.GameStateType != GameStateType.Replay + && _context.GameState.GameStateType != GameStateType.Paused + && timeAdvanced + ) { + // only rerender if we're not in a replay or paused _context.QuickChat.RenderChats(_context.RenderingMgmt, _context.GameState); + + // only render if we're not in a goal scored or ended state + if (_context.GameState.GameStateType != GameStateType.GoalScored) + _context.PerfMonitor.RenderSummary( + _context.RenderingMgmt, + _context.GameState, + deltaTime + ); + else + _context.PerfMonitor.ClearAll(); } - _context.LastMatchEnded = _context.GameState.MatchEnded; if ( _context is { DelayMatchCommandSend: true, QueuedMatchCommands: true, + QueuingCommandsComplete: true, MatchHasStarted: true, GameState.GameStateType: GameStateType.Paused } @@ -94,6 +134,7 @@ _context is _context.DelayMatchCommandSend = false; _context.QueuedMatchCommands = false; + _context.Logger.LogInformation("Sending delayed match commands"); _context.MatchCommandSender.Send(); } @@ -113,7 +154,7 @@ public void Cleanup() try { - _context.RenderingMgmt.ClearAllRenders(); + _context.RenderingMgmt.ClearAllRenders(_context.MatchCommandSender); // we can only clear so many renders each tick // so we do this until we've cleared them all @@ -126,6 +167,8 @@ public void Cleanup() messenger.ResetByteCount(); } } + catch (InvalidOperationException) { } + catch (IOException) { } catch (Exception e) { _context.Logger.LogError($"Error while cleaning up BridgeHandler: {e}"); diff --git a/RLBotCS/Server/BridgeMessage.cs b/RLBotCS/Server/BridgeMessage.cs index bbb0ff5..0dd0152 100644 --- a/RLBotCS/Server/BridgeMessage.cs +++ b/RLBotCS/Server/BridgeMessage.cs @@ -85,6 +85,7 @@ public void HandleMessage(BridgeContext context) Loadout loadout = FlatToModel.ToLoadout(Config.Loadout, Config.Team); context.QueuedMatchCommands = true; + context.QueuingCommandsComplete = false; ushort commandId = context.MatchCommandSender.AddBotSpawnCommand( Config.Name, (int)Config.Team, @@ -105,6 +106,14 @@ public void HandleMessage(BridgeContext context) } } +internal record MarkQueuingComplete() : IBridgeMessage +{ + public void HandleMessage(BridgeContext context) + { + context.QueuingCommandsComplete = true; + } +} + internal record RemoveOldPlayers(List spawnIds) : IBridgeMessage { public void HandleMessage(BridgeContext context) @@ -128,15 +137,6 @@ internal record ConsoleCommand(string Command) : IBridgeMessage public void HandleMessage(BridgeContext context) => context.QueueConsoleCommand(Command); } -internal record SetPaused(bool pause) : IBridgeMessage -{ - public void HandleMessage(BridgeContext context) - { - context.QueuedMatchCommands = true; - context.MatchCommandSender.AddSetPausedCommand(pause); - } -} - internal record SpawnMap(MatchSettingsT MatchSettings) : IBridgeMessage { public void HandleMessage(BridgeContext context) @@ -156,12 +156,12 @@ internal record FlushMatchCommands() : IBridgeMessage { public void HandleMessage(BridgeContext context) { - if (context.QueuedMatchCommands) - { - context.MatchCommandSender.Send(); - context.DelayMatchCommandSend = false; - context.QueuedMatchCommands = false; - } + if (!context.QueuedMatchCommands) + return; + + context.MatchCommandSender.Send(); + context.DelayMatchCommandSend = false; + context.QueuedMatchCommands = false; } } @@ -293,7 +293,7 @@ public void HandleMessage(BridgeContext context) if (car.Physics is DesiredPhysicsT physics) { - var currentPhysics = context.GameState.GameCars[(ushort)id].Physics; + var currentPhysics = context.GameState.GameCars[(uint)i].Physics; var fullState = FlatToModel.DesiredToPhysics(physics, currentPhysics); context.MatchCommandSender.AddSetPhysicsCommand((ushort)id, fullState); @@ -312,8 +312,36 @@ public void HandleMessage(BridgeContext context) } } +internal record EndMatch() : IBridgeMessage +{ + public void HandleMessage(BridgeContext context) + { + context.MatchCommandSender.AddMatchEndCommand(); + context.MatchCommandSender.Send(); + } +} + +internal record ClearRenders() : IBridgeMessage +{ + public void HandleMessage(BridgeContext context) + { + context.QuickChat.ClearChats(); + context.PerfMonitor.ClearAll(); + context.RenderingMgmt.ClearAllRenders(context.MatchCommandSender); + } +} + internal record ShowQuickChat(MatchCommT MatchComm) : IBridgeMessage { public void HandleMessage(BridgeContext context) => context.QuickChat.AddChat(MatchComm, context.GameState.SecondsElapsed); } + +internal record AddPerfSample(uint Index, bool GotInput) : IBridgeMessage +{ + public void HandleMessage(BridgeContext context) + { + if (context.GameState.GameCars.TryGetValue(Index, out var car)) + context.PerfMonitor.AddSample(car.Name, GotInput); + } +} diff --git a/RLBotCS/Server/FlatBuffersServer.cs b/RLBotCS/Server/FlatBuffersServer.cs index 14ee985..1aaeca9 100644 --- a/RLBotCS/Server/FlatBuffersServer.cs +++ b/RLBotCS/Server/FlatBuffersServer.cs @@ -59,7 +59,7 @@ private void AddSession(TcpClient client) }); sessionThread.Start(); - _context.Sessions.Add(clientId, (sessionChannel.Writer, sessionThread)); + _context.Sessions.Add(clientId, (sessionChannel.Writer, sessionThread, 0)); } private async Task HandleIncomingMessages() @@ -106,11 +106,11 @@ public void BlockingRun() public void Cleanup() { // Send stop message to all sessions - foreach (var (writer, _) in _context.Sessions.Values) + foreach (var (writer, _, _) in _context.Sessions.Values) writer.TryComplete(); // Ensure all sessions are stopped - foreach (var (_, thread) in _context.Sessions.Values) + foreach (var (_, thread, _) in _context.Sessions.Values) thread.Join(); _context.Sessions.Clear(); diff --git a/RLBotCS/Server/FlatBuffersSession.cs b/RLBotCS/Server/FlatBuffersSession.cs index d8d758a..0ea2f1e 100644 --- a/RLBotCS/Server/FlatBuffersSession.cs +++ b/RLBotCS/Server/FlatBuffersSession.cs @@ -23,9 +23,9 @@ public record RendersAllowed(bool Allowed) : SessionMessage; public record StateSettingAllowed(bool Allowed) : SessionMessage; - public record MatchComm(MatchCommT matchComm) : SessionMessage; + public record MatchComm(MatchCommT Message) : SessionMessage; - public record StopMatch : SessionMessage; + public record StopMatch(bool Force) : SessionMessage; } internal class FlatBuffersSession @@ -40,14 +40,20 @@ internal class FlatBuffersSession private readonly Channel _incomingMessages; private readonly ChannelWriter _rlbotServer; private readonly ChannelWriter _bridge; + private readonly Dictionary _gotInput = new(); private bool _connectionEstablished; private bool _wantsBallPredictions; private bool _wantsComms; private bool _closeAfterMatch; + private bool _isReady; private bool _stateSettingIsEnabled; private bool _renderingIsEnabled; + private int _spawnId; + private bool _sessionForceClosed; + private bool _closed; + private readonly FlatBufferBuilder _messageBuilder = new(1 << 10); public FlatBuffersSession( @@ -83,8 +89,8 @@ private async Task ParseClientMessage(TypedPayload message) // The client requested that we close the connection return false; - case DataType.ReadyMessage when !_connectionEstablished: - var readyMsg = ReadyMessage.GetRootAsReadyMessage(byteBuffer); + case DataType.ConnectionSettings when !_connectionEstablished: + var readyMsg = ConnectionSettings.GetRootAsConnectionSettings(byteBuffer); _wantsBallPredictions = readyMsg.WantsBallPredictions; _wantsComms = readyMsg.WantsComms; @@ -92,21 +98,35 @@ private async Task ParseClientMessage(TypedPayload message) await _rlbotServer.WriteAsync(new IntroDataRequest(_incomingMessages.Writer)); - if (_closeAfterMatch) - await _rlbotServer.WriteAsync(new SessionReady()); - _connectionEstablished = true; break; + case DataType.SetLoadout when !_isReady || _stateSettingIsEnabled: + var setLoadout = SetLoadout.GetRootAsSetLoadout(byteBuffer).UnPack(); + + await _rlbotServer.WriteAsync( + new SpawnLoadout(setLoadout.Loadout, setLoadout.SpawnId) + ); + break; + + case DataType.InitComplete when _connectionEstablished && !_isReady: + var initComplete = InitComplete.GetRootAsInitComplete(byteBuffer); + + _spawnId = initComplete.SpawnId; + + await _rlbotServer.WriteAsync( + new SessionReady(_closeAfterMatch, _clientId, _spawnId) + ); + _isReady = true; + break; + case DataType.StopCommand: - var stopCommand = StopCommand.GetRootAsStopCommand(byteBuffer).UnPack(); + var stopCommand = StopCommand.GetRootAsStopCommand(byteBuffer); await _rlbotServer.WriteAsync(new StopMatch(stopCommand.ShutdownServer)); break; case DataType.StartCommand: - StartCommandT startCommand = StartCommand - .GetRootAsStartCommand(byteBuffer) - .UnPack(); + var startCommand = StartCommand.GetRootAsStartCommand(byteBuffer); MatchSettingsT tomlMatchSettings = ConfigParser.GetMatchSettings( startCommand.ConfigPath ); @@ -121,15 +141,16 @@ private async Task ParseClientMessage(TypedPayload message) case DataType.PlayerInput: var playerInputMsg = PlayerInput.GetRootAsPlayerInput(byteBuffer).UnPack(); + _gotInput[playerInputMsg.PlayerIndex] = true; + await _bridge.WriteAsync(new Input(playerInputMsg)); break; - case DataType.MatchComms: - if (!_wantsComms) - break; - + case DataType.MatchComms when _wantsComms: var matchComms = MatchComm.GetRootAsMatchComm(byteBuffer).UnPack(); - await _rlbotServer.WriteAsync(new SendMatchComm(_clientId, matchComms)); + await _rlbotServer.WriteAsync( + new SendMatchComm(_clientId, _spawnId, matchComms) + ); await _bridge.WriteAsync(new ShowQuickChat(matchComms)); break; @@ -149,9 +170,9 @@ await _bridge.WriteAsync( if (!_renderingIsEnabled) break; - var removeRenderGroup = RemoveRenderGroup - .GetRootAsRemoveRenderGroup(byteBuffer) - .UnPack(); + var removeRenderGroup = RemoveRenderGroup.GetRootAsRemoveRenderGroup( + byteBuffer + ); await _bridge.WriteAsync(new RemoveRenders(_clientId, removeRenderGroup.Id)); break; @@ -190,6 +211,11 @@ private async Task SendPayloadToClientAsync(TypedPayload payload) await _socketSpecWriter.SendAsync(); } catch (ObjectDisposedException) + { + // we disconnected before the message could be sent + return; + } + catch (IOException) { // client disconnected before we could send the message return; @@ -223,7 +249,7 @@ await SendPayloadToClientAsync( ); break; case SessionMessage.DistributeBallPrediction m - when _connectionEstablished && _wantsBallPredictions: + when _isReady && _wantsBallPredictions: _messageBuilder.Clear(); _messageBuilder.Finish( BallPrediction.Pack(_messageBuilder, m.BallPrediction).Value @@ -236,7 +262,7 @@ await SendPayloadToClientAsync( ) ); break; - case SessionMessage.DistributeGameState m when _connectionEstablished: + case SessionMessage.DistributeGameState m when _isReady: _messageBuilder.Clear(); _messageBuilder.Finish( GameTickPacket.Pack(_messageBuilder, m.GameState).Value @@ -248,6 +274,13 @@ await SendPayloadToClientAsync( _messageBuilder ) ); + + foreach (var (index, gotInput) in _gotInput) + { + await _bridge.WriteAsync(new AddPerfSample(index, gotInput)); + _gotInput[index] = false; + } + break; case SessionMessage.RendersAllowed m: _renderingIsEnabled = m.Allowed; @@ -255,9 +288,9 @@ await SendPayloadToClientAsync( case SessionMessage.StateSettingAllowed m: _stateSettingIsEnabled = m.Allowed; break; - case SessionMessage.MatchComm m when _connectionEstablished && _wantsComms: + case SessionMessage.MatchComm m when _isReady && _wantsComms: _messageBuilder.Clear(); - _messageBuilder.Finish(MatchComm.Pack(_messageBuilder, m.matchComm).Value); + _messageBuilder.Finish(MatchComm.Pack(_messageBuilder, m.Message).Value); await SendPayloadToClientAsync( TypedPayload.FromFlatBufferBuilder( @@ -267,7 +300,9 @@ await SendPayloadToClientAsync( ); break; - case SessionMessage.StopMatch when _connectionEstablished && _closeAfterMatch: + case SessionMessage.StopMatch m + when m.Force || (_connectionEstablished && _closeAfterMatch): + _sessionForceClosed = m.Force; return; } } @@ -276,6 +311,11 @@ private async Task HandleClientMessages() { await foreach (TypedPayload message in _socketSpecReader.ReadAllAsync()) { + // if the session is closed, ignore any incoming messages + // this should allow the client to close cleanly + if (_closed) + continue; + bool keepRunning = await ParseClientMessage(message); if (keepRunning) continue; @@ -317,6 +357,7 @@ public void Cleanup() Logger.LogInformation($"Closing session {_clientId}."); _connectionEstablished = false; + _isReady = false; _incomingMessages.Writer.TryComplete(); // try to politely close the connection @@ -332,7 +373,12 @@ public void Cleanup() // if an exception was thrown, the client disconnected first // remove this session from the server _rlbotServer.TryWrite(new SessionClosed(_clientId)); - _client.Close(); + + // if we're trying to shutdown cleanly, + // let the bot finish sending messages and close the connection itself + _closed = true; + if (!_sessionForceClosed) + _client.Close(); } } } diff --git a/RLBotCS/Server/FlatbuffersMessage/DistributeGameState.cs b/RLBotCS/Server/FlatbuffersMessage/DistributeGameState.cs index eb46292..6fcd701 100644 --- a/RLBotCS/Server/FlatbuffersMessage/DistributeGameState.cs +++ b/RLBotCS/Server/FlatbuffersMessage/DistributeGameState.cs @@ -1,12 +1,12 @@ using Bridge.State; using rlbot.flat; -using RLBotCS.Conversion; using RLBotCS.ManagerTools; using GoalInfo = Bridge.Packet.GoalInfo; namespace RLBotCS.Server.FlatbuffersMessage; -internal record DistributeGameState(GameState GameState) : IServerMessage +internal record DistributeGameState(GameState GameState, GameTickPacketT? Packet) + : IServerMessage { private static void UpdateFieldInfo(ServerContext context, GameState gameState) { @@ -64,6 +64,15 @@ private static void UpdateFieldInfo(ServerContext context, GameState gameState) ); } + context.FieldInfo.BoostPads.Sort( + (a, b) => + { + if (a.Location.Y != b.Location.Y) + return a.Location.Y.CompareTo(b.Location.Y); + return a.Location.X.CompareTo(b.Location.X); + } + ); + // Distribute the field info to all waiting sessions foreach (var writer in context.FieldInfoWriters) { @@ -73,33 +82,45 @@ private static void UpdateFieldInfo(ServerContext context, GameState gameState) context.FieldInfoWriters.Clear(); } - private static void DistributeBallPrediction(ServerContext context, GameState gameState) + private static void DistributeBallPrediction(ServerContext context, GameTickPacketT packet) { - var firstBall = gameState.Balls.Values.FirstOrDefault(); + var firstBall = packet.Balls.FirstOrDefault(); if (firstBall is null) return; + (TouchT, uint)? lastTouch = null; + + foreach (var car in packet.Players) + { + if (car.LastestTouch is TouchT touch && touch.BallIndex == 0) + { + lastTouch = (touch, car.Team); + break; + } + } + BallPredictionT prediction = BallPredictor.Generate( context.PredictionMode, - gameState.SecondsElapsed, - firstBall + packet.GameInfo.SecondsElapsed, + firstBall, + lastTouch ); - foreach (var (writer, _) in context.Sessions.Values) + foreach (var (writer, _, _) in context.Sessions.Values) { SessionMessage message = new SessionMessage.DistributeBallPrediction(prediction); writer.TryWrite(message); } } - private static void DistributeState(ServerContext context, GameState gameState) + private static void DistributeState(ServerContext context, GameTickPacketT packet) { - context.MatchStarter.MatchEnded = gameState.MatchEnded; - - var gameTickPacket = gameState.ToFlatBuffers(); - foreach (var (writer, _) in context.Sessions.Values) + context.LastTickPacket = packet; + foreach (var (writer, _, _) in context.Sessions.Values) { - SessionMessage message = new SessionMessage.DistributeGameState(gameTickPacket); + SessionMessage message = new SessionMessage.DistributeGameState( + context.LastTickPacket + ); writer.TryWrite(message); } } @@ -107,8 +128,13 @@ private static void DistributeState(ServerContext context, GameState gameState) public ServerAction Execute(ServerContext context) { UpdateFieldInfo(context, GameState); - DistributeBallPrediction(context, GameState); - DistributeState(context, GameState); + context.MatchStarter.MatchEnded = GameState.MatchEnded; + + if (Packet is GameTickPacketT packet) + { + DistributeBallPrediction(context, packet); + DistributeState(context, packet); + } return ServerAction.Continue; } diff --git a/RLBotCS/Server/FlatbuffersMessage/MapSpawned.cs b/RLBotCS/Server/FlatbuffersMessage/MapSpawned.cs index bea5b3c..6983453 100644 --- a/RLBotCS/Server/FlatbuffersMessage/MapSpawned.cs +++ b/RLBotCS/Server/FlatbuffersMessage/MapSpawned.cs @@ -1,12 +1,12 @@ namespace RLBotCS.Server.FlatbuffersMessage; -internal record MapSpawned : IServerMessage +internal record MapSpawned(string MapName) : IServerMessage { public ServerAction Execute(ServerContext context) { context.FieldInfo = null; context.ShouldUpdateFieldInfo = true; - context.MatchStarter.MapSpawned(); + context.MatchStarter.MapSpawned(MapName); return ServerAction.Continue; } diff --git a/RLBotCS/Server/FlatbuffersMessage/SendMatchComm.cs b/RLBotCS/Server/FlatbuffersMessage/SendMatchComm.cs index 02842aa..ae9eabc 100644 --- a/RLBotCS/Server/FlatbuffersMessage/SendMatchComm.cs +++ b/RLBotCS/Server/FlatbuffersMessage/SendMatchComm.cs @@ -1,23 +1,46 @@ +using Microsoft.Extensions.Logging; using rlbot.flat; namespace RLBotCS.Server.FlatbuffersMessage; -internal record SendMatchComm(int ClientId, MatchCommT MatchComm) : IServerMessage +internal record SendMatchComm(int ClientId, int SpawnId, MatchCommT MatchComm) : IServerMessage { public ServerAction Execute(ServerContext context) { - var message = new SessionMessage.MatchComm(MatchComm); + SessionMessage.MatchComm message = new(MatchComm); - foreach (var session in context.Sessions) + if (context.LastTickPacket is null) { - var id = session.Key; - var writer = session.Value; + context.Logger.LogWarning("Received MatchComm before receiving a GameTickPacket."); + return ServerAction.Continue; + } + foreach (var (id, (writer, _, spawnId)) in context.Sessions) + { // Don't send the message back to the client that sent it. - if (id != ClientId) + if (id == ClientId) + continue; + + // intentional let spawnId == 0 pass through + // this should allow special connections like match managers to receive all messages + if (message.Message.TeamOnly && spawnId != 0) { - writer.writer.TryWrite(message); + // team 0 is blue, and 1 is orange + // but team 2 means only scripts (so, not players) should receive the message + + var player = context.LastTickPacket.Players.Find( + player => player.SpawnId == spawnId + ); + if (message.Message.Team == 2) + { + if (player is not null) + continue; + } + else if (player is null || player.Team != message.Message.Team) + continue; } + + writer.TryWrite(message); } return ServerAction.Continue; diff --git a/RLBotCS/Server/FlatbuffersMessage/ServerContext.cs b/RLBotCS/Server/FlatbuffersMessage/ServerContext.cs index d1bef49..39065f1 100644 --- a/RLBotCS/Server/FlatbuffersMessage/ServerContext.cs +++ b/RLBotCS/Server/FlatbuffersMessage/ServerContext.cs @@ -20,7 +20,7 @@ ChannelWriter bridge incomingMessages.Writer; public Dictionary< int, - (ChannelWriter writer, Thread thread) + (ChannelWriter writer, Thread thread, int spawnId) > Sessions { get; } = []; public FieldInfoT? FieldInfo { get; set; } @@ -34,4 +34,6 @@ public Dictionary< public PredictionMode PredictionMode { get; set; } public bool StateSettingIsEnabled = false; public bool RenderingIsEnabled = false; + + public GameTickPacketT? LastTickPacket { get; set; } } diff --git a/RLBotCS/Server/FlatbuffersMessage/SessionReady.cs b/RLBotCS/Server/FlatbuffersMessage/SessionReady.cs index 5278905..140209b 100644 --- a/RLBotCS/Server/FlatbuffersMessage/SessionReady.cs +++ b/RLBotCS/Server/FlatbuffersMessage/SessionReady.cs @@ -1,10 +1,14 @@ namespace RLBotCS.Server.FlatbuffersMessage; -internal record SessionReady() : IServerMessage +internal record SessionReady(bool incrConnections, int ClientId, int SpawnId) : IServerMessage { public ServerAction Execute(ServerContext context) { - context.MatchStarter.IncrementConnectionReadies(); + if (incrConnections) + context.MatchStarter.IncrementConnectionReadies(); + + var (writer, session, _) = context.Sessions[ClientId]; + context.Sessions[ClientId] = (writer, session, SpawnId); return ServerAction.Continue; } diff --git a/RLBotCS/Server/FlatbuffersMessage/SpawnLoadout.cs b/RLBotCS/Server/FlatbuffersMessage/SpawnLoadout.cs new file mode 100644 index 0000000..7680c0a --- /dev/null +++ b/RLBotCS/Server/FlatbuffersMessage/SpawnLoadout.cs @@ -0,0 +1,13 @@ +using rlbot.flat; + +namespace RLBotCS.Server.FlatbuffersMessage; + +internal record SpawnLoadout(PlayerLoadoutT Loadout, int SpawnId) : IServerMessage +{ + public ServerAction Execute(ServerContext context) + { + context.MatchStarter.AddLoadout(Loadout, SpawnId); + + return ServerAction.Continue; + } +} diff --git a/RLBotCS/Server/FlatbuffersMessage/StartMatch.cs b/RLBotCS/Server/FlatbuffersMessage/StartMatch.cs index 0e57182..4ac8dfb 100644 --- a/RLBotCS/Server/FlatbuffersMessage/StartMatch.cs +++ b/RLBotCS/Server/FlatbuffersMessage/StartMatch.cs @@ -7,8 +7,11 @@ internal record StartMatch(MatchSettingsT MatchSettings) : IServerMessage { public ServerAction Execute(ServerContext context) { - foreach (var (writer, _) in context.Sessions.Values) - writer.TryWrite(new SessionMessage.StopMatch()); + context.LastTickPacket = null; + context.Bridge.TryWrite(new ClearRenders()); + + foreach (var (writer, _, _) in context.Sessions.Values) + writer.TryWrite(new SessionMessage.StopMatch(false)); if (MatchSettings.MutatorSettings == null) MatchSettings.MutatorSettings = new(); @@ -26,7 +29,7 @@ public ServerAction Execute(ServerContext context) } // update all sessions with the new rendering and state setting settings - foreach (var (writer, _) in context.Sessions.Values) + foreach (var (writer, _, _) in context.Sessions.Values) { SessionMessage render = new SessionMessage.RendersAllowed( MatchSettings.EnableRendering diff --git a/RLBotCS/Server/FlatbuffersMessage/StopMatch.cs b/RLBotCS/Server/FlatbuffersMessage/StopMatch.cs index d1b94a2..527a378 100644 --- a/RLBotCS/Server/FlatbuffersMessage/StopMatch.cs +++ b/RLBotCS/Server/FlatbuffersMessage/StopMatch.cs @@ -4,18 +4,25 @@ internal record StopMatch(bool ShutdownServer) : IServerMessage { public ServerAction Execute(ServerContext context) { - context.MatchStarter.SetNullMatchSettings(); - context.FieldInfo = null; - context.ShouldUpdateFieldInfo = false; - if (ShutdownServer) { context.IncomingMessagesWriter.TryComplete(); return ServerAction.Stop; } - foreach (var (writer, _) in context.Sessions.Values) - writer.TryWrite(new SessionMessage.StopMatch()); + if (!context.MatchStarter.HasSpawnedMap) + return ServerAction.Continue; + + context.MatchStarter.SetNullMatchSettings(); + context.FieldInfo = null; + context.ShouldUpdateFieldInfo = false; + context.LastTickPacket = null; + context.Bridge.TryWrite(new ClearRenders()); + context.Bridge.TryWrite(new EndMatch()); + + foreach (var (writer, _, _) in context.Sessions.Values) + writer.TryWrite(new SessionMessage.StopMatch(ShutdownServer)); + return ServerAction.Continue; } } diff --git a/RLBotCS/Types/DataType.cs b/RLBotCS/Types/DataType.cs index 68f6166..5eacda9 100644 --- a/RLBotCS/Types/DataType.cs +++ b/RLBotCS/Types/DataType.cs @@ -7,14 +7,20 @@ 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 + /// + /// Arrives at a high rate according to https://wiki.rlbot.org/botmaking/tick-rate/ except + /// "desired tick rate" is not relevant here + /// GameTickPacket, - // Sent once when a match starts, or when you first connect. + /// + /// Sent once when a match starts, or when you first connect. + /// FieldInfo, - // Sent once when a match starts, or when you first connect. + /// + /// Sent once when a match starts, or when you first connect. + /// StartCommand, MatchSettings, PlayerInput, @@ -23,13 +29,29 @@ public enum DataType : ushort RemoveRenderGroup, MatchComms, - // Sent every time the ball diverges from the previous prediction, - // or when the previous prediction no longer gives a full 6 seconds into the future + /// + /// Sent every time the ball diverges from the previous prediction, + /// or when the previous prediction no longer gives a full 6 seconds into the future + /// BallPrediction, - // Clients must send this after connecting to the socket. - ReadyMessage, + /// + /// 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 + /// + /// 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 } diff --git a/RLBotCS/lib/Bridge.dll b/RLBotCS/lib/Bridge.dll index 17102d5..ccf999a 100644 Binary files a/RLBotCS/lib/Bridge.dll and b/RLBotCS/lib/Bridge.dll differ diff --git a/RLBotCSTests/BallPrediction.cs b/RLBotCSTests/BallPrediction.cs index 0cdcc5e..168c868 100644 --- a/RLBotCSTests/BallPrediction.cs +++ b/RLBotCSTests/BallPrediction.cs @@ -2,6 +2,7 @@ using Bridge.Models.Phys; using Bridge.State; using Microsoft.VisualStudio.TestTools.UnitTesting; +using RLBotCS.Conversion; using RLBotCS.ManagerTools; namespace RLBotCSTests; @@ -28,8 +29,9 @@ public void TestBallPred() var packet = new GameState(); packet.Balls[12345] = new(); + var gTP = packet.ToFlatBuffers(); - BallPredictor.Generate(PredictionMode.Standard, 1, packet.Balls[12345]); + BallPredictor.Generate(PredictionMode.Standard, 1, gTP.Balls[0], null); packet.Balls[12345].Physics = new Physics( new Vector3(0, 0, 1.1f * 91.25f), @@ -37,8 +39,9 @@ public void TestBallPred() new Vector3(0, 0, 0), new Rotator(0, 0, 0) ); + var gTP2 = packet.ToFlatBuffers(); - var ballPred = BallPredictor.Generate(PredictionMode.Standard, 1, packet.Balls[12345]); + var ballPred = BallPredictor.Generate(PredictionMode.Standard, 1, gTP2.Balls[0], null); int numSlices = 6 * 120; Assert.AreEqual(numSlices, ballPred.Slices.Count); @@ -59,9 +62,10 @@ public void TestBallPred() new Vector3(0, 0, 0), new Rotator(0, 0, 0) ); + var gTP3 = packet.ToFlatBuffers(); stopWatch.Start(); - BallPredictor.Generate(PredictionMode.Standard, 1, packet.Balls[12345]); + BallPredictor.Generate(PredictionMode.Standard, 1, gTP3.Balls[0], null); stopWatch.Stop(); } diff --git a/RLBotCSTests/FlatbufferTest.cs b/RLBotCSTests/FlatbufferTest.cs index cd4b7db..8b7ecce 100644 --- a/RLBotCSTests/FlatbufferTest.cs +++ b/RLBotCSTests/FlatbufferTest.cs @@ -47,6 +47,7 @@ public static List RandomScriptConfigurations() scriptConfigurations.Add( new ScriptConfigurationT() { + Name = RandomString(64), Location = RandomString(64), RunCommand = RandomString(64), } diff --git a/RLBotCSTests/GameState.cs b/RLBotCSTests/GameState.cs index 676ec77..afece4d 100644 --- a/RLBotCSTests/GameState.cs +++ b/RLBotCSTests/GameState.cs @@ -1,3 +1,4 @@ +using Bridge.Packet; using Bridge.State; using Google.FlatBuffers; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -17,5 +18,35 @@ public void Test() FlatBufferBuilder builder = new(1024); var offset = GameTickPacket.Pack(builder, packet.ToFlatBuffers()); builder.Finish(offset.Value); + + packet.BoostPads.Add( + 0, + new BoostPadInfo + { + SpawnPosition = new Bridge.Models.Phys.Vector3(1, 1, 0), + IsActive = true + } + ); + + packet.BoostPads.Add( + 1, + new BoostPadInfo + { + SpawnPosition = new Bridge.Models.Phys.Vector3(0, 0, 0), + IsActive = true + } + ); + + packet.BoostPads.Add( + 2, + new BoostPadInfo + { + SpawnPosition = new Bridge.Models.Phys.Vector3(0, 1, 0), + IsActive = true + } + ); + + var flatPacket = packet.ToFlatBuffers(); + Assert.AreEqual(3, flatPacket.BoostPads.Count); } } diff --git a/RLBotCSTests/PlayerMappingTest.cs b/RLBotCSTests/PlayerMappingTest.cs index d4f0db7..73bd581 100644 --- a/RLBotCSTests/PlayerMappingTest.cs +++ b/RLBotCSTests/PlayerMappingTest.cs @@ -1,4 +1,3 @@ -using Bridge.Models.Message; using Bridge.State; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -33,20 +32,25 @@ public void TestSpawnProcess() _playerMapping.AddPendingSpawn(spawnTracker); - var carSpawn = new CarSpawn() { ActorId = actorId, CommandId = commandId, }; - var metadata = _playerMapping.ApplyCarSpawn(carSpawn); + var metadata = _playerMapping.ApplyCarSpawn(actorId, commandId); Assert.AreEqual(desiredIndex, _playerMapping.PlayerIndexFromActorId(actorId)); Assert.IsTrue(metadata.IsBot); Assert.IsTrue(!metadata.IsCustomBot); - var metadata2 = _playerMapping.ApplyCarSpawn( - new CarSpawn() { ActorId = 111, CommandId = 222, } - ); + var metadata2 = _playerMapping.ApplyCarSpawn(111, 222); + uint? index = _playerMapping.PlayerIndexFromActorId(111); Assert.AreEqual(0u, _playerMapping.PlayerIndexFromActorId(111)); + Assert.IsNotNull(index); + Assert.AreEqual(index, 0u); Assert.AreEqual(desiredIndex, _playerMapping.PlayerIndexFromActorId(actorId)); Assert.IsTrue(!metadata2.IsBot); Assert.IsTrue(!metadata2.IsCustomBot); + + uint? index2 = _playerMapping.PlayerIndexFromActorId(456); + + Assert.IsNull(index2); + Assert.AreNotEqual(index2, 0u); } } diff --git a/RLBotCSTests/TomlTest/default.toml b/RLBotCSTests/TomlTest/default.toml index 5f8b8c0..f872db2 100644 --- a/RLBotCSTests/TomlTest/default.toml +++ b/RLBotCSTests/TomlTest/default.toml @@ -1,6 +1,7 @@ [rlbot] -# rocket_league_exe_path = "epicgames/path/to/rocketleague.exe" -# "Steam", "Epic" +# use this along with launcher = "custom" +# game_path = "legendary" +# "Steam", "Epic", "Custom" launcher = "steam" # Should RLBot start the bot processes automatically, or will a separate script start them auto_start_bots = true @@ -11,7 +12,7 @@ num_cars = 0 # Number of scripts which will be spawned. num_scripts = 0 # What game mode the game should load. -# Accepted values are "Soccer", "Hoops", "Dropshot", "Hockey", "Rumble", "Heatseeker", "Gridiron" +# Accepted values are "Soccer", "Hoops", "Dropshot", "Hockey", "Rumble", "Heatseeker", "Gridiron", "Knockout" game_mode = "Soccer" # Which map the game should load into game_map_upk = "Stadium_P" @@ -41,23 +42,29 @@ overtime = "Unlimited" game_speed = "Default" # "Default", "Slow", "Fast", "Super_Fast" ball_max_speed = "Default" -# "Default", "Cube", "Puck", "Basketball", "Beachball", "Anniversary", "Haunted" +# "Default", "Cube", "Puck", "Basketball", "Beachball", "Anniversary", "Haunted", "Ekin", "SpookyCube" ball_type = "Default" -# "Default", "Light", "Heavy", "Super_Light", "Curve_Ball", "Beach_Ball_Curve" +# "Default", "Light", "Heavy", "Super_Light", "Curve_Ball", "Beach_Ball_Curve", "Magnus_FutBall" ball_weight = "Default" -# "Default", "Small", "Large", "Gigantic" +# "Default", "Small", "Medium", "Large", "Gigantic" ball_size = "Default" -# "Default", "Low", "High", "Super_High" +# "Default", "Low", "LowishBounciness", "High", "Super_High" ball_bounciness = "Default" # "Normal_Boost", "Unlimited_Boost", "Slow_Recharge", "Rapid_Recharge", "No_Boost" boost_amount = "Normal_Boost" -# "No_Rumble", "Default", "Slow", "Civilized", "Destruction_Derby", "Spring_Loaded", "Spikes_Only", "Spike_Rush", "Haunted_Ball_Beam", "Tactical" +# "No_Rumble", "Default", "Slow", "Civilized", "Destruction_Derby", "Spring_Loaded", "Spikes_Only", "Spike_Rush", "Haunted_Ball_Beam", "Tactical", "BatmanRumble" rumble = "No_Rumble" -# "One", "OneAndAHalf", "Two", "Ten" +# "One", "OneAndAHalf", "Two", "Five", "Ten" boost_strength = "One" # "Default", "Low", "High", "Super_High", "Reverse" gravity = "Default" # "Default", "Disabled", "Friendly_Fire", "On_Contact", "On_Contact_FF" demolish = "Default" -# "Three_Seconds", "Two_Seconds", "One_Seconds", "Disable_Goal_Reset" +# "Three_Seconds", "Two_Seconds", "One_Second", "Disable_Goal_Reset" respawn_time = "Three_Seconds" +# "Default", "Eleven_Minutes" +max_time = "Default" +# "Default", "Haunted", "Rugby" +game_event = "Default" +# "Default", "Haunted" +audio = "Default" diff --git a/flatbuffers-schema b/flatbuffers-schema index fc972ab..c265764 160000 --- a/flatbuffers-schema +++ b/flatbuffers-schema @@ -1 +1 @@ -Subproject commit fc972ab24893a44f6df316c41d3eaf12b0c8ead1 +Subproject commit c26576471b4d0b7477634ed945c6b249ba432ce0