From 514dc8fb8e5cb2dffcfc84fafa14115890a2f7bf Mon Sep 17 00:00:00 2001 From: VirxEC Date: Wed, 9 Sep 2026 14:13:46 -0400 Subject: [PATCH 1/3] Add platform-specific environment variable overrides --- RLBotCS/Main.cs | 2 +- RLBotCS/ManagerTools/ConfigParser.cs | 73 +++++++++++++++++++-- RLBotCSTests/ConfigParserTest.cs | 70 ++++++++++++++++++++ RLBotCSTests/TestTomls/platform.bot.toml | 19 ++++++ RLBotCSTests/TestTomls/platform.script.toml | 19 ++++++ RLBotCSTests/TestTomls/platform.toml | 7 ++ 6 files changed, 184 insertions(+), 6 deletions(-) create mode 100644 RLBotCSTests/TestTomls/platform.bot.toml create mode 100644 RLBotCSTests/TestTomls/platform.script.toml create mode 100644 RLBotCSTests/TestTomls/platform.toml diff --git a/RLBotCS/Main.cs b/RLBotCS/Main.cs index d3148fe..4808238 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-rc17\n" + "RLBotServer v5.0.0-rc.18\n" + $"Bridge {BridgeVersion.Version}\n" + "@ https://www.rlbot.org & https://github.com/RLBot/core" ); diff --git a/RLBotCS/ManagerTools/ConfigParser.cs b/RLBotCS/ManagerTools/ConfigParser.cs index a5e01e1..fa9718c 100644 --- a/RLBotCS/ManagerTools/ConfigParser.cs +++ b/RLBotCS/ManagerTools/ConfigParser.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using RLBot.Flat; using RLBotCS.Model; @@ -77,6 +78,8 @@ public static class Fields public const string AgentRunCommand = "run_command"; public const string AgentRunCommandLinux = "run_command_linux"; public const string AgentEnvironment = "environment"; + public const string AgentEnvironmentWindows = "windows"; + public const string AgentEnvironmentLinux = "linux"; public const string AgentHivemind = "hivemind"; public const string LoadoutBlueTable = "blue_loadout"; @@ -261,6 +264,26 @@ private string GetRunCommand(TomlTable runnableSettings) #endif } + private static readonly Regex UnixEnvironmentVariablePattern = new( + @"\$(?:\{(?[A-Za-z_][A-Za-z0-9_]*)\}|(?[A-Za-z_][A-Za-z0-9_]*))", + RegexOptions.Compiled + ); + + private static string ExpandEnvironmentValue(string value) + { + value = Environment.ExpandEnvironmentVariables(value); + return UnixEnvironmentVariablePattern.Replace( + value, + match => + { + string name = match.Groups["braced"].Success + ? match.Groups["braced"].Value + : match.Groups["bare"].Value; + return Environment.GetEnvironmentVariable(name) ?? match.Value; + } + ); + } + private List GetEnvironment(TomlTable runnableSettings) { TomlTable environment = GetValue( @@ -269,11 +292,21 @@ private List GetEnvironment(TomlTable runnableSettings) [] ); - List variables = []; - using (_context.Begin(Fields.AgentEnvironment)) + Dictionary values = new(StringComparer.Ordinal); + + void ReadValues(TomlTable table, bool skipPlatformTables, bool expandValues) { - foreach (var (key, rawValue) in environment) + foreach (var (key, rawValue) in table) { + if ( + skipPlatformTables + && ( + key == Fields.AgentEnvironmentWindows + || key == Fields.AgentEnvironmentLinux + ) + ) + continue; + if (rawValue is not string value) { throw new InvalidCastException( @@ -281,11 +314,41 @@ private List GetEnvironment(TomlTable runnableSettings) ); } - variables.Add(new() { Name = key, Value = value }); + values[key] = expandValues ? ExpandEnvironmentValue(value) : value; + } + } + + using (_context.Begin(Fields.AgentEnvironment)) + { + ReadValues(environment, true, false); + + string? platform = + OperatingSystem.IsWindows() ? Fields.AgentEnvironmentWindows + : OperatingSystem.IsLinux() ? Fields.AgentEnvironmentLinux + : null; + + if ( + platform is not null + && environment.TryGetValue(platform, out var rawPlatformEnvironment) + ) + { + if (rawPlatformEnvironment is not TomlTable platformEnvironment) + { + throw new InvalidCastException( + $"{_context.ToStringWithEnd(platform)} has value {rawPlatformEnvironment}, but a table was expected." + ); + } + + using (_context.Begin(platform)) + { + ReadValues(platformEnvironment, false, true); + } } } - return variables; + return values + .Select(pair => new EnvironmentVariableT { Name = pair.Key, Value = pair.Value }) + .ToList(); } private ScriptConfigurationT LoadScriptConfig(string scriptConfigPath) diff --git a/RLBotCSTests/ConfigParserTest.cs b/RLBotCSTests/ConfigParserTest.cs index 853ec95..e25d9ba 100644 --- a/RLBotCSTests/ConfigParserTest.cs +++ b/RLBotCSTests/ConfigParserTest.cs @@ -200,6 +200,76 @@ public void Overrides() ); } + [TestMethod] + public void PlatformEnvironmentOverrides() + { + ConfigParser parser = new(); + MatchConfigurationT mc = parser.LoadMatchConfig("TestTomls/platform.toml"); + + string expectedShared = + OperatingSystem.IsWindows() ? "windows-bot-value" + : OperatingSystem.IsLinux() ? "linux-bot-value" + : "common-bot-value"; + string expectedScriptShared = + OperatingSystem.IsWindows() ? "windows-script-value" + : OperatingSystem.IsLinux() ? "linux-script-value" + : "common-script-value"; + + CustomBotT bot = mc.PlayerConfigurations[0].Variety.AsCustomBot(); + Assert.AreEqual( + "$HOME/common-value", + bot.Environment.Single(e => e.Name == "COMMON_ENV").Value + ); + Assert.AreEqual( + expectedShared, + bot.Environment.Single(e => e.Name == "SHARED_ENV").Value + ); + + ScriptConfigurationT script = mc.ScriptConfigurations[0]; + Assert.AreEqual( + "$HOME/common-value", + script.Environment.Single(e => e.Name == "COMMON_ENV").Value + ); + Assert.AreEqual( + expectedScriptShared, + script.Environment.Single(e => e.Name == "SHARED_ENV").Value + ); + + if (OperatingSystem.IsWindows()) + { + Assert.AreEqual( + Environment.ExpandEnvironmentVariables( + "%LOCALAPPDATA%\\RLBot5\\bots\\torch-archive" + ), + bot.Environment.Single(e => e.Name == "PLATFORM_ENV").Value + ); + Assert.AreEqual( + Environment.ExpandEnvironmentVariables( + "%LOCALAPPDATA%\\RLBot5\\bots\\torch-archive" + ), + script.Environment.Single(e => e.Name == "PLATFORM_ENV").Value + ); + } + else if (OperatingSystem.IsLinux()) + { + string expectedPlatform = + $"{Environment.GetEnvironmentVariable("HOME")}/.local/share/RLBot5/bots/torch-archive"; + Assert.AreEqual( + expectedPlatform, + bot.Environment.Single(e => e.Name == "PLATFORM_ENV").Value + ); + Assert.AreEqual( + expectedPlatform, + script.Environment.Single(e => e.Name == "PLATFORM_ENV").Value + ); + } + else + { + Assert.IsFalse(bot.Environment.Any(e => e.Name == "PLATFORM_ENV")); + Assert.IsFalse(script.Environment.Any(e => e.Name == "PLATFORM_ENV")); + } + } + [TestMethod] public void ConfigNotFound() { diff --git a/RLBotCSTests/TestTomls/platform.bot.toml b/RLBotCSTests/TestTomls/platform.bot.toml new file mode 100644 index 0000000..5369542 --- /dev/null +++ b/RLBotCSTests/TestTomls/platform.bot.toml @@ -0,0 +1,19 @@ +[settings] +agent_id = "test/platform-bot" +name = "Platform Test Bot" +run_command = "python bot.py" +run_command_linux = "python bot.py" + +[settings.environment] +COMMON_ENV = "$HOME/common-value" +SHARED_ENV = "common-bot-value" + +[settings.environment.windows] +SHARED_ENV = "windows-bot-value" +PLATFORM_ENV = "%LOCALAPPDATA%\\RLBot5\\bots\\torch-archive" + +[settings.environment.linux] +SHARED_ENV = "linux-bot-value" +PLATFORM_ENV = "$HOME/.local/share/RLBot5/bots/torch-archive" + +[details] diff --git a/RLBotCSTests/TestTomls/platform.script.toml b/RLBotCSTests/TestTomls/platform.script.toml new file mode 100644 index 0000000..af80fd4 --- /dev/null +++ b/RLBotCSTests/TestTomls/platform.script.toml @@ -0,0 +1,19 @@ +[settings] +agent_id = "test/platform-script" +name = "Platform Test Script" +run_command = "python script.py" +run_command_linux = "python script.py" + +[settings.environment] +COMMON_ENV = "$HOME/common-value" +SHARED_ENV = "common-script-value" + +[settings.environment.windows] +SHARED_ENV = "windows-script-value" +PLATFORM_ENV = "%LOCALAPPDATA%\\RLBot5\\bots\\torch-archive" + +[settings.environment.linux] +SHARED_ENV = "linux-script-value" +PLATFORM_ENV = "$HOME/.local/share/RLBot5/bots/torch-archive" + +[details] diff --git a/RLBotCSTests/TestTomls/platform.toml b/RLBotCSTests/TestTomls/platform.toml new file mode 100644 index 0000000..2146c93 --- /dev/null +++ b/RLBotCSTests/TestTomls/platform.toml @@ -0,0 +1,7 @@ +[match] + +[[cars]] +config_file = "platform.bot.toml" + +[[scripts]] +config_file = "platform.script.toml" From 829b5dbfa691b3f3d4cc66edfe772a6819b77a87 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 22 Oct 2026 09:43:17 -0400 Subject: [PATCH 2/3] Expand Windows and Unix environment variables once --- RLBotCS/ManagerTools/ConfigParser.cs | 18 ++--- RLBotCSTests/ConfigParserTest.cs | 113 +++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) diff --git a/RLBotCS/ManagerTools/ConfigParser.cs b/RLBotCS/ManagerTools/ConfigParser.cs index fa9718c..cb505c2 100644 --- a/RLBotCS/ManagerTools/ConfigParser.cs +++ b/RLBotCS/ManagerTools/ConfigParser.cs @@ -9,7 +9,7 @@ namespace RLBotCS.ManagerTools; -public class ConfigParser +public partial class ConfigParser { public static class Fields { @@ -264,21 +264,19 @@ private string GetRunCommand(TomlTable runnableSettings) #endif } - private static readonly Regex UnixEnvironmentVariablePattern = new( - @"\$(?:\{(?[A-Za-z_][A-Za-z0-9_]*)\}|(?[A-Za-z_][A-Za-z0-9_]*))", - RegexOptions.Compiled - ); + [GeneratedRegex(@"%([^%]+)%|\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))")] + private static partial Regex EnvironmentVariablePattern(); private static string ExpandEnvironmentValue(string value) { - value = Environment.ExpandEnvironmentVariables(value); - return UnixEnvironmentVariablePattern.Replace( + return EnvironmentVariablePattern().Replace( value, match => { - string name = match.Groups["braced"].Success - ? match.Groups["braced"].Value - : match.Groups["bare"].Value; + string name = + match.Groups[1].Success ? match.Groups[1].Value + : match.Groups[2].Success ? match.Groups[2].Value + : match.Groups[3].Value; return Environment.GetEnvironmentVariable(name) ?? match.Value; } ); diff --git a/RLBotCSTests/ConfigParserTest.cs b/RLBotCSTests/ConfigParserTest.cs index e25d9ba..5d2a624 100644 --- a/RLBotCSTests/ConfigParserTest.cs +++ b/RLBotCSTests/ConfigParserTest.cs @@ -270,6 +270,119 @@ public void PlatformEnvironmentOverrides() } } + [TestMethod] + [DoNotParallelize] + public void ExpandsSelectedPlatformEnvironmentValuesOnce() + { + string[] names = + [ + "RLBCPERCENT_TEST", + "RLBCEMPTY_TEST", + "RLBCDOLLAR_TEST", + "RLBCPERCENT_REPLACEMENT_TEST", + "RLBCPAREN_TEST(x86)", + "RLBCINNER_TEST", + "RLBCMISSING_TEST", + ]; + string?[] previousValues = names.Select(Environment.GetEnvironmentVariable).ToArray(); + string directory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + + try + { + Environment.SetEnvironmentVariable("RLBCPERCENT_TEST", "percent-value"); + Environment.SetEnvironmentVariable("RLBCEMPTY_TEST", ""); + Assert.AreEqual("", Environment.GetEnvironmentVariable("RLBCEMPTY_TEST")); + Environment.SetEnvironmentVariable("RLBCDOLLAR_TEST", "$RLBCINNER_TEST"); + Environment.SetEnvironmentVariable( + "RLBCPERCENT_REPLACEMENT_TEST", + "%RLBCINNER_TEST%" + ); + Environment.SetEnvironmentVariable("RLBCPAREN_TEST(x86)", "paren-value"); + Environment.SetEnvironmentVariable("RLBCINNER_TEST", "expanded"); + Environment.SetEnvironmentVariable("RLBCMISSING_TEST", null); + Assert.IsNull(Environment.GetEnvironmentVariable("RLBCMISSING_TEST")); + + string platform = OperatingSystem.IsWindows() ? "windows" : "linux"; + string matchPath = Path.Combine(directory, "match.toml"); + File.WriteAllText( + matchPath, + """ + [match] + [[cars]] + config_file = "bot.toml" + [[scripts]] + config_file = "script.toml" + """ + ); + string agentToml = $$""" + [settings] + agent_id = "test" + name = "test" + run_command = "" + run_command_linux = "" + + [settings.environment] + COMMON = "$HOME and %RLBCPERCENT_TEST%" + + [settings.environment.{{platform}}] + VALUE = "%RLBCPERCENT_TEST%$RLBCEMPTY_TEST${RLBCPERCENT_TEST}%RLBCPAREN_TEST(x86)%%RLBCMISSING_TEST%$RLBCMISSING_TEST${RLBCMISSING_TEST}" + DOLLAR_REPLACEMENT = "%RLBCDOLLAR_TEST%" + PERCENT_REPLACEMENT = "$RLBCPERCENT_REPLACEMENT_TEST" + """; + File.WriteAllText(Path.Combine(directory, "bot.toml"), agentToml); + File.WriteAllText(Path.Combine(directory, "script.toml"), agentToml); + + MatchConfigurationT config = new ConfigParser().LoadMatchConfig(matchPath); + CustomBotT bot = config.PlayerConfigurations[0].Variety.AsCustomBot(); + ScriptConfigurationT script = config.ScriptConfigurations[0]; + + AssertEnvironmentValue( + bot.Environment, + "VALUE", + "percent-valuepercent-valueparen-value%RLBCMISSING_TEST%$RLBCMISSING_TEST${RLBCMISSING_TEST}" + ); + AssertEnvironmentValue( + script.Environment, + "VALUE", + "percent-valuepercent-valueparen-value%RLBCMISSING_TEST%$RLBCMISSING_TEST${RLBCMISSING_TEST}" + ); + AssertEnvironmentValue(bot.Environment, "DOLLAR_REPLACEMENT", "$RLBCINNER_TEST"); + AssertEnvironmentValue( + script.Environment, + "DOLLAR_REPLACEMENT", + "$RLBCINNER_TEST" + ); + AssertEnvironmentValue(bot.Environment, "PERCENT_REPLACEMENT", "%RLBCINNER_TEST%"); + AssertEnvironmentValue( + script.Environment, + "PERCENT_REPLACEMENT", + "%RLBCINNER_TEST%" + ); + AssertEnvironmentValue(bot.Environment, "COMMON", "$HOME and %RLBCPERCENT_TEST%"); + AssertEnvironmentValue( + script.Environment, + "COMMON", + "$HOME and %RLBCPERCENT_TEST%" + ); + } + finally + { + for (int i = 0; i < names.Length; i++) + Environment.SetEnvironmentVariable(names[i], previousValues[i]); + Directory.Delete(directory, true); + } + } + + private static void AssertEnvironmentValue( + System.Collections.Generic.IEnumerable environment, + string name, + string expected + ) + { + Assert.AreEqual(expected, environment.Single(value => value.Name == name).Value); + } + [TestMethod] public void ConfigNotFound() { From ee8ec58b81bc3a33aea52dd388af350fb81b5977 Mon Sep 17 00:00:00 2001 From: VirxEC Date: Thu, 22 Oct 2026 09:49:41 -0400 Subject: [PATCH 3/3] Format --- RLBotCS/ManagerTools/ConfigParser.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/RLBotCS/ManagerTools/ConfigParser.cs b/RLBotCS/ManagerTools/ConfigParser.cs index cb505c2..fd38c97 100644 --- a/RLBotCS/ManagerTools/ConfigParser.cs +++ b/RLBotCS/ManagerTools/ConfigParser.cs @@ -269,17 +269,18 @@ private string GetRunCommand(TomlTable runnableSettings) private static string ExpandEnvironmentValue(string value) { - return EnvironmentVariablePattern().Replace( - value, - match => - { - string name = - match.Groups[1].Success ? match.Groups[1].Value - : match.Groups[2].Success ? match.Groups[2].Value - : match.Groups[3].Value; - return Environment.GetEnvironmentVariable(name) ?? match.Value; - } - ); + return EnvironmentVariablePattern() + .Replace( + value, + match => + { + string name = + match.Groups[1].Success ? match.Groups[1].Value + : match.Groups[2].Success ? match.Groups[2].Value + : match.Groups[3].Value; + return Environment.GetEnvironmentVariable(name) ?? match.Value; + } + ); } private List GetEnvironment(TomlTable runnableSettings)