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