diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml index ee7ba4d34ff..523e55af5f5 100644 --- a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml @@ -1,4 +1,4 @@ - diff --git a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs index c3bcf3ffa09..a57a6b5e8c5 100644 --- a/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/PanicBunkerTab/PanicBunkerTab.xaml.cs @@ -1,4 +1,5 @@ -using Content.Shared.Administration.Events; +using Content.Corvax.Interfaces.Shared; +using Content.Shared.Administration.Events; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; @@ -28,6 +29,16 @@ public PanicBunkerTab() MinOverallMinutes.OnTextEntered += args => SendMinOverallMinutes(args.Text); MinOverallMinutes.OnFocusExit += args => SendMinOverallMinutes(args.Text); _minOverallMinutes = MinOverallMinutes.Text; + + // Corvax-VPNGuard-Start + var haveSecrets = IoCManager.Instance!.TryResolveType(out _); // TODO: Probably need better way to detect Secrets module + if (haveSecrets) + { + VPNContainer.Visible = true; + DenyVPN.OnPressed += _ => SendDenyVpn(DenyVPN.Pressed); + } + // Corvax-VPNGuard-End + } private void SendMinAccountAge(string text) @@ -54,6 +65,13 @@ private void SendMinOverallMinutes(string text) _console.ExecuteCommand($"panicbunker_min_overall_minutes {minutes}"); } + // Corvax-VPNGuard-Start + private void SendDenyVpn(bool deny) + { + _console.ExecuteCommand($"panicbunker_deny_vpn {deny}"); + } + // Corvax-VPNGuard-End + public void UpdateStatus(PanicBunkerStatus status) { EnabledButton.Pressed = status.Enabled; @@ -73,5 +91,6 @@ public void UpdateStatus(PanicBunkerStatus status) MinOverallMinutes.Text = status.MinOverallMinutes.ToString(); _minOverallMinutes = MinOverallMinutes.Text; + DenyVPN.Pressed = status.DenyVpn; // Corvax-VPNGuard } } diff --git a/Content.Server/Administration/Commands/PanicBunkerCommand.cs b/Content.Server/Administration/Commands/PanicBunkerCommand.cs index 18aed7e3f08..ff3f2987f93 100644 --- a/Content.Server/Administration/Commands/PanicBunkerCommand.cs +++ b/Content.Server/Administration/Commands/PanicBunkerCommand.cs @@ -1,4 +1,4 @@ -using Content.Shared.Administration; +using Content.Shared.Administration; using Content.Shared.CCVar; using Robust.Shared.Configuration; using Robust.Shared.Console; @@ -144,7 +144,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length > 1) { - shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1))); + shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1))); return; } @@ -176,7 +176,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) if (args.Length > 1) { - shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1))); + shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1))); return; } diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index f964e8961ce..033a77db003 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Net; using System.Net.Http; using System.Security.Cryptography; @@ -470,13 +470,10 @@ Panic bunker status { var ticker = _entitySystemManager.GetEntitySystem(); var adminSystem = _entitySystemManager.GetEntitySystem(); - var players = new List(); - foreach (var player in _playerManager.Sessions) { var adminData = _adminManager.GetAdminData(player, true); - players.Add(new InfoResponse.Player { UserId = player.UserId.UserId, @@ -485,7 +482,6 @@ Panic bunker status IsDeadminned = !adminData?.Active ?? false }); } - InfoResponse.MapInfo? mapInfo = null; if (_gameMapManager.GetSelectedMap() is { } mapPrototype) { @@ -495,14 +491,12 @@ Panic bunker status Name = mapPrototype.MapName }; } - var gameRules = new List(); foreach (var addedGameRule in ticker.GetActiveGameRules()) { var meta = _entityManager.MetaQuery.GetComponent(addedGameRule); gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown"); } - var panicBunkerCVars = PanicBunkerCVars.ToDictionary(c => c, c => _config.GetCVar(c)); return new InfoResponse { diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index c51fa7f6146..d5ee31703bb 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Bank.Components; using Content.Shared.Bank.Events; using Content.Shared.CCVar; +using Content.Shared.Corvax.CCCVars; using Content.Shared.GameTicking; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; @@ -82,6 +83,7 @@ public override void Initialize() Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true); Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true); Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true); + Subs.CVar(_config, CCCVars.PanicBunkerDenyVPN, OnPanicBunkerDenyVpnChanged, true); // Corvax-VPNGuard /* * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. @@ -99,8 +101,6 @@ public override void Initialize() SubscribeLocalEvent(OnRoleEvent); SubscribeLocalEvent(OnRoleEvent); SubscribeLocalEvent(OnRoundRestartCleanup); - SubscribeLocalEvent(OnBalanceChanged); - } private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev) @@ -329,17 +329,25 @@ private void OnPanicBunkerMinAccountAgeChanged(int minutes) SendPanicBunkerStatusAll(); } - private void OnBabyJailMaxAccountAgeChanged(int minutes) + private void OnPanicBunkerMinOverallMinutesChanged(int minutes) { - BabyJail.MaxAccountAgeMinutes = minutes; - SendBabyJailStatusAll(); + PanicBunker.MinOverallMinutes = minutes; + SendPanicBunkerStatusAll(); } - private void OnPanicBunkerMinOverallMinutesChanged(int minutes) + // Corvax-VPNGuard-Start + private void OnPanicBunkerDenyVpnChanged(bool deny) { - PanicBunker.MinOverallMinutes = minutes; + PanicBunker.DenyVpn = deny; SendPanicBunkerStatusAll(); } + // Corvax-VPNGuard-End + + private void OnBabyJailMaxAccountAgeChanged(int minutes) + { + BabyJail.MaxAccountAgeMinutes = minutes; + SendBabyJailStatusAll(); + } private void OnBabyJailMaxOverallMinutesChanged(int minutes) { diff --git a/Content.Server/Connection/ConnectionManager.cs b/Content.Server/Connection/ConnectionManager.cs index 3d2e05cbb75..8b9c56e1e0b 100644 --- a/Content.Server/Connection/ConnectionManager.cs +++ b/Content.Server/Connection/ConnectionManager.cs @@ -1,21 +1,30 @@ using System.Collections.Immutable; +using System.Linq; using System.Runtime.InteropServices; using System.Text.Json.Nodes; using System.Threading.Tasks; +using Content.Corvax.Interfaces.Server; +using Content.Corvax.Interfaces.Shared; +using Content.Server.Chat.Managers; using Content.Server._NF.Auth; using Content.Server.Administration; using Content.Server.Database; -using Content.Corvax.Interfaces.Server; using Content.Server.GameTicking; using Content.Server.Preferences.Managers; -using Content.Shared.CCVar; using Content.Shared.GameTicking; +using Content.Shared.CCVar; +using Content.Shared.Corvax.CCCVars; using Content.Shared.Players.PlayTimeTracking; using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.Enums; using Robust.Shared.Network; +using Robust.Shared.Player; using Robust.Shared.Timing; +/* + * TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future. + */ namespace Content.Server.Connection { @@ -52,11 +61,16 @@ public sealed class ConnectionManager : IConnectionManager [Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; private IServerSponsorsManager? _sponsorsMgr; // + //frontier [Dependency] private readonly MiniAuthManager _authManager = default!; + private ISharedSponsorsManager? _sponsorsMgr; // Corvax-Sponsors + private IServerVPNGuardManager? _vpnGuardMgr; // Corvax-VPNGuard + private readonly Dictionary _temporaryBypasses = []; private ISawmill _sawmill = default!; @@ -67,6 +81,7 @@ public void Initialize() IoCManager.Instance!.TryResolveType(out _sponsorsMgr); // Corvax-Sponsors _netMgr.Connecting += NetMgrOnConnecting; _netMgr.AssignUserIdCallback = AssignUserIdCallback; + _plyMgr.PlayerStatusChanged += PlayerStatusChanged; // Approval-based IP bans disabled because they don't play well with Happy Eyeballs. // _netMgr.HandleApprovalCallback = HandleApproval; } @@ -135,6 +150,46 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) } } + private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs args) + { + if (args.NewStatus == SessionStatus.Connected) + { + AdminAlertIfSharedConnection(args.Session); + } + } + + private void AdminAlertIfSharedConnection(ICommonSession newSession) + { + var playerThreshold = _cfg.GetCVar(CCVars.AdminAlertMinPlayersSharingConnection); + if (playerThreshold < 0) + return; + + var addr = newSession.Channel.RemoteEndPoint.Address; + + var otherConnectionsFromAddress = _plyMgr.Sessions.Where(session => + session.Status is SessionStatus.Connected or SessionStatus.InGame + && session.Channel.RemoteEndPoint.Address.Equals(addr) + && session.UserId != newSession.UserId) + .ToList(); + + var otherConnectionCount = otherConnectionsFromAddress.Count; + if (otherConnectionCount + 1 < playerThreshold) // Add one for the total, not just others, using the address + return; + + var username = newSession.Name; + var otherUsernames = string.Join(", ", + otherConnectionsFromAddress.Select(session => session.Name)); + + _chatManager.SendAdminAlert(Loc.GetString("admin-alert-shared-connection", + ("player", username), + ("otherCount", otherConnectionCount), + ("otherList", otherUsernames))); + } + + /* + * TODO: Jesus H Christ what is this utter mess of a function + * TODO: Break this apart into is constituent steps. + */ private async Task<(ConnectionDenyReason, string, List? bansHit)?> ShouldDeny( NetConnectingArgs e) { @@ -197,8 +252,8 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) } var minOverallMinutes = _cfg.GetCVar(CCVars.PanicBunkerMinOverallMinutes); - var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall); - var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalHours > minOverallMinutes; + var overallTime = (await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall); + var haveMinOverallTime = overallTime != null && overallTime.TimeSpent.TotalMinutes > minOverallMinutes; // Use the custom reason if it exists & they don't have the minimum time if (customReason != string.Empty && !haveMinOverallTime && !bypassAllowed) @@ -210,10 +265,27 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) { return (ConnectionDenyReason.Panic, Loc.GetString("panic-bunker-account-denied-reason", - ("reason", Loc.GetString("panic-bunker-account-reason-overall", ("hours", minOverallMinutes)))), null); + ("reason", Loc.GetString("panic-bunker-account-reason-overall", ("minutes", minOverallMinutes)))), null); } - if (!validAccountAge || !haveMinOverallTime && !bypassAllowed) + // Corvax-VPNGuard-Start + if (_vpnGuardMgr == null) // "lazyload" because of problems with dependency resolve order + IoCManager.Instance!.TryResolveType(out _vpnGuardMgr); + + var denyVpn = false; + if (_cfg.GetCVar(CCCVars.PanicBunkerDenyVPN) && _vpnGuardMgr != null) + { + denyVpn = await _vpnGuardMgr.IsConnectionVpn(e.IP.Address); + if (denyVpn) + { + return (ConnectionDenyReason.Panic, + Loc.GetString("panic-bunker-account-denied-reason", + ("reason", Loc.GetString("panic-bunker-account-reason-vpn"))), null); + } + } + // Corvax-VPNGuard-End + + if ((!validAccountAge || !haveMinOverallTime || denyVpn) && !bypassAllowed) // Corvax-VPNGuard { return (ConnectionDenyReason.Panic, Loc.GetString("panic-bunker-account-denied"), null); } @@ -231,8 +303,8 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && adminData != null; // Corvax-Queue-Start var isQueueEnabled = IoCManager.Instance!.TryResolveType(out var mgr) && mgr.IsEnabled; - if (_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !isPrivileged && !isQueueEnabled) - // Corvax-Queue-End + if ((_plyMgr.PlayerCount >= _cfg.GetCVar(CCVars.SoftMaxPlayers) && !adminBypass) && !wasInGame && !isQueueEnabled) + // Corvax-Queue-End { return (ConnectionDenyReason.Full, Loc.GetString("soft-player-cap-full"), null); } @@ -253,6 +325,7 @@ private async Task NetMgrOnConnecting(NetConnectingArgs e) return (ConnectionDenyReason.Whitelist, msg, null); } } + // End of modified code //Frontier @@ -349,8 +422,8 @@ private bool HasTemporaryBypass(NetUserId user) // Corvax-Queue-Start: Make these conditions in one place, for checks in the connection and in the queue public async Task HavePrivilegedJoin(NetUserId userId) { - var adminBypass = await _dbManager.GetAdminDataForAsync(userId) != null; - var havePriorityJoin = _sponsorsMgr != null && _sponsorsMgr.HavePriorityJoin(userId); // Corvax-Sponsors + var adminBypass = _cfg.GetCVar(CCVars.AdminBypassMaxPlayers) && await _dbManager.GetAdminDataForAsync(userId) != null; + var havePriorityJoin = _sponsorsMgr != null && _sponsorsMgr.HaveServerPriorityJoin(userId); // Corvax-Sponsors var wasInGame = EntitySystem.TryGet(out var ticker) && ticker.PlayerGameStatuses.TryGetValue(userId, out var status) && status == PlayerGameStatus.JoinedGame; diff --git a/Content.Server/Corvax/Administration/Commands/PanicBunkerCommand.cs b/Content.Server/Corvax/Administration/Commands/PanicBunkerCommand.cs new file mode 100644 index 00000000000..1db98128a19 --- /dev/null +++ b/Content.Server/Corvax/Administration/Commands/PanicBunkerCommand.cs @@ -0,0 +1,33 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Corvax.CCCVars; +using Robust.Shared.Configuration; +using Robust.Shared.Console; + +namespace Content.Server.Corvax.Administration.Commands; + +[AdminCommand(AdminFlags.Server)] +public sealed class PanicBunkerDenyVpnCommand : LocalizedCommands +{ + [Dependency] private readonly IConfigurationManager _cfg = default!; + + public override string Command => "panicbunker_deny_vpn"; + + public override void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (args.Length != 1) + { + shell.WriteError(Loc.GetString("shell-need-exactly-one-argument")); + return; + } + + if (!bool.TryParse(args[0], out var deny)) + { + shell.WriteError(Loc.GetString("shell-argument-must-be-boolean")); + return; + } + + _cfg.SetCVar(CCCVars.PanicBunkerDenyVPN, deny); + shell.WriteLine(Loc.GetString(deny ? "panicbunker-command-deny-vpn-enabled" : "panicbunker-command-deny-vpn-disabled")); + } +} diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs index cb038f13a75..9d226472939 100644 --- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs +++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs @@ -179,7 +179,6 @@ public void FlushAllTrackers() FlushSingleTracker(data, time); } } - /// /// Flush time tracker information for a player, /// so APIs like return up-to-date info. diff --git a/Content.Shared/Administration/Events/PanicBunkerChangedEvent.cs b/Content.Shared/Administration/Events/PanicBunkerChangedEvent.cs index 786f645a7f0..b360d808a3d 100644 --- a/Content.Shared/Administration/Events/PanicBunkerChangedEvent.cs +++ b/Content.Shared/Administration/Events/PanicBunkerChangedEvent.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Serialization; +using Robust.Shared.Serialization; namespace Content.Shared.Administration.Events; @@ -12,6 +12,7 @@ public sealed class PanicBunkerStatus public bool ShowReason; public int MinAccountAgeMinutes; public int MinOverallMinutes; + public bool DenyVpn; // Corvax-VPNGuard } [Serializable, NetSerializable] diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 9cacd1aeec5..8c81185faea 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -872,6 +872,15 @@ public static readonly CVarDef public static readonly CVarDef ServerBanErasePlayer = CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED); + /// + /// Minimum players sharing a connection required to create an alert. -1 to disable the alert. + /// + /// + /// If you set this to 0 or 1 then it will alert on every connection, so probably don't do that. + /// + public static readonly CVarDef AdminAlertMinPlayersSharingConnection = + CVarDef.Create("admin.alert.min_players_sharing_connection", -1, CVar.SERVERONLY); + /// /// Minimum explosion intensity to create an admin alert message. -1 to disable the alert. /// diff --git a/Content.Shared/Corvax/CCCVars/CCCVars.cs b/Content.Shared/Corvax/CCCVars/CCCVars.cs new file mode 100644 index 00000000000..898e59d35a9 --- /dev/null +++ b/Content.Shared/Corvax/CCCVars/CCCVars.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.Corvax.CCCVars; + +/// +/// Corvax modules console variables +/// +[CVarDefs] +// ReSharper disable once InconsistentNaming +public sealed class CCCVars +{ + /// + /// Deny any VPN connections. + /// + public static readonly CVarDef PanicBunkerDenyVPN = + CVarDef.Create("game.panic_bunker.deny_vpn", false, CVar.SERVERONLY); +} diff --git a/Resources/ConfigPresets/Corvax/mrp.toml b/Resources/ConfigPresets/Corvax/mrp.toml index e4b7507656e..fc04ffc3db4 100644 --- a/Resources/ConfigPresets/Corvax/mrp.toml +++ b/Resources/ConfigPresets/Corvax/mrp.toml @@ -19,3 +19,12 @@ timerrestart = 30 map_enabled = false preset_enabled = false restart_enabled = false + +[game.panic_bunker] +enabled = true +show_reason = true +min_account_age = 0 +min_overall_minutes = 0 +deny_vpn = true +enable_without_admins = true +disable_with_admins = true diff --git a/Resources/Locale/ru-RU/administration/ui/tabs/panicbunker-tab.ftl b/Resources/Locale/ru-RU/administration/ui/tabs/panicbunker-tab.ftl index 3a90e14e4a7..56f7255995f 100644 --- a/Resources/Locale/ru-RU/administration/ui/tabs/panicbunker-tab.ftl +++ b/Resources/Locale/ru-RU/administration/ui/tabs/panicbunker-tab.ftl @@ -11,7 +11,7 @@ admin-ui-panic-bunker-count-deadminned-admins-tooltip = Count deadminned admins admin-ui-panic-bunker-show-reason = Show Reason admin-ui-panic-bunker-show-reason-tooltip = Show the user why they were blocked from connecting by the panic bunker. admin-ui-panic-bunker-min-account-age = Min. Account Age -admin-ui-panic-bunker-min-overall-hours = Min. Overall Playtime +admin-ui-panic-bunker-min-overall-minutes = Min. Overall Playtime admin-ui-panic-bunker-is-enabled = The panic bunker is currently enabled. admin-ui-panic-bunker-enabled-admin-alert = The panic bunker has been enabled. -admin-ui-panic-bunker-disabled-admin-alert = The panic bunker has been disabled. +admin-ui-panic-bunker-disabled-admin-alert = The panic bunker has been disabled. \ No newline at end of file diff --git a/Resources/Locale/ru-RU/corvax/administration/commands/panicbunker.ftl b/Resources/Locale/ru-RU/corvax/administration/commands/panicbunker.ftl index bd25c9d5512..27f9c8da369 100644 --- a/Resources/Locale/ru-RU/corvax/administration/commands/panicbunker.ftl +++ b/Resources/Locale/ru-RU/corvax/administration/commands/panicbunker.ftl @@ -1,4 +1,2 @@ -cmd-panicbunker_deny_vpn-desc = Включает или отключает запрет доступа через VPN-соединения. -cmd-panicbunker_deny_vpn-help = Использование: panicbunker_min_overall_hours panicbunker-command-deny-vpn-enabled = Бункер теперь будет блокировать подключения через VPN. panicbunker-command-deny-vpn-disabled = Бункер больше не будет блокировать подключения через VPN. diff --git a/Resources/Locale/ru-RU/generic.ftl b/Resources/Locale/ru-RU/generic.ftl index b74a568d6a7..3e9a08c08ce 100644 --- a/Resources/Locale/ru-RU/generic.ftl +++ b/Resources/Locale/ru-RU/generic.ftl @@ -8,5 +8,6 @@ generic-unknown-title = Неизвестно generic-error = ошибка generic-invalid = недействительно generic-hours = часов +generic-minutes = минут generic-playtime-title = Игровое время generic-confirm = Подтвердить diff --git a/RobustToolbox b/RobustToolbox index bf8054b1813..ec794ce4e46 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit bf8054b181392ec9a7eb9f4fea94f66837ed4a71 +Subproject commit ec794ce4e4693069d3b3ebf7a88ead5ff2f860e0