Vote type delay, code comments.

Added doc comments to server side voting API.
There is now a 4 minute delay between creating votes of the same type.
Shuffled some code around.
Made a StandardVoteType enum instead of string IDs.
This commit is contained in:
Pieter-Jan Briers
2021-07-21 19:03:10 +02:00
parent 1187185b89
commit e9af56c7c3
13 changed files with 348 additions and 49 deletions

View File

@@ -1,21 +1,87 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Content.Server.Voting.Managers;
using Robust.Server.Player;
namespace Content.Server.Voting
{
/// <summary>
/// A handle to vote, active or past.
/// </summary>
/// <remarks>
/// <para>
/// Vote options are referred to by UI/networking as integer IDs.
/// These IDs are the index of the vote option in the <see cref="VoteOptions.Options"/> list
/// used to create the vote.
/// </para>
/// </remarks>
public interface IVoteHandle
{
/// <summary>
/// The numeric ID of the vote. Can be used in <see cref="IVoteManager.TryGetVote"/>.
/// </summary>
int Id { get; }
/// <summary>
/// The title of the vote.
/// </summary>
string Title { get; }
/// <summary>
/// Text representing who/what initiated the vote.
/// </summary>
string InitiatorText { get; }
/// <summary>
/// Whether the vote has finished and is no longer active.
/// </summary>
bool Finished { get; }
/// <summary>
/// Whether the vote was cancelled by an administrator and did not finish naturally.
/// </summary>
/// <remarks>
/// If this is true, <see cref="Finished"/> is also true.
/// </remarks>
bool Cancelled { get; }
/// <summary>
/// Current count of votes per option type.
/// </summary>
IReadOnlyDictionary<object, int> VotesPerOption { get; }
/// <summary>
/// Invoked when this vote has successfully finished.
/// </summary>
event VoteFinishedEventHandler OnFinished;
/// <summary>
/// Invoked if this vote gets cancelled.
/// </summary>
event VoteCancelledEventHandler OnCancelled;
/// <summary>
/// Check whether a certain integer option ID is valid.
/// </summary>
/// <param name="optionId">The integer ID of the option.</param>
/// <returns>True if the option ID is valid, false otherwise.</returns>
bool IsValidOption(int optionId);
/// <summary>
/// Cast a vote for a specific player.
/// </summary>
/// <param name="session">The player session to vote for.</param>
/// <param name="optionId">
/// The integer option ID to vote for. If null, "no vote" is selected (abstaining).
/// </param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="optionId"/> is not a valid option ID.
/// </exception>
void CastVote(IPlayerSession session, int? optionId);
/// <summary>
/// Cancel this vote.
/// </summary>
void Cancel();
}
}

View File

@@ -1,18 +1,64 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Voting;
using Robust.Server.Player;
namespace Content.Server.Voting.Managers
{
/// <summary>
/// Manages in-game votes that players can vote on.
/// </summary>
public interface IVoteManager
{
/// <summary>
/// All votes that are currently active and can be voted on by players.
/// </summary>
IEnumerable<IVoteHandle> ActiveVotes { get; }
/// <summary>
/// Try to get a vote handle by integer ID.
/// </summary>
/// <remarks>
/// Only votes that are currently active can be retrieved.
/// </remarks>
/// <param name="voteId">The integer ID of the vote, corresponding to <see cref="IVoteHandle.Id"/>.</param>
/// <param name="vote">The vote handle, if found.</param>
/// <returns>True if the vote was found and it was returned, false otherwise.</returns>
bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote);
bool CanCallVote(IPlayerSession initiator);
void CreateRestartVote(IPlayerSession? initiator);
void CreatePresetVote(IPlayerSession? initiator);
/// <summary>
/// Check if a player can initiate a vote right now. Optionally of a specified standard type.
/// </summary>
/// <remarks>
/// Players cannot start votes if they have made another vote recently,
/// or if the specified vote type has been made recently.
/// </remarks>
/// <param name="initiator">The player to check.</param>
/// <param name="voteType">
/// The standard vote type to check cooldown for.
/// Null to only check timeout for all vote types for the specified player.
/// </param>
/// <returns>
/// True if <paramref name="initiator"/> can start votes right now,
/// and if provided if they can start votes of type <paramref name="voteType"/>.
/// </returns>
bool CanCallVote(IPlayerSession initiator, StandardVoteType? voteType = null);
/// <summary>
/// Initiate a standard vote such as restart round, that can be initiated by players.
/// </summary>
/// <param name="initiator">
/// The player that called the vote.
/// If null it is assumed to be an automatic vote by the server.
/// </param>
/// <param name="voteType">The type of standard vote to make.</param>
void CreateStandardVote(IPlayerSession? initiator, StandardVoteType voteType);
/// <summary>
/// Create a non-standard vote with special parameters.
/// </summary>
/// <param name="options">The options specifying the vote's behavior.</param>
/// <returns>A handle to the created vote.</returns>
IVoteHandle CreateVote(VoteOptions options);
void Initialize();

View File

@@ -1,11 +1,10 @@
using System;
using System.Collections.Generic;
using Content.Server.GameTicking;
using Content.Shared;
using Content.Shared.CCVar;
using Content.Shared.Voting;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Random;
@@ -13,7 +12,24 @@ namespace Content.Server.Voting.Managers
{
public sealed partial class VoteManager
{
public void CreateRestartVote(IPlayerSession? initiator)
public void CreateStandardVote(IPlayerSession? initiator, StandardVoteType voteType)
{
switch (voteType)
{
case StandardVoteType.Restart:
CreateRestartVote(initiator);
break;
case StandardVoteType.Preset:
CreatePresetVote(initiator);
break;
default:
throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null);
}
TimeoutStandardVote(voteType);
}
private void CreateRestartVote(IPlayerSession? initiator)
{
var alone = _playerManager.PlayerCount == 1 && initiator != null;
var options = new VoteOptions
@@ -72,7 +88,7 @@ namespace Content.Server.Voting.Managers
}
}
public void CreatePresetVote(IPlayerSession? initiator)
private void CreatePresetVote(IPlayerSession? initiator)
{
var presets = new Dictionary<string, string>
{
@@ -122,5 +138,12 @@ namespace Content.Server.Voting.Managers
EntitySystem.Get<GameTicker>().SetStartPreset(picked);
};
}
private void TimeoutStandardVote(StandardVoteType type)
{
var timeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout));
_standardVoteTimeout[type] = _timing.RealTime + timeout;
DirtyCanCallVoteAll();
}
}
}

