diff --git a/Content.Client/Voting/UI/VoteCallMenu.xaml.cs b/Content.Client/Voting/UI/VoteCallMenu.xaml.cs index 20c72ea0a9..0abc489a6a 100644 --- a/Content.Client/Voting/UI/VoteCallMenu.xaml.cs +++ b/Content.Client/Voting/UI/VoteCallMenu.xaml.cs @@ -1,3 +1,4 @@ +using System; using Content.Client.Stylesheets; using Content.Shared.Voting; using JetBrains.Annotations; @@ -104,8 +105,15 @@ namespace Content.Client.Voting.UI if (!isAvailable) { - var remaining = timeout - _gameTiming.RealTime; - VoteTypeTimeoutLabel.Text = Loc.GetString("ui-vote-type-timeout", ("remaining", remaining.ToString("mm\\:ss"))); + if (timeout == TimeSpan.Zero) + { + VoteTypeTimeoutLabel.Text = Loc.GetString("ui-vote-type-not-available"); + } + else + { + var remaining = timeout - _gameTiming.RealTime; + VoteTypeTimeoutLabel.Text = Loc.GetString("ui-vote-type-timeout", ("remaining", remaining.ToString("mm\\:ss"))); + } } } diff --git a/Content.Client/Voting/VoteManager.cs b/Content.Client/Voting/VoteManager.cs index d53c0e432d..f4b58f2fbe 100644 --- a/Content.Client/Voting/VoteManager.cs +++ b/Content.Client/Voting/VoteManager.cs @@ -186,7 +186,8 @@ namespace Content.Client.Voting _standardVoteTimeouts.Clear(); foreach (var (type, time) in message.VotesUnavailable) { - _standardVoteTimeouts.Add(type, _gameTiming.RealServerToLocal(time)); + var fixedTime = (time == TimeSpan.Zero) ? time : _gameTiming.RealServerToLocal(time); + _standardVoteTimeouts.Add(type, fixedTime); } CanCallStandardVotesChanged?.Invoke(); diff --git a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs index 5deccfc110..b7eef2588d 100644 --- a/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs +++ b/Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs @@ -7,6 +7,7 @@ using Content.Server.RoundEnd; using Content.Shared.CCVar; using Content.Shared.Voting; using Robust.Server.Player; +using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; @@ -16,6 +17,13 @@ namespace Content.Server.Voting.Managers { public sealed partial class VoteManager { + private static readonly Dictionary> _voteTypesToEnableCVars = new() + { + {StandardVoteType.Restart, CCVars.VoteRestartEnabled}, + {StandardVoteType.Preset, CCVars.VotePresetEnabled}, + {StandardVoteType.Map, CCVars.VoteMapEnabled}, + }; + public void CreateStandardVote(IPlayerSession? initiator, StandardVoteType voteType) { switch (voteType) diff --git a/Content.Server/Voting/Managers/VoteManager.cs b/Content.Server/Voting/Managers/VoteManager.cs index 9c3d04a3e0..0e9920561d 100644 --- a/Content.Server/Voting/Managers/VoteManager.cs +++ b/Content.Server/Voting/Managers/VoteManager.cs @@ -10,6 +10,7 @@ using Content.Server.Afk; using Content.Server.Chat.Managers; using Content.Server.Maps; using Content.Shared.Administration; +using Content.Shared.CCVar; using Content.Shared.Voting; using Robust.Server.Player; using Robust.Shared.Configuration; @@ -45,6 +46,7 @@ namespace Content.Server.Voting.Managers private readonly Dictionary _standardVoteTimeout = new(); private readonly Dictionary _voteTimeout = new(); private readonly HashSet _playerCanCallVoteDirty = new(); + private readonly StandardVoteType[] _standardVoteTypeValues = Enum.GetValues(); public void Initialize() { @@ -53,6 +55,17 @@ namespace Content.Server.Voting.Managers _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; _adminMgr.OnPermsChanged += AdminPermsChanged; + + _cfg.OnValueChanged(CCVars.VoteEnabled, value => { + DirtyCanCallVoteAll(); + }); + + foreach (var kvp in _voteTypesToEnableCVars) + { + _cfg.OnValueChanged(kvp.Value, value => { + DirtyCanCallVoteAll(); + }); + } } private void AdminPermsChanged(AdminPermsChangedEventArgs obj) @@ -262,9 +275,21 @@ namespace Content.Server.Voting.Managers msg.CanCall = CanCallVote(player, null, out var isAdmin, out var timeSpan); msg.WhenCanCallVote = timeSpan; - msg.VotesUnavailable = isAdmin - ? Array.Empty<(StandardVoteType, TimeSpan)>() - : _standardVoteTimeout.Select(kv => (kv.Key, kv.Value)).ToArray(); + if (isAdmin) + { + msg.VotesUnavailable = Array.Empty<(StandardVoteType, TimeSpan)>(); + } + else + { + var votesUnavailable = new List<(StandardVoteType, TimeSpan)>(); + foreach (var v in _standardVoteTypeValues) + { + if (CanCallVote(player, v, out var _isAdmin, out var typeTimeSpan)) + continue; + votesUnavailable.Add((v, typeTimeSpan)); + } + msg.VotesUnavailable = votesUnavailable.ToArray(); + } _netManager.ServerSendMessage(msg, player.ConnectedClient); } @@ -285,13 +310,20 @@ namespace Content.Server.Voting.Managers return true; } + // If voting is disabled, block votes. + if (!_cfg.GetCVar(CCVars.VoteEnabled)) + return false; + // Specific standard vote types can be disabled with cvars. + if ((voteType != null) && _voteTypesToEnableCVars.TryGetValue(voteType.Value, out var cvar) && !_cfg.GetCVar(cvar)) + return false; + // Cannot start vote if vote is already active (as non-admin). if (_votes.Count != 0) return false; // Standard vote on timeout, no calling. // Ghosts I understand you're dead but stop spamming the restart vote bloody hell. - if (voteType != null && _standardVoteTimeout.ContainsKey(voteType.Value)) + if (voteType != null && _standardVoteTimeout.TryGetValue(voteType.Value, out timeSpan)) return false; return !_voteTimeout.TryGetValue(initiator.UserId, out timeSpan); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 4d4058c988..e9c8b33de3 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -431,6 +431,30 @@ namespace Content.Shared.CCVar * VOTE */ + /// + /// Allows enabling/disabling player-started votes for ultimate authority + /// + public static readonly CVarDef VoteEnabled = + CVarDef.Create("vote.enabled", true, CVar.SERVERONLY); + + /// + /// See vote.enabled, but specific to restart votes + /// + public static readonly CVarDef VoteRestartEnabled = + CVarDef.Create("vote.restart_enabled", true, CVar.SERVERONLY); + + /// + /// See vote.enabled, but specific to preset votes + /// + public static readonly CVarDef VotePresetEnabled = + CVarDef.Create("vote.preset_enabled", true, CVar.SERVERONLY); + + /// + /// See vote.enabled, but specific to map votes + /// + public static readonly CVarDef VoteMapEnabled = + CVarDef.Create("vote.map_enabled", true, CVar.SERVERONLY); + /// /// The required ratio of the server that must agree for a restart round vote to go through. /// diff --git a/Content.Shared/Voting/MsgVoteCanCall.cs b/Content.Shared/Voting/MsgVoteCanCall.cs index 7c5d0daf30..408bff8487 100644 --- a/Content.Shared/Voting/MsgVoteCanCall.cs +++ b/Content.Shared/Voting/MsgVoteCanCall.cs @@ -18,6 +18,7 @@ namespace Content.Shared.Voting public TimeSpan WhenCanCallVote; // Which standard votes are currently unavailable, and when will they become available. + // The whenAvailable can be null if the reason is something not timeout related. public (StandardVoteType type, TimeSpan whenAvailable)[] VotesUnavailable = default!; // It's possible to be able to call votes but all standard votes to be timed out. diff --git a/Resources/Locale/en-US/voting/ui/vote-call-menu.ftl b/Resources/Locale/en-US/voting/ui/vote-call-menu.ftl index 293f890bfc..04ccd28513 100644 --- a/Resources/Locale/en-US/voting/ui/vote-call-menu.ftl +++ b/Resources/Locale/en-US/voting/ui/vote-call-menu.ftl @@ -11,6 +11,9 @@ ui-vote-create-button = Call Vote # Timeout text if a standard vote type is currently on timeout. ui-vote-type-timeout = This vote was called too recently ({$remaining}) +# Unavailable text if a vote type has been disabled manually. +ui-vote-type-not-available = This vote type has been disabled + # Hue hue hue ui-vote-fluff = Powered by Robustâ„¢ Anti-Tamper Technology