From 0ef8a9db0051f8a4f83eff9e4abc3503cf35456b Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 25 Jun 2026 18:37:53 -0500 Subject: [PATCH 01/10] Refactor LaunchManager for no EAC via Steam on Linux --- RLBotCS/ManagerTools/LaunchManager.cs | 496 ------------------ .../ManagerTools/LaunchManager/BotLauncher.cs | 103 ++++ .../LaunchManager/CustomLaunchers.cs | 47 ++ .../ManagerTools/LaunchManager/Epic.Linux.cs | 13 + .../LaunchManager/Epic.Windows.cs | 92 ++++ .../LaunchManager/ProcessUtils.cs | 183 +++++++ .../LaunchManager/RocketLeagueLauncher.cs | 43 ++ .../ManagerTools/LaunchManager/Steam.Linux.cs | 420 +++++++++++++++ .../LaunchManager/Steam.Windows.cs | 41 ++ RLBotCSTests/TestTomls/default.toml | 2 +- 10 files changed, 943 insertions(+), 497 deletions(-) delete mode 100644 RLBotCS/ManagerTools/LaunchManager.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/CustomLaunchers.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/Epic.Linux.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/RocketLeagueLauncher.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs create mode 100644 RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs diff --git a/RLBotCS/ManagerTools/LaunchManager.cs b/RLBotCS/ManagerTools/LaunchManager.cs deleted file mode 100644 index 5a4f622f..00000000 --- a/RLBotCS/ManagerTools/LaunchManager.cs +++ /dev/null @@ -1,496 +0,0 @@ -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using Microsoft.Win32; - -namespace RLBotCS.ManagerTools; - -static class LaunchManager -{ - private const string SteamGameId = "252950"; - public const int RlbotSocketsPort = 23234; - private const int DefaultGamePort = 50000; - private const int IdealGamePort = 23233; - - private static readonly ILogger Logger = Logging.GetLogger("LaunchManager"); - - public static string? GetGameArgs() - { - Process[] candidates = Process.GetProcesses(); - - foreach (var candidate in candidates) - { - if (!candidate.ProcessName.Contains("RocketLeague")) - continue; - - return GetProcessArgs(candidate); - } - - return null; - } - - public static void KillGame() - { - Process[] candidates = Process.GetProcesses(); - - foreach (var candidate in candidates) - { - if (!candidate.ProcessName.Contains("RocketLeague")) - continue; - - candidate.Kill(); - } - } - - public static int FindUsableGamePort(int rlbotSocketsPort) - { - Process[] candidates = Process.GetProcessesByName("RocketLeague"); - - // Search cmd line args for port - foreach (var candidate in candidates) - { - string[] args = GetProcessArgs(candidate).Split(" "); - foreach (var arg in args) - if (arg.Contains("RLBot_ControllerURL")) - { - string[] parts = arg.Split(':'); - var port = parts[^1].TrimEnd('"'); - return int.Parse(port); - } - } - - for (int portToTest = IdealGamePort; portToTest < 65535; portToTest++) - { - if (portToTest == rlbotSocketsPort) - // Skip the port we're using for sockets - continue; - - // Try booting up a server on the port - try - { - TcpListener listener = new(IPAddress.Any, portToTest); - listener.Start(); - listener.Stop(); - return portToTest; - } - catch (SocketException) { } - } - - return DefaultGamePort; - } - - private static string GetProcessArgs(Process process) - { -#if WINDOWS - int err = ProcessCommandLine.Retrieve(process, out string commandLine); - - if (err == 0) - return commandLine; - - Logger.LogError( - $"Failed to retrieve command line arguments for process {0}: {1}", - process.ProcessName, - ProcessCommandLine.ErrorToString(err) - ); - return ""; -#else - // Solution taken from: - // https://stackoverflow.com/a/58843225/10930209 - return File.ReadAllText($"/proc/{process.Id}/cmdline"); -#endif - } - - private static string[] GetRLBotArgs(int gamePort) => - [ - "-rlbot", - $"RLBot_ControllerURL=127.0.0.1:{gamePort}", - "RLBot_PacketSendRate=240", - "-nomovie", - ]; - -#if WINDOWS - private static List ParseCommand(string command) - { - // Only works on Windows due to exes on Linux running under Wine - var parts = new List(); - var regex = new Regex(@"(?[\""].+?[\""]|[^ ]+)"); - var matches = regex.Matches(command); - - foreach (Match match in matches) - { - parts.Add(match.Groups["match"].Value.Trim('"')); - } - - return parts; - } -#endif - - private static Process RunCommandInShell(string command) - { - Process process = new(); - -#if WINDOWS - process.StartInfo.FileName = "cmd.exe"; - process.StartInfo.Arguments = $"/c {command}"; -#else - process.StartInfo.FileName = "/bin/sh"; - process.StartInfo.Arguments = $"-c \"{command}\""; -#endif - - return process; - } - - private static void ApplyEnvironment( - ProcessStartInfo startInfo, - List environment - ) - { - foreach (var variable in environment) - { - startInfo.EnvironmentVariables[variable.Name] = variable.Value; - } - } - - private static void LaunchGameViaLegendary() - { - Process legendary = RunCommandInShell( - "legendary launch Sugar -rlbot RLBot_ControllerURL=127.0.0.1:23233 RLBot_PacketSendRate=240 -nomovie" - ); - legendary.Start(); - } - - private static void LaunchGameViaHeroic() - { - Process heroic; - -#if WINDOWS - heroic = RunCommandInShell( - "start \"\" \"heroic://launch?appName=Sugar&runner=legendary&arg=-rlbot&arg=RLBot_ControllerURL%3D127.0.0.1%3A23233&arg=RLBot_PacketSendRate%3D240&arg=-nomovie\"" - ); -#else - heroic = RunCommandInShell( - "xdg-open 'heroic://launch?appName=Sugar&runner=legendary&arg=-rlbot&arg=RLBot_ControllerURL%3D127.0.0.1%3A23233&arg=RLBot_PacketSendRate%3D240&arg=-nomovie'" - ); -#endif - - heroic.Start(); - } - - public static void LaunchBots( - List bots, - int rlbotSocketsPort - ) - { - foreach (var bot in bots) - { - var details = bot.Variety.AsCustomBot(); - - if (details.RunCommand == "") - { - Logger.LogWarning("Bot {} must be started manually.", details.Name); - continue; - } - - Process botProcess = RunCommandInShell(details.RunCommand); - - botProcess.StartInfo.WorkingDirectory = details.RootDir; - ApplyEnvironment(botProcess.StartInfo, details.Environment); - botProcess.StartInfo.EnvironmentVariables["RLBOT_AGENT_ID"] = details.AgentId; - botProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = - rlbotSocketsPort.ToString(); - botProcess.EnableRaisingEvents = true; - - botProcess.Exited += (_, _) => - { - if (botProcess.ExitCode != 0) - { - Logger.LogError( - "Bot {0} exited with error code {1}. See previous logs for more information.", - details.Name, - botProcess.ExitCode - ); - } - }; - - try - { - botProcess.Start(); - Logger.LogInformation("Launched bot: {}", details.Name); - } - catch (Exception e) - { - Logger.LogError($"Failed to launch bot {details.Name}: {e.Message}"); - } - } - } - - public static void LaunchScripts( - List scripts, - int rlbotSocketsPort - ) - { - foreach (var script in scripts) - { - if (script.RunCommand == "") - { - Logger.LogWarning("Script {} must be started manually.", script.Name); - continue; - } - - Process scriptProcess = RunCommandInShell(script.RunCommand); - - if (script.RootDir != "") - scriptProcess.StartInfo.WorkingDirectory = script.RootDir; - - ApplyEnvironment(scriptProcess.StartInfo, script.Environment); - scriptProcess.StartInfo.EnvironmentVariables["RLBOT_AGENT_ID"] = script.AgentId; - scriptProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = - rlbotSocketsPort.ToString(); - scriptProcess.EnableRaisingEvents = true; - - scriptProcess.Exited += (_, _) => - { - if (scriptProcess.ExitCode != 0) - { - Logger.LogError( - "Script {0} exited with error code {1}. See previous logs for more information.", - script.Name, - scriptProcess.ExitCode - ); - } - }; - - try - { - scriptProcess.Start(); - Logger.LogInformation("Launched script: {}", script.Name); - } - catch (Exception e) - { - Logger.LogError($"Failed to launch script: {e.Message}"); - } - } - } - - public static void LaunchRocketLeague( - RLBot.Flat.Launcher launcherPref, - string extraArg, - int gamePort - ) - { -#if WINDOWS - switch (launcherPref) - { - case RLBot.Flat.Launcher.Steam: - string steamPath = GetWindowsSteamPath(); - Process steam = new(); - steam.StartInfo.FileName = steamPath; - steam.StartInfo.Arguments = - $"-applaunch {SteamGameId} " + string.Join(" ", GetRLBotArgs(gamePort)); - - Logger.LogInformation( - $"Starting Rocket League with steam: {steamPath} {steam.StartInfo.Arguments}" - ); - steam.Start(); - break; - case RLBot.Flat.Launcher.Epic: - if (IsRocketLeagueRunningWithArgs()) - { - return; - } - - if (IsRocketLeagueRunning()) - { - Logger.LogError( - "Please close Rocket League so RLBot can start it in RLBot mode." - ); - return; - } - - // To launch RocketLeague for Epic we need some extra login parameters from Epic. - // We get these by launching the game normally, reading the args, and then closing it again. - - 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(); - Thread.Sleep(500); - - // Get login args - Logger.LogInformation("Finding Rocket League..."); - string? epicArgs = null; - int triesLeft = 40; - while (epicArgs is null && triesLeft-- > 0) - { - epicArgs = GetGameArgs(); - Thread.Sleep(500); - } - KillGame(); - if (epicArgs is null) - throw new Exception("Failed to get Rocket League args"); - Logger.LogDebug("Epic RocketLeague args: {}", epicArgs); - epicArgs = Regex.Replace(epicArgs, "\".*\"", "").Replace("\"\"", "").Trim(); - - // Get the game path from launch logs - WinReadLog logReader = new(); - (string, string)? pathAndAuth = null; - while (pathAndAuth is null) - { - pathAndAuth = logReader.GetGamePathAndAuth(); - Thread.Sleep(500); - } - if (pathAndAuth is null) - throw new Exception("Failed to get Rocket League exe path"); - string directGamePath = pathAndAuth.Value.Item1; - Logger.LogInformation($"Found Rocket League at \"{directGamePath}\""); - - // Wait for the game to fully close - Logger.LogDebug("Waiting for Rocket League to fully close..."); - while (IsRocketLeagueRunning()) - Thread.Sleep(500); - - string rlbotArgs = string.Join(" ", GetRLBotArgs(gamePort)); - string modifiedArgs = $"\"{directGamePath}\" {rlbotArgs} {epicArgs}"; - - // 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; - - Logger.LogInformation($"Starting Rocket League with Epic: {rlbotArgs}"); - Logger.LogDebug( - "Full command: {} {}", - epicRocketLeague.StartInfo.FileName, - epicRocketLeague.StartInfo.Arguments - ); - epicRocketLeague.Start(); - - // If we don't read the output, the game will hang - new Thread(() => - { - epicRocketLeague.StandardOutput.ReadToEnd(); - }).Start(); - - break; - case RLBot.Flat.Launcher.Custom: - if (extraArg.Equals("legendary", StringComparison.OrdinalIgnoreCase)) - { - LaunchGameViaLegendary(); - return; - } - else if (extraArg.Equals("heroic", StringComparison.OrdinalIgnoreCase)) - { - LaunchGameViaHeroic(); - return; - } - - throw new NotSupportedException($"Unexpected launcher, \"{extraArg}\""); - case RLBot.Flat.Launcher.NoLaunch: - break; - } -#else - switch (launcherPref) - { - case RLBot.Flat.Launcher.Steam: - string args = string.Join("%20", GetRLBotArgs(gamePort)); - Process rocketLeague = new(); - rocketLeague.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; - rocketLeague.StartInfo.FileName = "steam"; - rocketLeague.StartInfo.Arguments = $"steam://rungameid/{SteamGameId}//{args}"; - - Logger.LogInformation( - $"Starting Rocket League via Steam CLI with {rocketLeague.StartInfo.Arguments}" - ); - rocketLeague.Start(); - break; - case RLBot.Flat.Launcher.Epic: - throw new NotSupportedException( - "Epic Games Store is not directly supported on Linux." - ); - case RLBot.Flat.Launcher.Custom: - if (extraArg.Equals("legendary", StringComparison.OrdinalIgnoreCase)) - { - LaunchGameViaLegendary(); - return; - } - else if (extraArg.Equals("heroic", StringComparison.OrdinalIgnoreCase)) - { - LaunchGameViaHeroic(); - return; - } - - throw new NotSupportedException($"Unexpected launcher, \"{extraArg}\""); - case RLBot.Flat.Launcher.NoLaunch: - break; - } -#endif - } - - public static string? GetRocketLeaguePath() - { - // Assumes the game has already been launched - string? args = GetGameArgs(); - if (args is null) - return null; - - string directGamePath; - -#if WINDOWS - directGamePath = ParseCommand(args)[0]; -#else - // On Linux, Rocket League is running under Wine so args is something like - // Z:\home\username\.steam\debian-installation\steamapps\common\rocketleague\Binaries\Win64\RocketLeague.exe-rlbotRLBot_ControllerURL=127.0.0.1:23233RLBot_PacketSendRate=240-nomovie - // and we must get the real path to RocketLeague.exe from this - directGamePath = args.Remove(0, 2).Split("-rlbot")[0].Replace("\\", "/"); -#endif - - return directGamePath; - } - - public static bool IsRocketLeagueRunning() => - Process - .GetProcesses() - .Any(candidate => candidate.ProcessName.Contains("RocketLeague")); - - public static bool IsRocketLeagueRunningWithArgs() - { - Process[] candidates = Process.GetProcesses(); - - foreach (var candidate in candidates) - { - if (!candidate.ProcessName.Contains("RocketLeague")) - continue; - - var args = GetProcessArgs(candidate); - if (args.Contains("rlbot")) - return true; - } - - return false; - } - - private static string GetWindowsSteamPath() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException( - "Getting Windows path on non-Windows platform" - ); - - using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); - if (key?.GetValue("SteamExe")?.ToString() is { } value) - return value; - - throw new FileNotFoundException( - "Could not find registry entry for SteamExe. Is Steam installed?" - ); - } -} diff --git a/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs b/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs new file mode 100644 index 00000000..7764cbe5 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + public static void LaunchBots( + List bots, + int rlbotSocketsPort + ) + { + foreach (var bot in bots) + { + var details = bot.Variety.AsCustomBot(); + + if (details.RunCommand == "") + { + Logger.LogWarning("Bot {} must be started manually.", details.Name); + continue; + } + + Process botProcess = RunCommandInShell(details.RunCommand); + + botProcess.StartInfo.WorkingDirectory = details.RootDir; + ApplyEnvironment(botProcess.StartInfo, details.Environment); + botProcess.StartInfo.EnvironmentVariables["RLBOT_AGENT_ID"] = details.AgentId; + botProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = + rlbotSocketsPort.ToString(); + botProcess.EnableRaisingEvents = true; + + botProcess.Exited += (_, _) => + { + if (botProcess.ExitCode != 0) + { + Logger.LogError( + "Bot {0} exited with error code {1}. See previous logs for more information.", + details.Name, + botProcess.ExitCode + ); + } + }; + + try + { + botProcess.Start(); + Logger.LogInformation("Launched bot: {}", details.Name); + } + catch (Exception e) + { + Logger.LogError($"Failed to launch bot {details.Name}: {e.Message}"); + } + } + } + + public static void LaunchScripts( + List scripts, + int rlbotSocketsPort + ) + { + foreach (var script in scripts) + { + if (script.RunCommand == "") + { + Logger.LogWarning("Script {} must be started manually.", script.Name); + continue; + } + + Process scriptProcess = RunCommandInShell(script.RunCommand); + + if (script.RootDir != "") + scriptProcess.StartInfo.WorkingDirectory = script.RootDir; + + ApplyEnvironment(scriptProcess.StartInfo, script.Environment); + scriptProcess.StartInfo.EnvironmentVariables["RLBOT_AGENT_ID"] = script.AgentId; + scriptProcess.StartInfo.EnvironmentVariables["RLBOT_SERVER_PORT"] = + rlbotSocketsPort.ToString(); + scriptProcess.EnableRaisingEvents = true; + + scriptProcess.Exited += (_, _) => + { + if (scriptProcess.ExitCode != 0) + { + Logger.LogError( + "Script {0} exited with error code {1}. See previous logs for more information.", + script.Name, + scriptProcess.ExitCode + ); + } + }; + + try + { + scriptProcess.Start(); + Logger.LogInformation("Launched script: {}", script.Name); + } + catch (Exception e) + { + Logger.LogError($"Failed to launch script: {e.Message}"); + } + } + } +} diff --git a/RLBotCS/ManagerTools/LaunchManager/CustomLaunchers.cs b/RLBotCS/ManagerTools/LaunchManager/CustomLaunchers.cs new file mode 100644 index 00000000..87b44a53 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/CustomLaunchers.cs @@ -0,0 +1,47 @@ +using System.Diagnostics; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static void LaunchGameViaLegendary() + { + Process legendary = RunCommandInShell( + "legendary launch Sugar -rlbot RLBot_ControllerURL=127.0.0.1:23233 RLBot_PacketSendRate=240 -nomovie" + ); + legendary.Start(); + } + + private static void LaunchGameViaHeroic() + { + Process heroic; + +#if WINDOWS + heroic = RunCommandInShell( + "start \"\" \"heroic://launch?appName=Sugar&runner=legendary&arg=-rlbot&arg=RLBot_ControllerURL%3D127.0.0.1%3A23233&arg=RLBot_PacketSendRate%3D240&arg=-nomovie\"" + ); +#else + heroic = RunCommandInShell( + "xdg-open 'heroic://launch?appName=Sugar&runner=legendary&arg=-rlbot&arg=RLBot_ControllerURL%3D127.0.0.1%3A23233&arg=RLBot_PacketSendRate%3D240&arg=-nomovie'" + ); +#endif + + heroic.Start(); + } + + private static void LaunchCustomLauncher(string extraArg) + { + if (extraArg.Equals("legendary", StringComparison.OrdinalIgnoreCase)) + { + LaunchGameViaLegendary(); + } + else if (extraArg.Equals("heroic", StringComparison.OrdinalIgnoreCase)) + { + LaunchGameViaHeroic(); + } + else + { + throw new NotSupportedException($"Unexpected launcher, \"{extraArg}\""); + } + } +} diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Linux.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Linux.cs new file mode 100644 index 00000000..aca24376 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Linux.cs @@ -0,0 +1,13 @@ +#if !WINDOWS +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static void LaunchGameViaEpic(int gamePort) + { + throw new NotSupportedException( + "Epic Games Store is not directly supported on Linux." + ); + } +} +#endif diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs new file mode 100644 index 00000000..1ecc0ffb --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -0,0 +1,92 @@ +#if WINDOWS +using System.Diagnostics; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static void LaunchGameViaEpic(int gamePort) + { + if (IsRocketLeagueRunningWithArgs()) + return; + + if (IsRocketLeagueRunning()) + { + Logger.LogError("Please close Rocket League so RLBot can start it in RLBot mode."); + return; + } + + // To launch RocketLeague for Epic we need some extra login parameters from Epic. + // We get these by launching the game normally, reading the args, and then closing it again. + + 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(); + Thread.Sleep(500); + + // Get login args + Logger.LogInformation("Finding Rocket League..."); + string? epicArgs = null; + int triesLeft = 40; + while (epicArgs is null && triesLeft-- > 0) + { + epicArgs = GetGameArgs(); + Thread.Sleep(500); + } + KillGame(); + if (epicArgs is null) + throw new Exception("Failed to get Rocket League args"); + Logger.LogDebug("Epic RocketLeague args: {}", epicArgs); + epicArgs = Regex.Replace(epicArgs, "\".*\"", "").Replace("\"\"", "").Trim(); + + // Get the game path from launch logs + WinReadLog logReader = new(); + (string, string)? pathAndAuth = null; + while (pathAndAuth is null) + { + pathAndAuth = logReader.GetGamePathAndAuth(); + Thread.Sleep(500); + } + if (pathAndAuth is null) + throw new Exception("Failed to get Rocket League exe path"); + string directGamePath = pathAndAuth.Value.Item1; + Logger.LogInformation($"Found Rocket League at \"{directGamePath}\""); + + // Wait for the game to fully close + Logger.LogDebug("Waiting for Rocket League to fully close..."); + while (IsRocketLeagueRunning()) + Thread.Sleep(500); + + string rlbotArgs = string.Join(" ", GetRLBotArgs(gamePort)); + string modifiedArgs = $"\"{directGamePath}\" {rlbotArgs} {epicArgs}"; + + // 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; + + Logger.LogInformation($"Starting Rocket League with Epic: {rlbotArgs}"); + Logger.LogDebug( + "Full command: {} {}", + epicRocketLeague.StartInfo.FileName, + epicRocketLeague.StartInfo.Arguments + ); + epicRocketLeague.Start(); + + // If we don't read the output, the game will hang + new Thread(() => + { + epicRocketLeague.StandardOutput.ReadToEnd(); + }).Start(); + } +} +#endif diff --git a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs new file mode 100644 index 00000000..d93722dd --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs @@ -0,0 +1,183 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +#if WINDOWS +using System.Text.RegularExpressions; +#endif + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ +#if WINDOWS + private static List ParseCommand(string command) + { + // Only works on Windows due to exes on Linux running under Wine + var parts = new List(); + var regex = new Regex(@"(?[""].+?[""]|[^ ]+)"); + var matches = regex.Matches(command); + + foreach (Match match in matches) + { + parts.Add(match.Groups["match"].Value.Trim('"')); + } + + return parts; + } +#endif + + private static Process RunCommandInShell(string command) + { + Process process = new(); + +#if WINDOWS + process.StartInfo.FileName = "cmd.exe"; + process.StartInfo.Arguments = $"/c {command}"; +#else + process.StartInfo.FileName = "/bin/sh"; + process.StartInfo.Arguments = $"-c \"{command}\""; +#endif + + return process; + } + + private static void ApplyEnvironment( + ProcessStartInfo startInfo, + List environment + ) + { + foreach (var variable in environment) + { + startInfo.EnvironmentVariables[variable.Name] = variable.Value; + } + } + + public static string? GetGameArgs() + { + Process[] candidates = Process.GetProcesses(); + + foreach (var candidate in candidates) + { + if (!candidate.ProcessName.Contains("RocketLeague")) + continue; + + return GetProcessArgs(candidate); + } + + return null; + } + + public static void KillGame() + { + Process[] candidates = Process.GetProcesses(); + + foreach (var candidate in candidates) + { + if (!candidate.ProcessName.Contains("RocketLeague")) + continue; + + candidate.Kill(); + } + } + + public static int FindUsableGamePort(int rlbotSocketsPort) + { + Process[] candidates = Process.GetProcessesByName("RocketLeague"); + + // Search cmd line args for port + foreach (var candidate in candidates) + { + string[] args = GetProcessArgs(candidate).Split(" "); + foreach (var arg in args) + if (arg.Contains("RLBot_ControllerURL")) + { + string[] parts = arg.Split(':'); + var port = parts[^1].TrimEnd('"'); + return int.Parse(port); + } + } + + for (int portToTest = IdealGamePort; portToTest < 65535; portToTest++) + { + if (portToTest == rlbotSocketsPort) + // Skip the port we're using for sockets + continue; + + // Try booting up a server on the port + try + { + TcpListener listener = new(IPAddress.Any, portToTest); + listener.Start(); + listener.Stop(); + return portToTest; + } + catch (SocketException) { } + } + + return DefaultGamePort; + } + + private static string GetProcessArgs(Process process) + { +#if WINDOWS + int err = ProcessCommandLine.Retrieve(process, out string commandLine); + + if (err == 0) + return commandLine; + + Logger.LogError( + $"Failed to retrieve command line arguments for process {0}: {1}", + process.ProcessName, + ProcessCommandLine.ErrorToString(err) + ); + return ""; +#else + // Solution taken from: + // https://stackoverflow.com/a/58843225/10930209 + return File.ReadAllText($"/proc/{process.Id}/cmdline"); +#endif + } + + public static string? GetRocketLeaguePath() + { + // Assumes the game has already been launched + string? args = GetGameArgs(); + if (args is null) + return null; + + string directGamePath; + +#if WINDOWS + directGamePath = ParseCommand(args)[0]; +#else + // On Linux, Rocket League is running under Wine so args is something like + // Z:\home\username\.steam\debian-installation\steamapps\common\rocketleague\Binaries\Win64\RocketLeague.exe-rlbotRLBot_ControllerURL=127.0.0.1:23233RLBot_PacketSendRate=240-nomovie + // and we must get the real path to RocketLeague.exe from this + directGamePath = args.Remove(0, 2).Split("-rlbot")[0].Replace("\\", "/"); +#endif + + return directGamePath; + } + + public static bool IsRocketLeagueRunning() => + Process + .GetProcesses() + .Any(candidate => candidate.ProcessName.Contains("RocketLeague")); + + public static bool IsRocketLeagueRunningWithArgs() + { + Process[] candidates = Process.GetProcesses(); + + foreach (var candidate in candidates) + { + if (!candidate.ProcessName.Contains("RocketLeague")) + continue; + + var args = GetProcessArgs(candidate); + if (args.Contains("rlbot")) + return true; + } + + return false; + } +} diff --git a/RLBotCS/ManagerTools/LaunchManager/RocketLeagueLauncher.cs b/RLBotCS/ManagerTools/LaunchManager/RocketLeagueLauncher.cs new file mode 100644 index 00000000..5d63915a --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/RocketLeagueLauncher.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Logging; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private const string SteamGameId = "252950"; + public const int RlbotSocketsPort = 23234; + private const int DefaultGamePort = 50000; + private const int IdealGamePort = 23233; + + private static readonly ILogger Logger = Logging.GetLogger("LaunchManager"); + + private static string[] GetRLBotArgs(int gamePort) => + [ + "-rlbot", + $"RLBot_ControllerURL=127.0.0.1:{gamePort}", + "RLBot_PacketSendRate=240", + "-nomovie", + ]; + + public static void LaunchRocketLeague( + RLBot.Flat.Launcher launcherPref, + string extraArg, + int gamePort + ) + { + switch (launcherPref) + { + case RLBot.Flat.Launcher.Steam: + LaunchGameViaSteam(gamePort); + break; + case RLBot.Flat.Launcher.Epic: + LaunchGameViaEpic(gamePort); + break; + case RLBot.Flat.Launcher.Custom: + LaunchCustomLauncher(extraArg); + break; + case RLBot.Flat.Launcher.NoLaunch: + break; + } + } +} diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs new file mode 100644 index 00000000..41d12940 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs @@ -0,0 +1,420 @@ +#if !WINDOWS +using System.Diagnostics; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static string? ResolveDirectorySymlink(string path) + { + try + { + if (!Directory.Exists(path)) + return null; + + var info = new DirectoryInfo(path); + var resolved = info.LinkTarget != null ? info.ResolveLinkTarget(true) : info; + return resolved?.FullName; + } + catch + { + return null; + } + } + + private static bool IsValidSteamRoot(string path) + { + return Directory.Exists(Path.Combine(path, "steamapps")); + } + + private static void AddSteamRoot(string? path, List roots) + { + if (!string.IsNullOrEmpty(path) && IsValidSteamRoot(path) && !roots.Contains(path)) + { + roots.Add(path); + } + } + + private static List GetLinuxSteamRoots() + { + List roots = []; + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + + // Steam creates these symlinks pointing to the actual installation. + // Resolving them is the most reliable way to find the real root. + AddSteamRoot(ResolveDirectorySymlink(Path.Combine(home, ".steam", "steam")), roots); + AddSteamRoot(ResolveDirectorySymlink(Path.Combine(home, ".steam", "root")), roots); + + // Standard XDG data location. + string xdgDataHome = + Environment.GetEnvironmentVariable("XDG_DATA_HOME") + ?? Path.Combine(home, ".local", "share"); + AddSteamRoot(Path.Combine(xdgDataHome, "Steam"), roots); + + // Sandbox/ alternative distribution paths. + AddSteamRoot( + Path.Combine( + home, + ".var", + "app", + "com.valvesoftware.Steam", + ".local", + "share", + "Steam" + ), + roots + ); + AddSteamRoot( + Path.Combine(home, "snap", "steam", "common", ".local", "share", "Steam"), + roots + ); + + // Last resort: locate the steam binary in PATH and derive the root from it. + AddSteamRoot(FindSteamRootFromPath(), roots); + + return roots; + } + + private static string? FindSteamRootFromPath() + { + string? pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(pathEnv)) + return null; + + foreach (var dir in pathEnv.Split(':')) + { + string steamExe = Path.Combine(dir, "steam"); + if (!File.Exists(steamExe)) + continue; + + try + { + var info = new FileInfo(steamExe); + var resolved = info.LinkTarget != null ? info.ResolveLinkTarget(true) : info; + string? target = resolved?.FullName; + if (target == null) + continue; + + // The binary is usually at /bin/steam or /steam. + DirectoryInfo? parent = Directory.GetParent(target); + if (parent?.Name == "bin") + parent = parent.Parent; + + if (parent != null && IsValidSteamRoot(parent.FullName)) + return parent.FullName; + } + catch + { + // Ignore individual PATH entries that cannot be inspected. + } + } + + return null; + } + + private static List GetLinuxSteamLibraryFolders() + { + var folders = new List(); + + foreach (var steamRoot in GetLinuxSteamRoots()) + { + if (!folders.Contains(steamRoot)) + folders.Add(steamRoot); + + string vdfPath = Path.Combine(steamRoot, "steamapps", "libraryfolders.vdf"); + if (!File.Exists(vdfPath)) + continue; + + try + { + string vdf = File.ReadAllText(vdfPath); + var matches = Regex.Matches(vdf, @"""path""\s+""([^""]+)"""); + foreach (Match match in matches) + { + string path = match.Groups[1].Value; + if (Directory.Exists(path) && !folders.Contains(path)) + folders.Add(path); + } + } + catch (Exception e) + { + Logger.LogWarning($"Failed to read Steam libraryfolders.vdf: {e.Message}"); + } + } + + return folders; + } + + private static string? FindLinuxRocketLeaguePath() + { + foreach (var folder in GetLinuxSteamLibraryFolders()) + { + string manifestPath = Path.Combine( + folder, + "steamapps", + $"appmanifest_{SteamGameId}.acf" + ); + if (!File.Exists(manifestPath)) + continue; + + try + { + string manifest = File.ReadAllText(manifestPath); + var match = Regex.Match(manifest, @"""installdir""\s+""([^""]+)"""); + if (!match.Success) + continue; + + string installDir = match.Groups[1].Value; + string exePath = Path.Combine( + folder, + "steamapps", + "common", + installDir, + "Binaries", + "Win64", + "RocketLeague.exe" + ); + if (File.Exists(exePath)) + return exePath; + } + catch (Exception e) + { + Logger.LogWarning($"Failed to read Rocket League appmanifest: {e.Message}"); + } + } + + return null; + } + + private static Version? GetProtonVersion(string protonDir) + { + string name = Path.GetFileName(protonDir); + var match = Regex.Match(name, @"Proton\s+(\d+(?:\.\d+)*)", RegexOptions.IgnoreCase); + if (match.Success && Version.TryParse(match.Groups[1].Value, out var version)) + return version; + return null; + } + + private static string? GetConfiguredProtonToolName() + { + foreach (var steamRoot in GetLinuxSteamRoots()) + { + string configPath = Path.Combine(steamRoot, "config", "config.vdf"); + if (!File.Exists(configPath)) + continue; + + try + { + string config = File.ReadAllText(configPath); + var match = Regex.Match( + config, + @"""CompatToolMapping""[\s\S]*?""252950""\s*\{\s*""name""\s*""([^""]+)""", + RegexOptions.Singleline + ); + if (match.Success) + { + string toolName = match.Groups[1].Value; + Logger.LogInformation( + $"Steam compatibility tool for Rocket League: {toolName}" + ); + return toolName; + } + + Logger.LogInformation( + "No compatibility tool entry found in Steam config for Rocket League." + ); + } + catch (Exception e) + { + Logger.LogWarning($"Failed to read Steam config.vdf: {e.Message}"); + } + } + + return null; + } + + private static string? GetProtonToolNameFromManifest(string protonDir) + { + string manifestPath = Path.Combine(protonDir, "toolmanifest.vdf"); + if (!File.Exists(manifestPath)) + return null; + + try + { + string manifest = File.ReadAllText(manifestPath); + var match = Regex.Match(manifest, @"""nameid""\s*""([^""]+)"""); + if (match.Success) + return match.Groups[1].Value; + + match = Regex.Match(manifest, @"""name""\s*""([^""]+)"""); + if (match.Success) + return match.Groups[1].Value; + } + catch { } + + return null; + } + + private static string NormalizeProtonName(string name) + { + return Regex.Replace(name.ToLowerInvariant(), @"[^a-z0-9]+", ""); + } + + private static List? ExtractProtonVersion(string name) + { + var matches = Regex.Matches(name, @"\d+"); + if (matches.Count == 0) + return null; + return matches.Select(m => int.Parse(m.Value)).ToList(); + } + + private static bool ToolNameMatchesDirectory(string toolName, string protonDir) + { + string? manifestName = GetProtonToolNameFromManifest(protonDir); + if ( + manifestName != null + && manifestName.Equals(toolName, StringComparison.OrdinalIgnoreCase) + ) + return true; + + string dirName = Path.GetFileName(protonDir); + + // Try a normalized match, e.g. "proton_10_4" == "Proton 10.4". + if (NormalizeProtonName(toolName) == NormalizeProtonName(dirName)) + return true; + + // Try matching the numeric version components (common prefix). + var toolVersion = ExtractProtonVersion(toolName); + var dirVersion = ExtractProtonVersion(dirName); + if (toolVersion != null && dirVersion != null) + { + int commonLength = Math.Min(toolVersion.Count, dirVersion.Count); + if (toolVersion.Take(commonLength).SequenceEqual(dirVersion.Take(commonLength))) + return true; + } + + // Special names like Hotfix / Experimental. + if ( + toolName.Contains("hotfix", StringComparison.OrdinalIgnoreCase) + && dirName.Contains("Hotfix", StringComparison.OrdinalIgnoreCase) + ) + return true; + + if ( + toolName.Contains("experimental", StringComparison.OrdinalIgnoreCase) + && dirName.Contains("Experimental", StringComparison.OrdinalIgnoreCase) + ) + return true; + + return false; + } + + private static string? FindLinuxProtonPath() + { + string? configuredTool = GetConfiguredProtonToolName(); + + string? newestProtonPath = null; + Version? newestVersion = null; + string? anyProtonPath = null; + + foreach (var folder in GetLinuxSteamLibraryFolders()) + { + string commonPath = Path.Combine(folder, "steamapps", "common"); + if (!Directory.Exists(commonPath)) + continue; + + foreach (var protonDir in Directory.GetDirectories(commonPath, "Proton*")) + { + string protonExe = Path.Combine(protonDir, "proton"); + if (!File.Exists(protonExe)) + continue; + + anyProtonPath ??= protonExe; + + // Prefer the Proton version Steam has configured for Rocket League. + if ( + configuredTool != null + && ToolNameMatchesDirectory(configuredTool, protonDir) + ) + { + Logger.LogInformation($"Using configured Proton: {protonDir}"); + return protonExe; + } + + // Track the newest numeric Proton as a fallback. + Version? version = GetProtonVersion(protonDir); + if (version != null && (newestVersion == null || version > newestVersion)) + { + newestVersion = version; + newestProtonPath = protonExe; + } + } + } + + string? fallbackPath = newestProtonPath ?? anyProtonPath; + if (fallbackPath != null) + { + Logger.LogInformation($"Falling back to installed Proton: {fallbackPath}"); + return fallbackPath; + } + + return null; + } + + private static void LaunchGameViaSteam(int gamePort) + { + string? gamePath = FindLinuxRocketLeaguePath(); + if (gamePath == null) + throw new FileNotFoundException( + "Could not find Rocket League installation. Ensure Rocket League is installed via Steam." + ); + + string? protonPath = FindLinuxProtonPath(); + if (protonPath == null) + throw new FileNotFoundException( + "Could not find Proton installation. Ensure a Proton version is installed via Steam." + ); + + string? compatDataPath = null; + foreach (var folder in GetLinuxSteamLibraryFolders()) + { + if (gamePath.StartsWith(Path.Combine(folder, "steamapps", "common"))) + { + compatDataPath = Path.Combine(folder, "steamapps", "compatdata", SteamGameId); + break; + } + } + + if (compatDataPath == null) + throw new DirectoryNotFoundException("Could not find Steam compatdata directory."); + + string? steamClientPath = GetLinuxSteamRoots().FirstOrDefault(); + if (steamClientPath == null) + throw new DirectoryNotFoundException("Could not find Steam installation."); + + string args = string.Join(" ", GetRLBotArgs(gamePort)); + + Process rocketLeague = new(); + rocketLeague.StartInfo.FileName = protonPath; + rocketLeague.StartInfo.ArgumentList.Add("run"); + rocketLeague.StartInfo.ArgumentList.Add(gamePath); + foreach (var arg in GetRLBotArgs(gamePort)) + rocketLeague.StartInfo.ArgumentList.Add(arg); + rocketLeague.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + + rocketLeague.StartInfo.Environment["STEAM_COMPAT_CLIENT_INSTALL_PATH"] = + steamClientPath; + rocketLeague.StartInfo.Environment["STEAM_COMPAT_DATA_PATH"] = compatDataPath; + rocketLeague.StartInfo.Environment["STEAM_COMPAT_APP_ID"] = SteamGameId; + rocketLeague.StartInfo.Environment["SteamAppId"] = SteamGameId; + rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; + + Logger.LogInformation( + $"Starting Rocket League via Proton without EAC: {protonPath} run {gamePath} {args}" + ); + rocketLeague.Start(); + } +} +#endif diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs new file mode 100644 index 00000000..e07a9530 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -0,0 +1,41 @@ +#if WINDOWS +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static string GetWindowsSteamPath() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + throw new PlatformNotSupportedException( + "Getting Windows path on non-Windows platform" + ); + + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); + if (key?.GetValue("SteamExe")?.ToString() is { } value) + return value; + + throw new FileNotFoundException( + "Could not find registry entry for SteamExe. Is Steam installed?" + ); + } + + private static void LaunchGameViaSteam(int gamePort) + { + string steamPath = GetWindowsSteamPath(); + Process steam = new(); + steam.StartInfo.FileName = steamPath; + steam.StartInfo.Arguments = + $"-applaunch {SteamGameId} " + string.Join(" ", GetRLBotArgs(gamePort)); + + Logger.LogInformation( + $"Starting Rocket League with steam: {steamPath} {steam.StartInfo.Arguments}" + ); + steam.Start(); + } +} +#endif diff --git a/RLBotCSTests/TestTomls/default.toml b/RLBotCSTests/TestTomls/default.toml index 0ee9fefd..88a2abcd 100644 --- a/RLBotCSTests/TestTomls/default.toml +++ b/RLBotCSTests/TestTomls/default.toml @@ -86,7 +86,7 @@ demolish_score = "Zero" # "One", "Zero", "Two", "Three", "Five", "Ten" normal_goal_score = "One" # "One", "Zero", "Two", "Three", "Five", "Ten" -aerial_goal_score = "One" +aerial_goal_score = "One" # "Zero", "One", "Two", "Three" assist_goal_score = "Zero" # "Default", "Backwards" From 445fce5d6c0c53f796118f9785ae998de4dc2925 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 25 Jun 2026 19:12:24 -0500 Subject: [PATCH 02/10] Add Microsoft.Extensions.Logging to fix Windows build --- RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs index d93722dd..c6458841 100644 --- a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; #if WINDOWS using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; #endif namespace RLBotCS.ManagerTools; From 6317efc1e76b069144dd965344b24bf46cbf9559 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 25 Jun 2026 19:42:54 -0500 Subject: [PATCH 03/10] No EAC via Steam on Windows Bump version to v5.0.0-rc.15. --- RLBotCS/Main.cs | 2 +- .../ManagerTools/LaunchManager/BotLauncher.cs | 16 +-- .../LaunchManager/Epic.Windows.cs | 8 +- .../LaunchManager/ProcessUtils.cs | 31 +++-- .../ManagerTools/LaunchManager/Steam.Linux.cs | 5 +- .../LaunchManager/Steam.Windows.cs | 110 ++++++++++++++++-- 6 files changed, 128 insertions(+), 44 deletions(-) diff --git a/RLBotCS/Main.cs b/RLBotCS/Main.cs index 05d209ac..49891965 100644 --- a/RLBotCS/Main.cs +++ b/RLBotCS/Main.cs @@ -10,7 +10,7 @@ if (args.Length > 0 && args[0] == "--version") { Console.WriteLine( - "RLBotServer v5.0.0-rc.14\n" + "RLBotServer v5.0.0-rc.15\n" + $"Bridge {BridgeVersion.Version}\n" + "@ https://www.rlbot.org & https://github.com/RLBot/core" ); diff --git a/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs b/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs index 7764cbe5..8090412c 100644 --- a/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs +++ b/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs @@ -16,7 +16,7 @@ int rlbotSocketsPort if (details.RunCommand == "") { - Logger.LogWarning("Bot {} must be started manually.", details.Name); + Logger.LogWarning($"Bot {details.Name} must be started manually."); continue; } @@ -34,9 +34,7 @@ int rlbotSocketsPort if (botProcess.ExitCode != 0) { Logger.LogError( - "Bot {0} exited with error code {1}. See previous logs for more information.", - details.Name, - botProcess.ExitCode + $"Bot {details.Name} exited with error code {botProcess.ExitCode}. See previous logs for more information." ); } }; @@ -44,7 +42,7 @@ int rlbotSocketsPort try { botProcess.Start(); - Logger.LogInformation("Launched bot: {}", details.Name); + Logger.LogInformation($"Launched bot: {details.Name}"); } catch (Exception e) { @@ -62,7 +60,7 @@ int rlbotSocketsPort { if (script.RunCommand == "") { - Logger.LogWarning("Script {} must be started manually.", script.Name); + Logger.LogWarning($"Script {script.Name} must be started manually."); continue; } @@ -82,9 +80,7 @@ int rlbotSocketsPort if (scriptProcess.ExitCode != 0) { Logger.LogError( - "Script {0} exited with error code {1}. See previous logs for more information.", - script.Name, - scriptProcess.ExitCode + $"Script {script.Name} exited with error code {scriptProcess.ExitCode}. See previous logs for more information." ); } }; @@ -92,7 +88,7 @@ int rlbotSocketsPort try { scriptProcess.Start(); - Logger.LogInformation("Launched script: {}", script.Name); + Logger.LogInformation($"Launched script: {script.Name}"); } catch (Exception e) { diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index 1ecc0ffb..be55f32b 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -40,7 +40,7 @@ private static void LaunchGameViaEpic(int gamePort) KillGame(); if (epicArgs is null) throw new Exception("Failed to get Rocket League args"); - Logger.LogDebug("Epic RocketLeague args: {}", epicArgs); + Logger.LogDebug($"Epic RocketLeague args: {epicArgs}"); epicArgs = Regex.Replace(epicArgs, "\".*\"", "").Replace("\"\"", "").Trim(); // Get the game path from launch logs @@ -51,8 +51,6 @@ private static void LaunchGameViaEpic(int gamePort) pathAndAuth = logReader.GetGamePathAndAuth(); Thread.Sleep(500); } - if (pathAndAuth is null) - throw new Exception("Failed to get Rocket League exe path"); string directGamePath = pathAndAuth.Value.Item1; Logger.LogInformation($"Found Rocket League at \"{directGamePath}\""); @@ -76,9 +74,7 @@ private static void LaunchGameViaEpic(int gamePort) Logger.LogInformation($"Starting Rocket League with Epic: {rlbotArgs}"); Logger.LogDebug( - "Full command: {} {}", - epicRocketLeague.StartInfo.FileName, - epicRocketLeague.StartInfo.Arguments + $"Full command: {epicRocketLeague.StartInfo.FileName} {epicRocketLeague.StartInfo.Arguments}" ); epicRocketLeague.Start(); diff --git a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs index c6458841..0265390c 100644 --- a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs @@ -127,9 +127,7 @@ private static string GetProcessArgs(Process process) return commandLine; Logger.LogError( - $"Failed to retrieve command line arguments for process {0}: {1}", - process.ProcessName, - ProcessCommandLine.ErrorToString(err) + $"Failed to retrieve command line arguments for process {process.ProcessName}: {ProcessCommandLine.ErrorToString(err)}" ); return ""; #else @@ -160,25 +158,24 @@ private static string GetProcessArgs(Process process) return directGamePath; } - public static bool IsRocketLeagueRunning() => - Process + public static bool IsRocketLeagueRunning() + { + return Process .GetProcesses() .Any(candidate => candidate.ProcessName.Contains("RocketLeague")); + } public static bool IsRocketLeagueRunningWithArgs() { - Process[] candidates = Process.GetProcesses(); - - foreach (var candidate in candidates) - { - if (!candidate.ProcessName.Contains("RocketLeague")) - continue; - - var args = GetProcessArgs(candidate); - if (args.Contains("rlbot")) - return true; - } + return Process + .GetProcesses() + .Any(candidate => + { + if (!candidate.ProcessName.Contains("RocketLeague")) + return false; - return false; + var args = GetProcessArgs(candidate); + return args.Contains("rlbot"); + }); } } diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs index 41d12940..749f9415 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs @@ -252,7 +252,10 @@ private static List GetLinuxSteamLibraryFolders() if (match.Success) return match.Groups[1].Value; } - catch { } + catch (Exception e) + { + Logger.LogWarning($"Failed to read tool manifest {protonDir}: {e.Message}"); + } return null; } diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs index e07a9530..c2e0de83 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -1,6 +1,7 @@ #if WINDOWS using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Win32; @@ -24,18 +25,109 @@ private static string GetWindowsSteamPath() ); } - private static void LaunchGameViaSteam(int gamePort) + private static bool IsValidSteamRoot(string path) + { + return Directory.Exists(Path.Combine(path, "steamapps")); + } + + private static List GetWindowsSteamLibraryFolders() { + List folders = []; + string steamPath = GetWindowsSteamPath(); - Process steam = new(); - steam.StartInfo.FileName = steamPath; - steam.StartInfo.Arguments = - $"-applaunch {SteamGameId} " + string.Join(" ", GetRLBotArgs(gamePort)); + string? steamRoot = Path.GetDirectoryName(steamPath); + if (steamRoot != null && IsValidSteamRoot(steamRoot) && !folders.Contains(steamRoot)) + folders.Add(steamRoot); - Logger.LogInformation( - $"Starting Rocket League with steam: {steamPath} {steam.StartInfo.Arguments}" - ); - steam.Start(); + string vdfPath = Path.Combine(steamRoot ?? "", "steamapps", "libraryfolders.vdf"); + if (File.Exists(vdfPath)) + { + try + { + string vdf = File.ReadAllText(vdfPath); + var matches = Regex.Matches(vdf, @"""path""\s+""([^""]+)"""); + foreach (Match match in matches) + { + string path = match.Groups[1].Value; + if (Directory.Exists(path) && !folders.Contains(path)) + folders.Add(path); + } + } + catch (Exception e) + { + Logger.LogWarning($"Failed to read Steam libraryfolders.vdf: {e.Message}"); + } + } + + return folders; + } + + private static string? FindWindowsRocketLeaguePath() + { + foreach (var folder in GetWindowsSteamLibraryFolders()) + { + string manifestPath = Path.Combine( + folder, + "steamapps", + $"appmanifest_{SteamGameId}.acf" + ); + if (!File.Exists(manifestPath)) + continue; + + try + { + string manifest = File.ReadAllText(manifestPath); + var match = Regex.Match(manifest, @"""installdir""\s+""([^""]+)"""); + if (!match.Success) + continue; + + string installDir = match.Groups[1].Value; + string exePath = Path.Combine( + folder, + "steamapps", + "common", + installDir, + "Binaries", + "Win64", + "RocketLeague.exe" + ); + if (File.Exists(exePath)) + return exePath; + } + catch (Exception e) + { + Logger.LogWarning($"Failed to read Rocket League appmanifest: {e.Message}"); + } + } + + return null; + } + + private static void LaunchGameViaSteam(int gamePort) + { + string? gamePath = FindWindowsRocketLeaguePath(); + if (gamePath == null) + throw new FileNotFoundException( + "Could not find Rocket League installation. Ensure Rocket League is installed via Steam." + ); + + string args = string.Join(" ", GetRLBotArgs(gamePort)); + + Process rocketLeague = new(); + rocketLeague.StartInfo.FileName = gamePath; + + foreach (var arg in GetRLBotArgs(gamePort)) + rocketLeague.StartInfo.ArgumentList.Add(arg); + + rocketLeague.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; + + // Let Steamworks initialize by providing the AppId, + // so the game can authenticate with the running Steam client. + rocketLeague.StartInfo.Environment["SteamAppId"] = SteamGameId; + rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; + + Logger.LogInformation($"Starting Rocket League without EAC: {gamePath} {args}"); + rocketLeague.Start(); } } #endif From 0ca32a734cf9450644686a91e08b90b356d02e4d Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 25 Jun 2026 20:29:50 -0500 Subject: [PATCH 04/10] Refactor Epic launch: read path and auth from log Delete old launch log before reading and launch the game directly via its executable path instead of wrapping with cmd.exe. --- .../LaunchManager/Epic.Windows.cs | 32 +++----- RLBotCS/ManagerTools/WinReadLog.cs | 74 ++++++++++++------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index be55f32b..130fac71 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -18,6 +18,10 @@ private static void LaunchGameViaEpic(int gamePort) return; } + // Start with a fresh Launch.log so we don't read stale data from a previous run. + WinReadLog logReader = new(); + logReader.DeleteLog(); + // To launch RocketLeague for Epic we need some extra login parameters from Epic. // We get these by launching the game normally, reading the args, and then closing it again. @@ -28,30 +32,18 @@ private static void LaunchGameViaEpic(int gamePort) launcher.Start(); Thread.Sleep(500); - // Get login args - Logger.LogInformation("Finding Rocket League..."); - string? epicArgs = null; - int triesLeft = 40; - while (epicArgs is null && triesLeft-- > 0) - { - epicArgs = GetGameArgs(); - Thread.Sleep(500); - } - KillGame(); - if (epicArgs is null) - throw new Exception("Failed to get Rocket League args"); - Logger.LogDebug($"Epic RocketLeague args: {epicArgs}"); - epicArgs = Regex.Replace(epicArgs, "\".*\"", "").Replace("\"\"", "").Trim(); - - // Get the game path from launch logs - WinReadLog logReader = new(); + // Get the game path and login info from launch logs (string, string)? pathAndAuth = null; while (pathAndAuth is null) { pathAndAuth = logReader.GetGamePathAndAuth(); Thread.Sleep(500); } + + KillGame(); string directGamePath = pathAndAuth.Value.Item1; + string authArgs = pathAndAuth.Value.Item2; + Logger.LogDebug($"Epic RocketLeague args: {authArgs}"); Logger.LogInformation($"Found Rocket League at \"{directGamePath}\""); // Wait for the game to fully close @@ -60,12 +52,12 @@ private static void LaunchGameViaEpic(int gamePort) Thread.Sleep(500); string rlbotArgs = string.Join(" ", GetRLBotArgs(gamePort)); - string modifiedArgs = $"\"{directGamePath}\" {rlbotArgs} {epicArgs}"; + string modifiedArgs = $"{rlbotArgs} {authArgs}"; // Relaunch the game with the new args Process epicRocketLeague = new(); - epicRocketLeague.StartInfo.FileName = "cmd.exe"; - epicRocketLeague.StartInfo.Arguments = $"/c \"{modifiedArgs}\""; + epicRocketLeague.StartInfo.FileName = directGamePath; + epicRocketLeague.StartInfo.Arguments = modifiedArgs; // Prevent the game from printing to the console epicRocketLeague.StartInfo.UseShellExecute = false; diff --git a/RLBotCS/ManagerTools/WinReadLog.cs b/RLBotCS/ManagerTools/WinReadLog.cs index a78d4a3c..50cb3477 100644 --- a/RLBotCS/ManagerTools/WinReadLog.cs +++ b/RLBotCS/ManagerTools/WinReadLog.cs @@ -40,42 +40,66 @@ public WinReadLog() ); } + public void DeleteLog() + { + if (File.Exists(LogPath)) + File.Delete(LogPath); + } + public (string, string)? GetGamePathAndAuth() { if (!File.Exists(LogPath)) return null; - string? auth = null; - string? path = null; + try + { + string? auth = null; + string? path = null; - using var reader = new StreamReader( - LogPath, - Encoding.UTF8, - detectEncodingFromByteOrderMarks: true - ); + using var stream = new FileStream( + LogPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite + ); + using var reader = new StreamReader( + stream, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: true + ); - string? line; - while ((line = reader.ReadLine()) != null) - { - if (auth == null && line.StartsWith(AUTH_LINE_PREFIX, StringComparison.Ordinal)) - { - auth = line[AUTH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); - } - else if ( - path == null - && line.StartsWith(PATH_LINE_PREFIX, StringComparison.Ordinal) - ) + string? line; + while ((line = reader.ReadLine()) != null) { - path = line[PATH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); - } + if ( + auth == null + && line.StartsWith(AUTH_LINE_PREFIX, StringComparison.Ordinal) + ) + { + auth = line[AUTH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); + } + else if ( + path == null + && line.StartsWith(PATH_LINE_PREFIX, StringComparison.Ordinal) + ) + { + path = line[PATH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); + } - if (auth != null && path != null) - { - return (Path.Combine(path, BINARY_NAME), auth); + if (auth != null && path != null) + { + return (Path.Combine(path, BINARY_NAME), auth); + } } - } - return null; + return null; + } + catch (IOException) + { + // Rocket League may still be writing to the log file. + // Return null so the caller retries. + return null; + } } } #endif From e202d702f08383d47ac299042e84805426edf299 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Fri, 26 Jun 2026 19:49:01 -0500 Subject: [PATCH 05/10] Convert Windows launch utilities to static classes Move WinProcArgs and WinReadLog into the LaunchManager directory. Rename ProcessCommandLine to ProcArgs. Rename WinReadLog to ReadLog and make it static. Update all references. --- .../ManagerTools/LaunchManager/Epic.Windows.cs | 5 ++--- .../ProcArgs.Windows.cs} | 4 +++- .../ManagerTools/LaunchManager/ProcessUtils.cs | 4 ++-- .../ReadLog.Windows.cs} | 16 +++++++--------- 4 files changed, 14 insertions(+), 15 deletions(-) rename RLBotCS/ManagerTools/{WinProcArgs.cs => LaunchManager/ProcArgs.Windows.cs} (99%) rename RLBotCS/ManagerTools/{WinReadLog.cs => LaunchManager/ReadLog.Windows.cs} (92%) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index 130fac71..11199cac 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -19,8 +19,7 @@ private static void LaunchGameViaEpic(int gamePort) } // Start with a fresh Launch.log so we don't read stale data from a previous run. - WinReadLog logReader = new(); - logReader.DeleteLog(); + ReadLog.DeleteLog(); // To launch RocketLeague for Epic we need some extra login parameters from Epic. // We get these by launching the game normally, reading the args, and then closing it again. @@ -36,7 +35,7 @@ private static void LaunchGameViaEpic(int gamePort) (string, string)? pathAndAuth = null; while (pathAndAuth is null) { - pathAndAuth = logReader.GetGamePathAndAuth(); + pathAndAuth = ReadLog.GetGamePathAndAuth(); Thread.Sleep(500); } diff --git a/RLBotCS/ManagerTools/WinProcArgs.cs b/RLBotCS/ManagerTools/LaunchManager/ProcArgs.Windows.cs similarity index 99% rename from RLBotCS/ManagerTools/WinProcArgs.cs rename to RLBotCS/ManagerTools/LaunchManager/ProcArgs.Windows.cs index fcfd224d..3e25e77e 100644 --- a/RLBotCS/ManagerTools/WinProcArgs.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ProcArgs.Windows.cs @@ -2,9 +2,11 @@ using System.Diagnostics; using System.Runtime.InteropServices; +namespace RLBotCS.ManagerTools; + // Solution taken from: // https://stackoverflow.com/a/46006415/10930209 -public static class ProcessCommandLine +public static class ProcArgs { private static class Win32Native { diff --git a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs index 0265390c..4fd7c789 100644 --- a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs @@ -121,13 +121,13 @@ public static int FindUsableGamePort(int rlbotSocketsPort) private static string GetProcessArgs(Process process) { #if WINDOWS - int err = ProcessCommandLine.Retrieve(process, out string commandLine); + int err = ProcArgs.Retrieve(process, out string commandLine); if (err == 0) return commandLine; Logger.LogError( - $"Failed to retrieve command line arguments for process {process.ProcessName}: {ProcessCommandLine.ErrorToString(err)}" + $"Failed to retrieve command line arguments for process {process.ProcessName}: {ProcArgs.ErrorToString(err)}" ); return ""; #else diff --git a/RLBotCS/ManagerTools/WinReadLog.cs b/RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs similarity index 92% rename from RLBotCS/ManagerTools/WinReadLog.cs rename to RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs index 50cb3477..9fdd0297 100644 --- a/RLBotCS/ManagerTools/WinReadLog.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs @@ -2,7 +2,9 @@ using System.Runtime.InteropServices; using System.Text; -public class WinReadLog +namespace RLBotCS.ManagerTools; + +public static class ReadLog { private const int CSIDL_PERSONAL = 0x0005; private const int SHGFP_TYPE_CURRENT = 0; @@ -26,11 +28,8 @@ static string GetMyDocumentsFolder() return sb.ToString(); } - private string LogPath; - - public WinReadLog() - { - LogPath = Path.Combine( + private static string LogPath { get; } = + Path.Combine( GetMyDocumentsFolder(), "My Games", "Rocket League", @@ -38,15 +37,14 @@ public WinReadLog() "Logs", "Launch.log" ); - } - public void DeleteLog() + public static void DeleteLog() { if (File.Exists(LogPath)) File.Delete(LogPath); } - public (string, string)? GetGamePathAndAuth() + public static (string, string)? GetGamePathAndAuth() { if (!File.Exists(LogPath)) return null; From d5ac0b5ba26f768fecda480de8d8712bb52ff65c Mon Sep 17 00:00:00 2001 From: VirxEC Date: Fri, 26 Jun 2026 20:49:07 -0500 Subject: [PATCH 06/10] Remove redundant platform check in Steam path --- RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs index c2e0de83..ad042717 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -1,6 +1,6 @@ #if WINDOWS +#pragma warning disable CA1416 using System.Diagnostics; -using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Win32; @@ -11,11 +11,6 @@ public static partial class LaunchManager { private static string GetWindowsSteamPath() { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException( - "Getting Windows path on non-Windows platform" - ); - using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); if (key?.GetValue("SteamExe")?.ToString() is { } value) return value; From fd0e45579d057b5fc5038118da057cb759b7783a Mon Sep 17 00:00:00 2001 From: VirxEC Date: Fri, 26 Jun 2026 20:49:07 -0500 Subject: [PATCH 07/10] Permit already-running Rocket League on Epic launch Improve argument parsing to avoid false positives from empty command lines Downgrade argument retrieval failure log from Error to Warning --- .../LaunchManager/Epic.Windows.cs | 33 ++++++++----------- .../LaunchManager/ProcessUtils.cs | 8 +++-- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index 11199cac..d782bfb1 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -1,6 +1,5 @@ #if WINDOWS using System.Diagnostics; -using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; namespace RLBotCS.ManagerTools; @@ -9,27 +8,23 @@ public static partial class LaunchManager { private static void LaunchGameViaEpic(int gamePort) { - if (IsRocketLeagueRunningWithArgs()) - return; - - if (IsRocketLeagueRunning()) + if (!IsRocketLeagueRunning()) { - Logger.LogError("Please close Rocket League so RLBot can start it in RLBot mode."); - return; - } - - // Start with a fresh Launch.log so we don't read stale data from a previous run. - ReadLog.DeleteLog(); + // Start with a fresh Launch.log so we don't read stale data from a previous run. + ReadLog.DeleteLog(); - // To launch RocketLeague for Epic we need some extra login parameters from Epic. - // We get these by launching the game normally, reading the args, and then closing it again. + // To launch RocketLeague for Epic we need some extra login parameters from Epic. + // We get these by launching the game normally, reading the args, and then closing it again. - 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(); - Thread.Sleep(500); + 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(); + Thread.Sleep(500); + } else { + Logger.LogInformation("Relaunching Rocket League via Epic."); + } // Get the game path and login info from launch logs (string, string)? pathAndAuth = null; diff --git a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs index 4fd7c789..2eb48348 100644 --- a/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs +++ b/RLBotCS/ManagerTools/LaunchManager/ProcessUtils.cs @@ -62,7 +62,9 @@ private static void ApplyEnvironment( if (!candidate.ProcessName.Contains("RocketLeague")) continue; - return GetProcessArgs(candidate); + string args = GetProcessArgs(candidate); + if (args.Length > 0) + return args; } return null; @@ -126,7 +128,7 @@ private static string GetProcessArgs(Process process) if (err == 0) return commandLine; - Logger.LogError( + Logger.LogWarning( $"Failed to retrieve command line arguments for process {process.ProcessName}: {ProcArgs.ErrorToString(err)}" ); return ""; @@ -175,7 +177,7 @@ public static bool IsRocketLeagueRunningWithArgs() return false; var args = GetProcessArgs(candidate); - return args.Contains("rlbot"); + return args.Length > 0 && args.Contains("rlbot"); }); } } From ba7c758dcd98434f7ac9586c3e0a388f661951eb Mon Sep 17 00:00:00 2001 From: VirxEC Date: Fri, 26 Jun 2026 20:49:07 -0500 Subject: [PATCH 08/10] Automatically launch Steam if not running before launching RL --- .../LaunchManager/Epic.Windows.cs | 4 ++- .../LaunchManager/Steam.Windows.cs | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index d782bfb1..6d9f3c26 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -22,7 +22,9 @@ private static void LaunchGameViaEpic(int gamePort) "/c start \"\" \"com.epicgames.launcher://apps/9773aa1aa54f4f7b80e44bef04986cea%3A530145df28a24424923f5828cc9031a1%3ASugar?action=launch&silent=true\""; launcher.Start(); Thread.Sleep(500); - } else { + } + else + { Logger.LogInformation("Relaunching Rocket League via Epic."); } diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs index ad042717..d78927e0 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -1,6 +1,7 @@ #if WINDOWS #pragma warning disable CA1416 using System.Diagnostics; +using System.Runtime.InteropServices; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Microsoft.Win32; @@ -9,6 +10,9 @@ namespace RLBotCS.ManagerTools; public static partial class LaunchManager { + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr FindWindow(string? lpClassName, string? lpWindowName); + private static string GetWindowsSteamPath() { using RegistryKey? key = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam"); @@ -98,6 +102,13 @@ private static List GetWindowsSteamLibraryFolders() return null; } + private static bool IsSteamRunning() + { + return Process + .GetProcesses() + .Any(p => p.ProcessName.Equals("steam", StringComparison.OrdinalIgnoreCase)); + } + private static void LaunchGameViaSteam(int gamePort) { string? gamePath = FindWindowsRocketLeaguePath(); @@ -106,6 +117,23 @@ private static void LaunchGameViaSteam(int gamePort) "Could not find Rocket League installation. Ensure Rocket League is installed via Steam." ); + if (!IsSteamRunning()) + { + string steamPath = GetWindowsSteamPath(); + Logger.LogInformation($"Launching Steam at \"{steamPath}\"..."); + + Process steam = new(); + steam.StartInfo.FileName = steamPath; + steam.Start(); + + // Wait for Steam's main window to appear, + // otherwise we will launch the game too soon and won't be logged in + while (FindWindow(null, "Steam") == IntPtr.Zero) + { + Thread.Sleep(500); + } + } + string args = string.Join(" ", GetRLBotArgs(gamePort)); Process rocketLeague = new(); @@ -116,8 +144,6 @@ private static void LaunchGameViaSteam(int gamePort) rocketLeague.StartInfo.WindowStyle = ProcessWindowStyle.Hidden; - // Let Steamworks initialize by providing the AppId, - // so the game can authenticate with the running Steam client. rocketLeague.StartInfo.Environment["SteamAppId"] = SteamGameId; rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; From fe0d66dafcfbe225d7e100d70e942824943b8c48 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 27 Jun 2026 14:04:56 +0200 Subject: [PATCH 09/10] Extra sleep for Steam launch and consistent launch logging --- RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs | 2 +- RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs | 2 +- RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index 6d9f3c26..0da91493 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -60,7 +60,7 @@ private static void LaunchGameViaEpic(int gamePort) epicRocketLeague.StartInfo.RedirectStandardOutput = true; epicRocketLeague.StartInfo.RedirectStandardError = true; - Logger.LogInformation($"Starting Rocket League with Epic: {rlbotArgs}"); + Logger.LogInformation($"Launching Rocket League via Epic: \"{directGamePath} {rlbotArgs}\"..."); Logger.LogDebug( $"Full command: {epicRocketLeague.StartInfo.FileName} {epicRocketLeague.StartInfo.Arguments}" ); diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs index 749f9415..08e31eef 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs @@ -415,7 +415,7 @@ private static void LaunchGameViaSteam(int gamePort) rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; Logger.LogInformation( - $"Starting Rocket League via Proton without EAC: {protonPath} run {gamePath} {args}" + $"Launching Rocket League via Proton: \"{protonPath} run {gamePath} {args}\"..." ); rocketLeague.Start(); } diff --git a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs index d78927e0..dac478a0 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -120,7 +120,7 @@ private static void LaunchGameViaSteam(int gamePort) if (!IsSteamRunning()) { string steamPath = GetWindowsSteamPath(); - Logger.LogInformation($"Launching Steam at \"{steamPath}\"..."); + Logger.LogInformation($"Launching Steam: \"{steamPath}\"..."); Process steam = new(); steam.StartInfo.FileName = steamPath; @@ -132,6 +132,7 @@ private static void LaunchGameViaSteam(int gamePort) { Thread.Sleep(500); } + Thread.Sleep(500); } string args = string.Join(" ", GetRLBotArgs(gamePort)); @@ -147,7 +148,7 @@ private static void LaunchGameViaSteam(int gamePort) rocketLeague.StartInfo.Environment["SteamAppId"] = SteamGameId; rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; - Logger.LogInformation($"Starting Rocket League without EAC: {gamePath} {args}"); + Logger.LogInformation($"Launching Rocket League via Steam: \"{gamePath} {args}\"..."); rocketLeague.Start(); } } From 256d31c76e257515216daed796e3d87cc09cd064 Mon Sep 17 00:00:00 2001 From: NicEastvillage Date: Sat, 27 Jun 2026 14:09:25 +0200 Subject: [PATCH 10/10] Formatting --- RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs index 0da91493..307b7881 100644 --- a/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -60,7 +60,9 @@ private static void LaunchGameViaEpic(int gamePort) epicRocketLeague.StartInfo.RedirectStandardOutput = true; epicRocketLeague.StartInfo.RedirectStandardError = true; - Logger.LogInformation($"Launching Rocket League via Epic: \"{directGamePath} {rlbotArgs}\"..."); + Logger.LogInformation( + $"Launching Rocket League via Epic: \"{directGamePath} {rlbotArgs}\"..." + ); Logger.LogDebug( $"Full command: {epicRocketLeague.StartInfo.FileName} {epicRocketLeague.StartInfo.Arguments}" );