View File

@@ -7,7 +7,6 @@ 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;
@@ -39,6 +38,7 @@ namespace Content.Server.Voting.Managers
private readonly Dictionary<int, VoteReg> _votes = new();
private readonly Dictionary<int, VoteHandle> _voteHandles = new();
private readonly Dictionary<StandardVoteType, TimeSpan> _standardVoteTimeout = new();
private readonly Dictionary<NetUserId, TimeSpan> _voteTimeout = new();
private readonly HashSet<IPlayerSession> _playerCanCallVoteDirty = new();
@@ -146,6 +146,21 @@ namespace Content.Server.Voting.Managers
DirtyCanCallVote(session);
}
// Handle standard vote timeouts.
var stdTimeoutRemQueue = new RemQueue<StandardVoteType>();
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)
{
@@ -240,26 +255,47 @@ namespace Content.Server.Voting.Managers
private void SendUpdateCanCallVote(IPlayerSession player)
{
var msg = _netManager.CreateNetMessage<MsgVoteCanCall>();
msg.CanCall = CanCallVote(player);
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();
_netManager.ServerSendMessage(msg, player.ConnectedClient);
}
public bool CanCallVote(IPlayerSession player)
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(player, AdminFlags.Admin))
if (_adminMgr.HasAdminFlag(initiator, AdminFlags.Admin))
{
isAdmin = true;
return true;
}
// Cannot start vote if vote is already active (as non-admin).
if (_votes.Count != 0)
{
return false;
}
return !_voteTimeout.ContainsKey(player.UserId);
// 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))
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)
@@ -484,7 +520,7 @@ namespace Content.Server.Voting.Managers
}
public IEnumerable<object> Keys => _reg.Entries.Select(c => c.Data);
public IEnumerable<int> Values => _reg.Entries.Select(c => c.Votes);
public IEnumerable<int> Values => _reg.Entries.Select(c => c.Votes);
}
}

View File

@@ -1,16 +1,15 @@
using System;
using System.Linq;
using System.Text;
using Content.Server.Administration;
using Content.Server.Chat.Managers;
using Content.Server.Voting.Managers;
using Content.Shared.Administration;
using Content.Shared.Voting;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
namespace Content.Server.Voting
{
[AnyCommand]
@@ -28,28 +27,21 @@ namespace Content.Server.Voting
return;
}
var type = args[0];
if (!Enum.TryParse<StandardVoteType>(args[0], ignoreCase: true, out var type))
{
shell.WriteError(Loc.GetString("create-vote-command-invalid-vote-type"));
return;
}
var mgr = IoCManager.Resolve<IVoteManager>();
if (shell.Player != null && !mgr.CanCallVote((IPlayerSession) shell.Player))
if (shell.Player != null && !mgr.CanCallVote((IPlayerSession) shell.Player, type))
{
shell.WriteError(Loc.GetString("create-vote-command-cannot-call-vote-now"));
return;
}
switch (type)
{
case "restart":
mgr.CreateRestartVote((IPlayerSession?) shell.Player);
break;
case "preset":
mgr.CreatePresetVote((IPlayerSession?) shell.Player);
break;
default:
shell.WriteError(Loc.GetString("create-vote-command-invalid-vote-type"));
break;
}
mgr.CreateStandardVote((IPlayerSession?) shell.Player, type);
}
}