Re-organize all projects (#4166)
This commit is contained in:
22
Content.Server/Voting/Managers/IVoteManager.cs
Normal file
22
Content.Server/Voting/Managers/IVoteManager.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Server.Player;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Content.Server.Voting.Managers
|
||||
{
|
||||
public interface IVoteManager
|
||||
{
|
||||
IEnumerable<IVoteHandle> ActiveVotes { get; }
|
||||
bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote);
|
||||
|
||||
bool CanCallVote(IPlayerSession initiator);
|
||||
void CreateRestartVote(IPlayerSession? initiator);
|
||||
void CreatePresetVote(IPlayerSession? initiator);
|
||||
IVoteHandle CreateVote(VoteOptions options);
|
||||
|
||||
void Initialize();
|
||||
void Update();
|
||||
}
|
||||
}
|
||||
115
Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs
Normal file
115
Content.Server/Voting/Managers/VoteManager.DefaultVotes.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Voting.Managers
|
||||
{
|
||||
public sealed partial class VoteManager
|
||||
{
|
||||
public void CreateRestartVote(IPlayerSession? initiator)
|
||||
{
|
||||
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||
var options = new VoteOptions
|
||||
{
|
||||
Title = Loc.GetString("ui-vote-restart-title"),
|
||||
Options =
|
||||
{
|
||||
(Loc.GetString("ui-vote-restart-yes"), true),
|
||||
(Loc.GetString("ui-vote-restart-no"), false)
|
||||
},
|
||||
Duration = alone
|
||||
? TimeSpan.FromSeconds(10)
|
||||
: TimeSpan.FromSeconds(30),
|
||||
InitiatorTimeout = TimeSpan.FromMinutes(3)
|
||||
};
|
||||
|
||||
if (alone)
|
||||
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
WirePresetVoteInitiator(options, initiator);
|
||||
|
||||
var vote = CreateVote(options);
|
||||
|
||||
vote.OnFinished += (_, _) =>
|
||||
{
|
||||
var votesYes = vote.VotesPerOption[true];
|
||||
var votesNo = vote.VotesPerOption[false];
|
||||
var total = votesYes + votesNo;
|
||||
|
||||
var ratioRequired = _cfg.GetCVar(CCVars.VoteRestartRequiredRatio);
|
||||
if (votesYes / (float) total >= ratioRequired)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded"));
|
||||
_ticker.RestartRound();
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(
|
||||
Loc.GetString("ui-vote-restart-failed", ("ratio", ratioRequired)));
|
||||
}
|
||||
};
|
||||
|
||||
if (initiator != null)
|
||||
{
|
||||
// Cast yes vote if created the vote yourself.
|
||||
vote.CastVote(initiator, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void CreatePresetVote(IPlayerSession? initiator)
|
||||
{
|
||||
var presets = new Dictionary<string, string>
|
||||
{
|
||||
["traitor"] = "mode-traitor",
|
||||
["extended"] = "mode-extended",
|
||||
["sandbox"] = "mode-sandbox",
|
||||
["suspicion"] = "mode-suspicion",
|
||||
};
|
||||
|
||||
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||
var options = new VoteOptions
|
||||
{
|
||||
Title = Loc.GetString("ui-vote-gamemode-title"),
|
||||
Duration = alone
|
||||
? TimeSpan.FromSeconds(10)
|
||||
: TimeSpan.FromSeconds(30)
|
||||
};
|
||||
|
||||
if (alone)
|
||||
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
foreach (var (k, v) in presets)
|
||||
{
|
||||
options.Options.Add((Loc.GetString(v), k));
|
||||
}
|
||||
|
||||
WirePresetVoteInitiator(options, initiator);
|
||||
|
||||
var vote = CreateVote(options);
|
||||
|
||||
vote.OnFinished += (_, args) =>
|
||||
{
|
||||
string picked;
|
||||
if (args.Winner == null)
|
||||
{
|
||||
picked = (string) IoCManager.Resolve<IRobustRandom>().Pick(args.Winners);
|
||||
_chatManager.DispatchServerAnnouncement(
|
||||
Loc.GetString("ui-vote-gamemode-tie", ("picked", Loc.GetString(presets[picked]))));
|
||||
}
|
||||
else
|
||||
{
|
||||
picked = (string) args.Winner;
|
||||
_chatManager.DispatchServerAnnouncement(
|
||||
Loc.GetString("ui-vote-gamemode-win", ("winner", Loc.GetString(presets[picked]))));
|
||||
}
|
||||
|
||||
_ticker.SetStartPreset(picked);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
493
Content.Server/Voting/Managers/VoteManager.cs
Normal file
493
Content.Server/Voting/Managers/VoteManager.cs
Normal file
@@ -0,0 +1,493 @@
|
||||
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.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Collections;
|
||||
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.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
#nullable enable
|
||||
|
||||
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 IGameTicker _ticker = default!;
|
||||
[Dependency] private readonly IAdminManager _adminMgr = default!;
|
||||
|
||||
private int _nextVoteId = 1;
|
||||
|
||||
private readonly Dictionary<int, VoteReg> _votes = new();
|
||||
private readonly Dictionary<int, VoteHandle> _voteHandles = new();
|
||||
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _voteTimeout = new();
|
||||
private readonly HashSet<IPlayerSession> _playerCanCallVoteDirty = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgVoteData>(MsgVoteData.NAME);
|
||||
_netManager.RegisterNetMessage<MsgVoteCanCall>(MsgVoteCanCall.NAME);
|
||||
|
||||
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
|
||||
_adminMgr.OnPermsChanged += AdminPermsChanged;
|
||||
}
|
||||
|
||||
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<int>();
|
||||
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<NetUserId>();
|
||||
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 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.GetAllPlayers())
|
||||
{
|
||||
SendSingleUpdate(v, player);
|
||||
}
|
||||
|
||||
v.VotesDirty.Clear();
|
||||
v.Dirty = false;
|
||||
}
|
||||
|
||||
private void SendSingleUpdate(VoteReg v, IPlayerSession player)
|
||||
{
|
||||
var msg = _netManager.CreateNetMessage<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.GetAllPlayers());
|
||||
}
|
||||
|
||||
private void SendUpdateCanCallVote(IPlayerSession player)
|
||||
{
|
||||
var msg = _netManager.CreateNetMessage<MsgVoteCanCall>();
|
||||
msg.CanCall = CanCallVote(player);
|
||||
|
||||
_netManager.ServerSendMessage(msg, player.ConnectedClient);
|
||||
}
|
||||
|
||||
public bool CanCallVote(IPlayerSession player)
|
||||
{
|
||||
// Admins can always call votes.
|
||||
if (_adminMgr.HasAdminFlag(player, AdminFlags.Admin))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cannot start vote if vote is already active (as non-admin).
|
||||
if (_votes.Count != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !_voteTimeout.ContainsKey(player.UserId);
|
||||
}
|
||||
|
||||
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<IVoteHandle> 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<IPlayerSession, int> 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<IPlayerSession> 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<object, int> 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<object, int>
|
||||
{
|
||||
private readonly VoteReg _reg;
|
||||
|
||||
public VoteDict(VoteReg reg)
|
||||
{
|
||||
_reg = reg;
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<object, int>> 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<object> Keys => _reg.Entries.Select(c => c.Data);
|
||||
public IEnumerable<int> Values => _reg.Entries.Select(c => c.Votes);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user