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.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..8090412c --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/BotLauncher.cs @@ -0,0 +1,99 @@ +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 {details.Name} must be started manually."); + 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 {details.Name} exited with error code {botProcess.ExitCode}. See previous logs for more information." + ); + } + }; + + 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 {script.Name} must be started manually."); + 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 {script.Name} exited with error code {scriptProcess.ExitCode}. See previous logs for more information." + ); + } + }; + + 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..307b7881 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Epic.Windows.cs @@ -0,0 +1,78 @@ +#if WINDOWS +using System.Diagnostics; +using Microsoft.Extensions.Logging; + +namespace RLBotCS.ManagerTools; + +public static partial class LaunchManager +{ + private static void LaunchGameViaEpic(int gamePort) + { + if (!IsRocketLeagueRunning()) + { + // 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. + + 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; + while (pathAndAuth is null) + { + pathAndAuth = ReadLog.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 + Logger.LogDebug("Waiting for Rocket League to fully close..."); + while (IsRocketLeagueRunning()) + Thread.Sleep(500); + + string rlbotArgs = string.Join(" ", GetRLBotArgs(gamePort)); + string modifiedArgs = $"{rlbotArgs} {authArgs}"; + + // Relaunch the game with the new args + Process epicRocketLeague = new(); + epicRocketLeague.StartInfo.FileName = directGamePath; + epicRocketLeague.StartInfo.Arguments = modifiedArgs; + + // Prevent the game from printing to the console + epicRocketLeague.StartInfo.UseShellExecute = false; + epicRocketLeague.StartInfo.RedirectStandardOutput = true; + epicRocketLeague.StartInfo.RedirectStandardError = true; + + Logger.LogInformation( + $"Launching Rocket League via Epic: \"{directGamePath} {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/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 new file mode 100644 index 00000000..2eb48348 --- /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; +using Microsoft.Extensions.Logging; +#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; + + string args = GetProcessArgs(candidate); + if (args.Length > 0) + return args; + } + + 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 = ProcArgs.Retrieve(process, out string commandLine); + + if (err == 0) + return commandLine; + + Logger.LogWarning( + $"Failed to retrieve command line arguments for process {process.ProcessName}: {ProcArgs.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() + { + return Process + .GetProcesses() + .Any(candidate => candidate.ProcessName.Contains("RocketLeague")); + } + + public static bool IsRocketLeagueRunningWithArgs() + { + return Process + .GetProcesses() + .Any(candidate => + { + if (!candidate.ProcessName.Contains("RocketLeague")) + return false; + + var args = GetProcessArgs(candidate); + return args.Length > 0 && args.Contains("rlbot"); + }); + } +} diff --git a/RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs b/RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs new file mode 100644 index 00000000..9fdd0297 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/ReadLog.Windows.cs @@ -0,0 +1,103 @@ +#if WINDOWS +using System.Runtime.InteropServices; +using System.Text; + +namespace RLBotCS.ManagerTools; + +public static class ReadLog +{ + private const int CSIDL_PERSONAL = 0x0005; + private const int SHGFP_TYPE_CURRENT = 0; + private const string AUTH_LINE_PREFIX = "Init: Command line: "; + private const string PATH_LINE_PREFIX = "Init: Base directory: "; + private const string BINARY_NAME = "RocketLeague.exe"; + + [DllImport("shell32.dll", CharSet = CharSet.Unicode)] + private static extern int SHGetFolderPathW( + IntPtr hwnd, + int csidl, + IntPtr hToken, + int dwFlags, + StringBuilder pszPath + ); + + static string GetMyDocumentsFolder() + { + var sb = new StringBuilder(260); + SHGetFolderPathW(IntPtr.Zero, CSIDL_PERSONAL, IntPtr.Zero, SHGFP_TYPE_CURRENT, sb); + return sb.ToString(); + } + + private static string LogPath { get; } = + Path.Combine( + GetMyDocumentsFolder(), + "My Games", + "Rocket League", + "TAGame", + "Logs", + "Launch.log" + ); + + public static void DeleteLog() + { + if (File.Exists(LogPath)) + File.Delete(LogPath); + } + + public static (string, string)? GetGamePathAndAuth() + { + if (!File.Exists(LogPath)) + return null; + + try + { + string? auth = null; + string? path = null; + + 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) + ) + { + path = line[PATH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); + } + + if (auth != null && path != null) + { + return (Path.Combine(path, BINARY_NAME), auth); + } + } + + return null; + } + catch (IOException) + { + // Rocket League may still be writing to the log file. + // Return null so the caller retries. + return null; + } + } +} +#endif 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..08e31eef --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Linux.cs @@ -0,0 +1,423 @@ +#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 (Exception e) + { + Logger.LogWarning($"Failed to read tool manifest {protonDir}: {e.Message}"); + } + + 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( + $"Launching Rocket League via Proton: \"{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..dac478a0 --- /dev/null +++ b/RLBotCS/ManagerTools/LaunchManager/Steam.Windows.cs @@ -0,0 +1,155 @@ +#if WINDOWS +#pragma warning disable CA1416 +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; + +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"); + if (key?.GetValue("SteamExe")?.ToString() is { } value) + return value; + + throw new FileNotFoundException( + "Could not find registry entry for SteamExe. Is Steam installed?" + ); + } + + private static bool IsValidSteamRoot(string path) + { + return Directory.Exists(Path.Combine(path, "steamapps")); + } + + private static List GetWindowsSteamLibraryFolders() + { + List folders = []; + + string steamPath = GetWindowsSteamPath(); + string? steamRoot = Path.GetDirectoryName(steamPath); + if (steamRoot != null && IsValidSteamRoot(steamRoot) && !folders.Contains(steamRoot)) + folders.Add(steamRoot); + + 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 bool IsSteamRunning() + { + return Process + .GetProcesses() + .Any(p => p.ProcessName.Equals("steam", StringComparison.OrdinalIgnoreCase)); + } + + 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." + ); + + if (!IsSteamRunning()) + { + string steamPath = GetWindowsSteamPath(); + Logger.LogInformation($"Launching Steam: \"{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); + } + Thread.Sleep(500); + } + + 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; + + rocketLeague.StartInfo.Environment["SteamAppId"] = SteamGameId; + rocketLeague.StartInfo.Environment["SteamGameId"] = SteamGameId; + + Logger.LogInformation($"Launching Rocket League via Steam: \"{gamePath} {args}\"..."); + rocketLeague.Start(); + } +} +#endif diff --git a/RLBotCS/ManagerTools/WinReadLog.cs b/RLBotCS/ManagerTools/WinReadLog.cs deleted file mode 100644 index a78d4a3c..00000000 --- a/RLBotCS/ManagerTools/WinReadLog.cs +++ /dev/null @@ -1,81 +0,0 @@ -#if WINDOWS -using System.Runtime.InteropServices; -using System.Text; - -public class WinReadLog -{ - private const int CSIDL_PERSONAL = 0x0005; - private const int SHGFP_TYPE_CURRENT = 0; - private const string AUTH_LINE_PREFIX = "Init: Command line: "; - private const string PATH_LINE_PREFIX = "Init: Base directory: "; - private const string BINARY_NAME = "RocketLeague.exe"; - - [DllImport("shell32.dll", CharSet = CharSet.Unicode)] - private static extern int SHGetFolderPathW( - IntPtr hwnd, - int csidl, - IntPtr hToken, - int dwFlags, - StringBuilder pszPath - ); - - static string GetMyDocumentsFolder() - { - var sb = new StringBuilder(260); - SHGetFolderPathW(IntPtr.Zero, CSIDL_PERSONAL, IntPtr.Zero, SHGFP_TYPE_CURRENT, sb); - return sb.ToString(); - } - - private string LogPath; - - public WinReadLog() - { - LogPath = Path.Combine( - GetMyDocumentsFolder(), - "My Games", - "Rocket League", - "TAGame", - "Logs", - "Launch.log" - ); - } - - public (string, string)? GetGamePathAndAuth() - { - if (!File.Exists(LogPath)) - return null; - - string? auth = null; - string? path = null; - - using var reader = new StreamReader( - LogPath, - 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) - ) - { - path = line[PATH_LINE_PREFIX.Length..].TrimEnd('\r', '\n'); - } - - if (auth != null && path != null) - { - return (Path.Combine(path, BINARY_NAME), auth); - } - } - - return null; - } -} -#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"