using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Administration; using Content.Server.Administration.Managers; 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; using Robust.Shared.Enums; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Voting.Managers { public sealed partial class VoteManager : IVoteManager { [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IAdminManager _adminMgr = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAfkManager _afkManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameMapManager _gameMapManager = default!; private int _nextVoteId = 1; private readonly Dictionary _votes = new(); private readonly Dictionary _voteHandles = new(); private readonly Dictionary _standardVoteTimeout = new(); private readonly Dictionary _voteTimeout = new(); private readonly HashSet _playerCanCallVoteDirty = new(); private readonly StandardVoteType[] _standardVoteTypeValues = Enum.GetValues(); public void Initialize() { _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _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) { DirtyCanCallVote(obj.Player); } private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) { if (e.NewStatus == SessionStatus.InGame) { // Send current votes to newly connected players. foreach (var voteReg in _votes.Values) { SendSingleUpdate(voteReg, e.Session); } DirtyCanCallVote(e.Session); } else if (e.NewStatus == SessionStatus.Disconnected) { // Clear votes from disconnected players. foreach (var voteReg in _votes.Values) { CastVote(voteReg, e.Session, null); } } } private void CastVote(VoteReg v, IPlayerSession player, int? option) { if (!IsValidOption(v, option)) throw new ArgumentOutOfRangeException(nameof(option), "Invalid vote option ID"); if (v.CastVotes.TryGetValue(player, out var existingOption)) { v.Entries[existingOption].Votes -= 1; } if (option != null) { v.Entries[option.Value].Votes += 1; v.CastVotes[player] = option.Value; } else { v.CastVotes.Remove(player); } v.VotesDirty.Add(player); v.Dirty = true; } private bool IsValidOption(VoteReg voteReg, int? option) { return option == null || option >= 0 && option < voteReg.Entries.Length; } public void Update() { // Handle active votes. var remQueue = new RemQueue(); foreach (var v in _votes.Values) { // Logger.Debug($"{_timing.ServerTime}"); if (_timing.RealTime >= v.EndTime) EndVote(v); if (v.Finished) remQueue.Add(v.Id); if (v.Dirty) SendUpdates(v); } foreach (var id in remQueue) { _votes.Remove(id); _voteHandles.Remove(id); } // Handle player timeouts. var timeoutRemQueue = new RemQueue(); foreach (var (userId, timeout) in _voteTimeout) { if (timeout < _timing.RealTime) timeoutRemQueue.Add(userId); } foreach (var userId in timeoutRemQueue) { _voteTimeout.Remove(userId); if (_playerManager.TryGetSessionById(userId, out var session)) DirtyCanCallVote(session); } // Handle standard vote timeouts. var stdTimeoutRemQueue = new RemQueue(); foreach (var (type, timeout) in _standardVoteTimeout) { if (timeout < _timing.RealTime) stdTimeoutRemQueue.Add(type); } foreach (var type in stdTimeoutRemQueue) { _standardVoteTimeout.Remove(type); DirtyCanCallVoteAll(); } // Handle dirty canCallVotes. foreach (var dirtyPlayer in _playerCanCallVoteDirty) { if (dirtyPlayer.Status != SessionStatus.Disconnected) SendUpdateCanCallVote(dirtyPlayer); } _playerCanCallVoteDirty.Clear(); } public IVoteHandle CreateVote(VoteOptions options) { var id = _nextVoteId++; var entries = options.Options.Select(o => new VoteEntry(o.data, o.text)).ToArray(); var start = _timing.RealTime; var end = start + options.Duration; var reg = new VoteReg(id, entries, options.Title, options.InitiatorText, options.InitiatorPlayer, start, end); var handle = new VoteHandle(this, reg); _votes.Add(id, reg); _voteHandles.Add(id, handle); if (options.InitiatorPlayer != null) { var timeout = options.InitiatorTimeout ?? options.Duration * 2; _voteTimeout[options.InitiatorPlayer.UserId] = _timing.RealTime + timeout; } DirtyCanCallVoteAll(); return handle; } private void SendUpdates(VoteReg v) { foreach (var player in _playerManager.ServerSessions) { SendSingleUpdate(v, player); } v.VotesDirty.Clear(); v.Dirty = false; } private void SendSingleUpdate(VoteReg v, IPlayerSession player) { var msg = new MsgVoteData(); msg.VoteId = v.Id; msg.VoteActive = !v.Finished; if (!v.Finished) { msg.VoteTitle = v.Title; msg.VoteInitiator = v.InitiatorText; msg.StartTime = v.StartTime; msg.EndTime = v.EndTime; } if (v.CastVotes.TryGetValue(player, out var cast)) { // Only send info for your vote IF IT CHANGED. // Otherwise there would be a reconciliation b*g causing the UI to jump back and forth. // (votes are not in simulation so can't use normal prediction/reconciliation sadly). var dirty = v.VotesDirty.Contains(player); msg.IsYourVoteDirty = dirty; if (dirty) { msg.YourVote = (byte) cast; } } msg.Options = new (ushort votes, string name)[v.Entries.Length]; for (var i = 0; i < msg.Options.Length; i++) { ref var entry = ref v.Entries[i]; msg.Options[i] = ((ushort) entry.Votes, entry.Text); } player.ConnectedClient.SendMessage(msg); } private void DirtyCanCallVoteAll() { _playerCanCallVoteDirty.UnionWith(_playerManager.ServerSessions); } private void SendUpdateCanCallVote(IPlayerSession player) { var msg = new MsgVoteCanCall(); msg.CanCall = CanCallVote(player, null, out var isAdmin, out var timeSpan); msg.WhenCanCallVote = timeSpan; 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); } private bool CanCallVote( IPlayerSession initiator, StandardVoteType? voteType, out bool isAdmin, out TimeSpan timeSpan) { isAdmin = false; timeSpan = default; // Admins can always call votes. if (_adminMgr.HasAdminFlag(initiator, AdminFlags.Admin)) { isAdmin = true; 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.TryGetValue(voteType.Value, out timeSpan)) return false; return !_voteTimeout.TryGetValue(initiator.UserId, out timeSpan); } public bool CanCallVote(IPlayerSession initiator, StandardVoteType? voteType = null) { return CanCallVote(initiator, voteType, out _, out _); } private void EndVote(VoteReg v) { if (v.Finished) { return; } // Find winner or stalemate. var winners = v.Entries .GroupBy(e => e.Votes) .OrderByDescending(g => g.Key) .First() .Select(e => e.Data) .ToImmutableArray(); v.Finished = true; v.Dirty = true; var args = new VoteFinishedEventArgs(winners.Length == 1 ? winners[0] : null, winners); v.OnFinished?.Invoke(_voteHandles[v.Id], args); DirtyCanCallVoteAll(); } private void CancelVote(VoteReg v) { if (v.Cancelled) return; v.Cancelled = true; v.Finished = true; v.Dirty = true; v.OnCancelled?.Invoke(_voteHandles[v.Id]); DirtyCanCallVoteAll(); } public IEnumerable ActiveVotes => _voteHandles.Values; public bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote) { if (_voteHandles.TryGetValue(voteId, out var vHandle)) { vote = vHandle; return true; } vote = default; return false; } private void DirtyCanCallVote(IPlayerSession player) { _playerCanCallVoteDirty.Add(player); } #region Preset Votes private void WirePresetVoteInitiator(VoteOptions options, IPlayerSession? player) { if (player != null) { options.SetInitiator(player); } else { options.InitiatorText = Loc.GetString("ui-vote-initiator-server"); } } #endregion #region Vote Data private sealed class VoteReg { public readonly int Id; public readonly Dictionary CastVotes = new(); public readonly VoteEntry[] Entries; public readonly string Title; public readonly string InitiatorText; public readonly TimeSpan StartTime; public readonly TimeSpan EndTime; public readonly HashSet VotesDirty = new(); public bool Cancelled; public bool Finished; public bool Dirty = true; public VoteFinishedEventHandler? OnFinished; public VoteCancelledEventHandler? OnCancelled; public IPlayerSession? Initiator { get; } public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText, IPlayerSession? initiator, TimeSpan start, TimeSpan end) { Id = id; Entries = entries; Title = title; InitiatorText = initiatorText; Initiator = initiator; StartTime = start; EndTime = end; } } private struct VoteEntry { public object Data; public string Text; public int Votes; public VoteEntry(object data, string text) { Data = data; Text = text; Votes = 0; } } #endregion #region IVoteHandle API surface private sealed class VoteHandle : IVoteHandle { private readonly VoteManager _mgr; private readonly VoteReg _reg; public int Id => _reg.Id; public string Title => _reg.Title; public string InitiatorText => _reg.InitiatorText; public bool Finished => _reg.Finished; public bool Cancelled => _reg.Cancelled; public IReadOnlyDictionary VotesPerOption { get; } public event VoteFinishedEventHandler? OnFinished { add => _reg.OnFinished += value; remove => _reg.OnFinished -= value; } public event VoteCancelledEventHandler? OnCancelled { add => _reg.OnCancelled += value; remove => _reg.OnCancelled -= value; } public VoteHandle(VoteManager mgr, VoteReg reg) { _mgr = mgr; _reg = reg; VotesPerOption = new VoteDict(reg); } public bool IsValidOption(int optionId) { return _mgr.IsValidOption(_reg, optionId); } public void CastVote(IPlayerSession session, int? optionId) { _mgr.CastVote(_reg, session, optionId); } public void Cancel() { _mgr.CancelVote(_reg); } private sealed class VoteDict : IReadOnlyDictionary { private readonly VoteReg _reg; public VoteDict(VoteReg reg) { _reg = reg; } public IEnumerator> GetEnumerator() { return _reg.Entries.Select(e => KeyValuePair.Create(e.Data, e.Votes)).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int Count => _reg.Entries.Length; public bool ContainsKey(object key) { return TryGetValue(key, out _); } public bool TryGetValue(object key, out int value) { var entry = _reg.Entries.FirstOrNull(a => a.Data.Equals(key)); if (entry != null) { value = entry.Value.Votes; return true; } value = default; return false; } public int this[object key] { get { if (!TryGetValue(key, out var votes)) { throw new KeyNotFoundException(); } return votes; } } public IEnumerable Keys => _reg.Entries.Select(c => c.Data); public IEnumerable Values => _reg.Entries.Select(c => c.Votes); } } #endregion } }