Add Votekick functionality (#32005)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<ui:VoteCallMenu xmlns="https://spacestation14.io"
|
||||
<ui:VoteCallMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.Voting.UI"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MouseFilter="Stop" MinSize="350 150">
|
||||
MouseFilter="Stop" MinSize="350 200">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Margin="8 0" Orientation="Horizontal">
|
||||
@@ -13,16 +13,18 @@
|
||||
<controls:HighDivider />
|
||||
|
||||
<BoxContainer Orientation="Vertical" Margin="8 2 8 0" VerticalExpand="True" VerticalAlignment="Top">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<OptionButton Name="VoteTypeButton" HorizontalExpand="True" />
|
||||
<Control HorizontalExpand="True">
|
||||
<OptionButton Name="VoteSecondButton" Visible="False" />
|
||||
</Control>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<OptionButton Margin="2 1" Name="VoteTypeButton" HorizontalExpand="False" />
|
||||
<BoxContainer Name="VoteOptionsButtonContainer" HorizontalExpand="False" Orientation="Vertical">
|
||||
</BoxContainer>
|
||||
<Button Margin="64 4" Name="FollowButton" Text="{Loc 'ui-vote-follow-button'}" Visible="False" />
|
||||
<Label Margin="2 2" Name="VoteNotTrustedLabel" Text="{Loc 'ui-vote-trusted-users-notice'}" Visible="False" />
|
||||
<Label Margin="2 2" Name="VoteWarningLabel" Text="{Loc 'ui-vote-abuse-warning'}" Visible="False" HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
<Label Name="VoteTypeTimeoutLabel" Visible="False" />
|
||||
<Label Margin="8 2" Name="VoteTypeTimeoutLabel" Visible="False" />
|
||||
</BoxContainer>
|
||||
|
||||
<Button Margin="8 2" Name="CreateButton" Text="{Loc 'ui-vote-create-button'}" />
|
||||
|
||||
<Button Margin="8 32 8 2" Name="CreateButton" Text="{Loc 'ui-vote-create-button'}" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<Label Margin="12 0 0 0" StyleClasses="LabelSubText" Text="{Loc 'ui-vote-fluff'}" />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Voting;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
@@ -9,10 +11,8 @@ using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -25,32 +25,54 @@ namespace Content.Client.Voting.UI
|
||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _entNetManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public static readonly (string name, StandardVoteType type, (string name, string id)[]? secondaries)[]
|
||||
AvailableVoteTypes =
|
||||
{
|
||||
("ui-vote-type-restart", StandardVoteType.Restart, null),
|
||||
("ui-vote-type-gamemode", StandardVoteType.Preset, null),
|
||||
("ui-vote-type-map", StandardVoteType.Map, null)
|
||||
};
|
||||
private VotingSystem _votingSystem;
|
||||
|
||||
public StandardVoteType Type;
|
||||
|
||||
public Dictionary<StandardVoteType, CreateVoteOption> AvailableVoteOptions = new Dictionary<StandardVoteType, CreateVoteOption>()
|
||||
{
|
||||
{ StandardVoteType.Restart, new CreateVoteOption("ui-vote-type-restart", new(), false, null) },
|
||||
{ StandardVoteType.Preset, new CreateVoteOption("ui-vote-type-gamemode", new(), false, null) },
|
||||
{ StandardVoteType.Map, new CreateVoteOption("ui-vote-type-map", new(), false, null) },
|
||||
{ StandardVoteType.Votekick, new CreateVoteOption("ui-vote-type-votekick", new(), true, 0) }
|
||||
};
|
||||
|
||||
public Dictionary<string, string> VotekickReasons = new Dictionary<string, string>()
|
||||
{
|
||||
{ VotekickReasonType.Raiding.ToString(), Loc.GetString("ui-vote-votekick-type-raiding") },
|
||||
{ VotekickReasonType.Cheating.ToString(), Loc.GetString("ui-vote-votekick-type-cheating") },
|
||||
{ VotekickReasonType.Spam.ToString(), Loc.GetString("ui-vote-votekick-type-spamming") }
|
||||
};
|
||||
|
||||
public Dictionary<NetUserId, (NetEntity, string)> PlayerList = new();
|
||||
|
||||
public OptionButton? _followDropdown = null;
|
||||
|
||||
public bool IsAllowedVotekick = false;
|
||||
|
||||
public VoteCallMenu()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
_votingSystem = _entityManager.System<VotingSystem>();
|
||||
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime) / 60));
|
||||
|
||||
for (var i = 0; i < AvailableVoteTypes.Length; i++)
|
||||
foreach (StandardVoteType voteType in Enum.GetValues<StandardVoteType>())
|
||||
{
|
||||
var (text, _, _) = AvailableVoteTypes[i];
|
||||
VoteTypeButton.AddItem(Loc.GetString(text), i);
|
||||
var option = AvailableVoteOptions[voteType];
|
||||
VoteTypeButton.AddItem(Loc.GetString(option.Name), (int)voteType);
|
||||
}
|
||||
|
||||
VoteTypeButton.OnItemSelected += VoteTypeSelected;
|
||||
VoteSecondButton.OnItemSelected += VoteSecondSelected;
|
||||
CreateButton.OnPressed += CreatePressed;
|
||||
FollowButton.OnPressed += FollowSelected;
|
||||
}
|
||||
|
||||
protected override void Opened()
|
||||
@@ -60,6 +82,8 @@ namespace Content.Client.Voting.UI
|
||||
_netManager.ClientSendMessage(new MsgVoteMenu());
|
||||
|
||||
_voteManager.CanCallVoteChanged += CanCallVoteChanged;
|
||||
_votingSystem.VotePlayerListResponse += UpdateVotePlayerList;
|
||||
_votingSystem.RequestVotePlayerList();
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
@@ -67,6 +91,7 @@ namespace Content.Client.Voting.UI
|
||||
base.Close();
|
||||
|
||||
_voteManager.CanCallVoteChanged -= CanCallVoteChanged;
|
||||
_votingSystem.VotePlayerListResponse -= UpdateVotePlayerList;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
@@ -82,21 +107,50 @@ namespace Content.Client.Voting.UI
|
||||
Close();
|
||||
}
|
||||
|
||||
private void UpdateVotePlayerList(VotePlayerListResponseEvent msg)
|
||||
{
|
||||
Dictionary<string, string> optionsList = new();
|
||||
Dictionary<NetUserId, (NetEntity, string)> playerList = new();
|
||||
foreach ((NetUserId, NetEntity, string) player in msg.Players)
|
||||
{
|
||||
optionsList.Add(player.Item1.ToString(), player.Item3);
|
||||
playerList.Add(player.Item1, (player.Item2, player.Item3));
|
||||
}
|
||||
if (optionsList.Count == 0)
|
||||
optionsList.Add(" ", " ");
|
||||
|
||||
PlayerList = playerList;
|
||||
|
||||
IsAllowedVotekick = !msg.Denied;
|
||||
|
||||
var updatedDropdownOption = AvailableVoteOptions[StandardVoteType.Votekick];
|
||||
updatedDropdownOption.Dropdowns = new List<Dictionary<string, string>>() { optionsList, VotekickReasons };
|
||||
AvailableVoteOptions[StandardVoteType.Votekick] = updatedDropdownOption;
|
||||
}
|
||||
|
||||
private void CreatePressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
var typeId = VoteTypeButton.SelectedId;
|
||||
var (_, typeKey, secondaries) = AvailableVoteTypes[typeId];
|
||||
var voteType = AvailableVoteOptions[(StandardVoteType)typeId];
|
||||
|
||||
if (secondaries != null)
|
||||
var commandArgs = "";
|
||||
|
||||
if (voteType.Dropdowns == null || voteType.Dropdowns.Count == 0)
|
||||
{
|
||||
var secondaryId = VoteSecondButton.SelectedId;
|
||||
var (_, secondKey) = secondaries[secondaryId];
|
||||
|
||||
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey} {secondKey}");
|
||||
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {((StandardVoteType)typeId).ToString()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {typeKey}");
|
||||
int i = 0;
|
||||
foreach(var dropdowns in VoteOptionsButtonContainer.Children)
|
||||
{
|
||||
if (dropdowns is OptionButton optionButton && AvailableVoteOptions[(StandardVoteType)typeId].Dropdowns != null)
|
||||
{
|
||||
commandArgs += AvailableVoteOptions[(StandardVoteType)typeId].Dropdowns[i].ElementAt(optionButton.SelectedId).Key + " ";
|
||||
i++;
|
||||
}
|
||||
}
|
||||
_consoleHost.LocalShell.RemoteExecuteCommand($"createvote {((StandardVoteType)typeId).ToString()} {commandArgs}");
|
||||
}
|
||||
|
||||
Close();
|
||||
@@ -104,9 +158,16 @@ namespace Content.Client.Voting.UI
|
||||
|
||||
private void UpdateVoteTimeout()
|
||||
{
|
||||
var (_, typeKey, _) = AvailableVoteTypes[VoteTypeButton.SelectedId];
|
||||
var typeKey = (StandardVoteType)VoteTypeButton.SelectedId;
|
||||
var isAvailable = _voteManager.CanCallStandardVote(typeKey, out var timeout);
|
||||
CreateButton.Disabled = !isAvailable;
|
||||
if (typeKey == StandardVoteType.Votekick && !IsAllowedVotekick)
|
||||
{
|
||||
CreateButton.Disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateButton.Disabled = !isAvailable;
|
||||
}
|
||||
VoteTypeTimeoutLabel.Visible = !isAvailable;
|
||||
|
||||
if (!isAvailable)
|
||||
@@ -123,29 +184,73 @@ namespace Content.Client.Voting.UI
|
||||
}
|
||||
}
|
||||
|
||||
private static void VoteSecondSelected(OptionButton.ItemSelectedEventArgs obj)
|
||||
private static void ButtonSelected(OptionButton.ItemSelectedEventArgs obj)
|
||||
{
|
||||
obj.Button.SelectId(obj.Id);
|
||||
}
|
||||
|
||||
private void FollowSelected(Button.ButtonEventArgs obj)
|
||||
{
|
||||
if (_followDropdown == null)
|
||||
return;
|
||||
|
||||
if (_followDropdown.SelectedId >= PlayerList.Count)
|
||||
return;
|
||||
|
||||
var netEntity = PlayerList.ElementAt(_followDropdown.SelectedId).Value.Item1;
|
||||
|
||||
var msg = new GhostWarpToTargetRequestEvent(netEntity);
|
||||
_entNetManager.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
|
||||
private void VoteTypeSelected(OptionButton.ItemSelectedEventArgs obj)
|
||||
{
|
||||
VoteTypeButton.SelectId(obj.Id);
|
||||
|
||||
var (_, _, options) = AvailableVoteTypes[obj.Id];
|
||||
if (options == null)
|
||||
VoteNotTrustedLabel.Visible = false;
|
||||
if ((StandardVoteType)obj.Id == StandardVoteType.Votekick)
|
||||
{
|
||||
VoteSecondButton.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
VoteSecondButton.Visible = true;
|
||||
VoteSecondButton.Clear();
|
||||
|
||||
for (var i = 0; i < options.Length; i++)
|
||||
if (!IsAllowedVotekick)
|
||||
{
|
||||
var (text, _) = options[i];
|
||||
VoteSecondButton.AddItem(Loc.GetString(text), i);
|
||||
VoteNotTrustedLabel.Visible = true;
|
||||
var updatedDropdownOption = AvailableVoteOptions[StandardVoteType.Votekick];
|
||||
updatedDropdownOption.Dropdowns = new List<Dictionary<string, string>>();
|
||||
AvailableVoteOptions[StandardVoteType.Votekick] = updatedDropdownOption;
|
||||
}
|
||||
else
|
||||
{
|
||||
_votingSystem.RequestVotePlayerList();
|
||||
}
|
||||
}
|
||||
|
||||
VoteWarningLabel.Visible = AvailableVoteOptions[(StandardVoteType)obj.Id].EnableVoteWarning;
|
||||
FollowButton.Visible = false;
|
||||
|
||||
var voteList = AvailableVoteOptions[(StandardVoteType)obj.Id].Dropdowns;
|
||||
|
||||
VoteOptionsButtonContainer.RemoveAllChildren();
|
||||
if (voteList != null)
|
||||
{
|
||||
int i = 0;
|
||||
foreach (var voteDropdown in voteList)
|
||||
{
|
||||
var optionButton = new OptionButton();
|
||||
int j = 0;
|
||||
foreach (var (key, value) in voteDropdown)
|
||||
{
|
||||
optionButton.AddItem(Loc.GetString(value), j);
|
||||
j++;
|
||||
}
|
||||
VoteOptionsButtonContainer.AddChild(optionButton);
|
||||
optionButton.Visible = true;
|
||||
optionButton.OnItemSelected += ButtonSelected;
|
||||
optionButton.Margin = new Thickness(2, 1);
|
||||
if (AvailableVoteOptions[(StandardVoteType)obj.Id].FollowDropdownId != null && AvailableVoteOptions[(StandardVoteType)obj.Id].FollowDropdownId == i)
|
||||
{
|
||||
_followDropdown = optionButton;
|
||||
FollowButton.Visible = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,4 +273,20 @@ namespace Content.Client.Voting.UI
|
||||
new VoteCallMenu().OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public record struct CreateVoteOption
|
||||
{
|
||||
public string Name;
|
||||
public List<Dictionary<string, string>> Dropdowns;
|
||||
public bool EnableVoteWarning;
|
||||
public int? FollowDropdownId; // If set, this will enable the Follow button and use the dropdown matching the ID as input.
|
||||
|
||||
public CreateVoteOption(string name, List<Dictionary<string, string>> dropdowns, bool enableVoteWarning, int? followDropdownId)
|
||||
{
|
||||
Name = name;
|
||||
Dropdowns = dropdowns;
|
||||
EnableVoteWarning = enableVoteWarning;
|
||||
FollowDropdownId = followDropdownId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<Control xmlns="https://spacestation14.io" MinWidth="300" MaxWidth="500">
|
||||
<Control xmlns="https://spacestation14.io" MinWidth="300" MaxWidth="500">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<BoxContainer Margin="4" Orientation="Vertical">
|
||||
<Label Name="VoteCaller" />
|
||||
<RichTextLabel Name="VoteTitle" />
|
||||
|
||||
<GridContainer Columns="3" Name="VoteOptionsContainer" />
|
||||
<Button Margin="4 4" Name="FollowVoteTarget" Text="{Loc 'ui-vote-follow-button-popup'}" Visible="False"></Button>
|
||||
|
||||
<GridContainer Columns="3" Name="VoteOptionsContainer"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<ProgressBar Margin="4" HorizontalExpand="True" Name="TimeLeftBar" MinValue="0" MaxValue="1" />
|
||||
<Label Name="TimeLeftText" />
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -17,9 +15,11 @@ namespace Content.Client.Voting.UI
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _net = default!;
|
||||
|
||||
private readonly VoteManager.ActiveVote _vote;
|
||||
private readonly Button[] _voteButtons;
|
||||
private readonly NetEntity? _targetEntity;
|
||||
|
||||
public VotePopup(VoteManager.ActiveVote vote)
|
||||
{
|
||||
@@ -29,6 +29,13 @@ namespace Content.Client.Voting.UI
|
||||
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||
|
||||
if (_vote.TargetEntity != null && _vote.TargetEntity != 0)
|
||||
{
|
||||
_targetEntity = new NetEntity(_vote.TargetEntity.Value);
|
||||
FollowVoteTarget.Visible = true;
|
||||
FollowVoteTarget.OnPressed += _ => AttemptFollowVoteEntity();
|
||||
}
|
||||
|
||||
Modulate = Color.White.WithAlpha(0.75f);
|
||||
_voteButtons = new Button[vote.Entries.Length];
|
||||
var group = new ButtonGroup();
|
||||
@@ -55,13 +62,29 @@ namespace Content.Client.Voting.UI
|
||||
for (var i = 0; i < _voteButtons.Length; i++)
|
||||
{
|
||||
var entry = _vote.Entries[i];
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
|
||||
if (_vote.DisplayVotes)
|
||||
{
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button", ("text", entry.Text), ("votes", entry.Votes));
|
||||
}
|
||||
else
|
||||
{
|
||||
_voteButtons[i].Text = Loc.GetString("ui-vote-button-no-votes", ("text", entry.Text));
|
||||
}
|
||||
|
||||
if (_vote.OurVote == i)
|
||||
_voteButtons[i].Pressed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void AttemptFollowVoteEntity()
|
||||
{
|
||||
if (_targetEntity != null)
|
||||
{
|
||||
var msg = new GhostWarpToTargetRequestEvent(_targetEntity.Value);
|
||||
_net.SendSystemNetworkMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
// Logger.Debug($"{_gameTiming.ServerTime}, {_vote.StartTime}, {_vote.EndTime}");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Voting;
|
||||
@@ -184,6 +184,8 @@ namespace Content.Client.Voting
|
||||
existingVote.Title = message.VoteTitle;
|
||||
existingVote.StartTime = _gameTiming.RealServerToLocal(message.StartTime);
|
||||
existingVote.EndTime = _gameTiming.RealServerToLocal(message.EndTime);
|
||||
existingVote.DisplayVotes = message.DisplayVotes;
|
||||
existingVote.TargetEntity = message.TargetEntity;
|
||||
|
||||
// Logger.Debug($"{existingVote.StartTime}, {existingVote.EndTime}, {_gameTiming.RealTime}");
|
||||
|
||||
@@ -245,7 +247,8 @@ namespace Content.Client.Voting
|
||||
public string Initiator = "";
|
||||
public int? OurVote;
|
||||
public int Id;
|
||||
|
||||
public bool DisplayVotes;
|
||||
public int? TargetEntity; // NetEntity
|
||||
public ActiveVote(int voteId)
|
||||
{
|
||||
Id = voteId;
|
||||
|
||||
34
Content.Client/Voting/VotingSystem.cs
Normal file
34
Content.Client/Voting/VotingSystem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Content.Client.Ghost;
|
||||
using Content.Shared.Voting;
|
||||
|
||||
namespace Content.Client.Voting;
|
||||
|
||||
public sealed class VotingSystem : EntitySystem
|
||||
{
|
||||
|
||||
public event Action<VotePlayerListResponseEvent>? VotePlayerListResponse; //Provides a list of players elligble for vote actions
|
||||
|
||||
[Dependency] private readonly GhostSystem _ghostSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<VotePlayerListResponseEvent>(OnVotePlayerListResponseEvent);
|
||||
}
|
||||
|
||||
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
|
||||
{
|
||||
if (!_ghostSystem.IsGhost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VotePlayerListResponse?.Invoke(msg);
|
||||
}
|
||||
|
||||
public void RequestVotePlayerList()
|
||||
{
|
||||
RaiseNetworkEvent(new VotePlayerListRequestEvent());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Voting.Managers;
|
||||
using Content.Server.Voting.Managers;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Voting
|
||||
@@ -43,6 +43,11 @@ namespace Content.Server.Voting
|
||||
/// </remarks>
|
||||
bool Cancelled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary of votes cast by players, matching the option's id.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<ICommonSession, int> CastVotes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current count of votes per option type.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Content.Server.Voting.Managers
|
||||
/// 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(ICommonSession? initiator, StandardVoteType voteType);
|
||||
void CreateStandardVote(ICommonSession? initiator, StandardVoteType voteType, string[]? args = null);
|
||||
|
||||
/// <summary>
|
||||
/// Create a non-standard vote with special parameters.
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -16,20 +23,33 @@ namespace Content.Server.Voting.Managers
|
||||
{
|
||||
public sealed partial class VoteManager
|
||||
{
|
||||
[Dependency] private readonly IPlayerLocator _locator = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IBanManager _bans = default!;
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
|
||||
private VotingSystem? _votingSystem;
|
||||
private RoleSystem? _roleSystem;
|
||||
|
||||
private static readonly Dictionary<StandardVoteType, CVarDef<bool>> _voteTypesToEnableCVars = new()
|
||||
{
|
||||
{StandardVoteType.Restart, CCVars.VoteRestartEnabled},
|
||||
{StandardVoteType.Preset, CCVars.VotePresetEnabled},
|
||||
{StandardVoteType.Map, CCVars.VoteMapEnabled},
|
||||
{StandardVoteType.Votekick, CCVars.VotekickEnabled}
|
||||
};
|
||||
|
||||
public void CreateStandardVote(ICommonSession? initiator, StandardVoteType voteType)
|
||||
public void CreateStandardVote(ICommonSession? initiator, StandardVoteType voteType, string[]? args = null)
|
||||
{
|
||||
if (initiator != null)
|
||||
if (initiator != null && args == null)
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"{initiator} initiated a {voteType.ToString()} vote");
|
||||
else if (initiator != null && args != null)
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"{initiator} initiated a {voteType.ToString()} vote with the arguments: {String.Join(",", args)}");
|
||||
else
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Initiated a {voteType.ToString()} vote");
|
||||
|
||||
bool timeoutVote = true;
|
||||
|
||||
switch (voteType)
|
||||
{
|
||||
case StandardVoteType.Restart:
|
||||
@@ -41,12 +61,17 @@ namespace Content.Server.Voting.Managers
|
||||
case StandardVoteType.Map:
|
||||
CreateMapVote(initiator);
|
||||
break;
|
||||
case StandardVoteType.Votekick:
|
||||
timeoutVote = false; // Allows the timeout to be updated manually in the create method
|
||||
CreateVotekickVote(initiator, args);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(voteType), voteType, null);
|
||||
}
|
||||
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
|
||||
ticker.UpdateInfoText();
|
||||
TimeoutStandardVote(voteType);
|
||||
if (timeoutVote)
|
||||
TimeoutStandardVote(voteType);
|
||||
}
|
||||
|
||||
private void CreateRestartVote(ICommonSession? initiator)
|
||||
@@ -56,104 +81,127 @@ namespace Content.Server.Voting.Managers
|
||||
var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
|
||||
|
||||
var ghostVotePercentageRequirement = _cfg.GetCVar(CCVars.VoteRestartGhostPercentage);
|
||||
var ghostCount = 0;
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
_playerManager.UpdateState(player);
|
||||
if (player.Status != SessionStatus.Disconnected && _entityManager.HasComponent<GhostComponent>(player.AttachedEntity))
|
||||
{
|
||||
ghostCount++;
|
||||
}
|
||||
}
|
||||
var ghostVoterPercentage = CalculateEligibleVoterPercentage(VoterEligibility.Ghost);
|
||||
|
||||
var ghostPercentage = 0.0;
|
||||
if (totalPlayers > 0)
|
||||
{
|
||||
ghostPercentage = ((double)ghostCount / totalPlayers) * 100;
|
||||
}
|
||||
|
||||
var roundedGhostPercentage = (int)Math.Round(ghostPercentage);
|
||||
|
||||
if (totalPlayers <= playerVoteMaximum || roundedGhostPercentage >= ghostVotePercentageRequirement)
|
||||
if (totalPlayers <= playerVoteMaximum || ghostVoterPercentage >= ghostVotePercentageRequirement)
|
||||
{
|
||||
StartVote(initiator);
|
||||
}
|
||||
else
|
||||
{
|
||||
NotifyNotEnoughGhostPlayers(ghostVotePercentageRequirement, roundedGhostPercentage);
|
||||
NotifyNotEnoughGhostPlayers(ghostVotePercentageRequirement, ghostVoterPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the current percentage of players eligible to vote, rounded to nearest percentage point.
|
||||
/// </summary>
|
||||
/// <param name="eligibility">The eligibility requirement to vote.</param>
|
||||
public int CalculateEligibleVoterPercentage(VoterEligibility eligibility)
|
||||
{
|
||||
var eligibleCount = CalculateEligibleVoterNumber(eligibility);
|
||||
var totalPlayers = _playerManager.Sessions.Count(session => session.Status != SessionStatus.Disconnected);
|
||||
|
||||
var eligiblePercentage = 0.0;
|
||||
if (totalPlayers > 0)
|
||||
{
|
||||
eligiblePercentage = ((double)eligibleCount / totalPlayers) * 100;
|
||||
}
|
||||
|
||||
var roundedEligiblePercentage = (int)Math.Round(eligiblePercentage);
|
||||
|
||||
return roundedEligiblePercentage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the current number of players eligible to vote.
|
||||
/// </summary>
|
||||
/// <param name="eligibility">The eligibility requirement to vote.</param>
|
||||
public int CalculateEligibleVoterNumber(VoterEligibility eligibility)
|
||||
{
|
||||
var eligibleCount = 0;
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
_playerManager.UpdateState(player);
|
||||
if (player.Status != SessionStatus.Disconnected && CheckVoterEligibility(player, eligibility))
|
||||
{
|
||||
eligibleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return eligibleCount;
|
||||
}
|
||||
|
||||
private void StartVote(ICommonSession? initiator)
|
||||
{
|
||||
var alone = _playerManager.PlayerCount == 1 && initiator != null;
|
||||
var options = new VoteOptions
|
||||
var options = new VoteOptions
|
||||
{
|
||||
Title = Loc.GetString("ui-vote-restart-title"),
|
||||
Options =
|
||||
{
|
||||
Title = Loc.GetString("ui-vote-restart-title"),
|
||||
Options =
|
||||
{
|
||||
(Loc.GetString("ui-vote-restart-yes"), "yes"),
|
||||
(Loc.GetString("ui-vote-restart-no"), "no"),
|
||||
(Loc.GetString("ui-vote-restart-abstain"), "abstain")
|
||||
},
|
||||
Duration = alone
|
||||
? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
|
||||
: TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerRestart)),
|
||||
InitiatorTimeout = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
(Loc.GetString("ui-vote-restart-yes"), "yes"),
|
||||
(Loc.GetString("ui-vote-restart-no"), "no"),
|
||||
(Loc.GetString("ui-vote-restart-abstain"), "abstain")
|
||||
},
|
||||
Duration = alone
|
||||
? TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerAlone))
|
||||
: TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteTimerRestart)),
|
||||
InitiatorTimeout = TimeSpan.FromMinutes(5)
|
||||
};
|
||||
|
||||
if (alone)
|
||||
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||
if (alone)
|
||||
options.InitiatorTimeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
WirePresetVoteInitiator(options, initiator);
|
||||
WirePresetVoteInitiator(options, initiator);
|
||||
|
||||
var vote = CreateVote(options);
|
||||
var vote = CreateVote(options);
|
||||
|
||||
vote.OnFinished += (_, _) =>
|
||||
vote.OnFinished += (_, _) =>
|
||||
{
|
||||
var votesYes = vote.VotesPerOption["yes"];
|
||||
var votesNo = vote.VotesPerOption["no"];
|
||||
var total = votesYes + votesNo;
|
||||
|
||||
var ratioRequired = _cfg.GetCVar(CCVars.VoteRestartRequiredRatio);
|
||||
if (total > 0 && votesYes / (float) total >= ratioRequired)
|
||||
{
|
||||
var votesYes = vote.VotesPerOption["yes"];
|
||||
var votesNo = vote.VotesPerOption["no"];
|
||||
var total = votesYes + votesNo;
|
||||
|
||||
var ratioRequired = _cfg.GetCVar(CCVars.VoteRestartRequiredRatio);
|
||||
if (total > 0 && votesYes / (float) total >= ratioRequired)
|
||||
// Check if an admin is online, and ignore the passed vote if the cvar is enabled
|
||||
if (_cfg.GetCVar(CCVars.VoteRestartNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
|
||||
{
|
||||
// Check if an admin is online, and ignore the passed vote if the cvar is enabled
|
||||
if (_cfg.GetCVar(CCVars.VoteRestartNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote attempted to pass, but an admin was online. {votesYes}/{votesNo}");
|
||||
}
|
||||
else // If the cvar is disabled or there's no admins on, proceed as normal
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote succeeded: {votesYes}/{votesNo}");
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded"));
|
||||
var roundEnd = _entityManager.EntitySysManager.GetEntitySystem<RoundEndSystem>();
|
||||
roundEnd.EndRound();
|
||||
}
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote attempted to pass, but an admin was online. {votesYes}/{votesNo}");
|
||||
}
|
||||
else
|
||||
else // If the cvar is disabled or there's no admins on, proceed as normal
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: {votesYes}/{votesNo}");
|
||||
_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);
|
||||
}
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player != initiator)
|
||||
{
|
||||
// Everybody else defaults to an abstain vote to say they don't mind.
|
||||
vote.CastVote(player, 2);
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote succeeded: {votesYes}/{votesNo}");
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-restart-succeeded"));
|
||||
var roundEnd = _entityManager.EntitySysManager.GetEntitySystem<RoundEndSystem>();
|
||||
roundEnd.EndRound();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Restart vote failed: {votesYes}/{votesNo}");
|
||||
_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);
|
||||
}
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player != initiator)
|
||||
{
|
||||
// Everybody else defaults to an abstain vote to say they don't mind.
|
||||
vote.CastVote(player, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NotifyNotEnoughGhostPlayers(int ghostPercentageRequirement, int roundedGhostPercentage)
|
||||
@@ -275,6 +323,230 @@ namespace Content.Server.Voting.Managers
|
||||
};
|
||||
}
|
||||
|
||||
private async void CreateVotekickVote(ICommonSession? initiator, string[]? args)
|
||||
{
|
||||
if (args == null || args.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_roleSystem == null)
|
||||
_roleSystem = _entityManager.SystemOrNull<RoleSystem>();
|
||||
if (_votingSystem == null)
|
||||
_votingSystem = _entityManager.SystemOrNull<VotingSystem>();
|
||||
|
||||
// Check that the initiator is actually allowed to do a votekick.
|
||||
if (_votingSystem != null && !await _votingSystem.CheckVotekickInitEligibility(initiator))
|
||||
{
|
||||
_logManager.GetSawmill("admin.votekick").Warning($"User {initiator} attempted a votekick, despite not being eligible to!");
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator}, but they are not eligible to votekick!");
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
var eligibleVoterNumberRequirement = _cfg.GetCVar(CCVars.VotekickEligibleNumberRequirement);
|
||||
var eligibleVoterNumber = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? CalculateEligibleVoterNumber(VoterEligibility.GhostMinimumPlaytime) : CalculateEligibleVoterNumber(VoterEligibility.MinimumPlaytime);
|
||||
|
||||
string target = args[0];
|
||||
string reason = args[1];
|
||||
|
||||
// Start by getting all relevant target data
|
||||
var located = await _locator.LookupIdByNameOrIdAsync(target);
|
||||
if (located == null)
|
||||
{
|
||||
_logManager.GetSawmill("admin.votekick")
|
||||
.Warning($"Votekick attempted for player {target} but they couldn't be found!");
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
var targetUid = located.UserId;
|
||||
var targetHWid = located.LastHWId;
|
||||
if (!_playerManager.TryGetSessionById(located.UserId, out ICommonSession? targetSession))
|
||||
{
|
||||
_logManager.GetSawmill("admin.votekick")
|
||||
.Warning($"Votekick attempted for player {target} but their session couldn't be found!");
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player string {target}, but they could not be found!");
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
string targetEntityName = located.Username; // Target's player-facing name when voting; uses the player's username as fallback if no entity name is found
|
||||
if (targetSession.AttachedEntity is { Valid: true } attached && _votingSystem != null)
|
||||
targetEntityName = _votingSystem.GetPlayerVoteListName(attached);
|
||||
|
||||
var isAntagSafe = false;
|
||||
var targetMind = targetSession.GetMind();
|
||||
var playtime = _playtimeManager.GetPlayTimes(targetSession);
|
||||
|
||||
// Check whether the target is an antag, and if they are, give them protection against the Raider votekick if they have the requisite hours.
|
||||
if (targetMind != null &&
|
||||
_roleSystem != null &&
|
||||
_roleSystem.MindIsAntagonist(targetMind) &&
|
||||
playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) &&
|
||||
overallTime >= TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickAntagRaiderProtection)))
|
||||
{
|
||||
isAntagSafe = true;
|
||||
}
|
||||
|
||||
|
||||
// Don't let a user votekick themselves
|
||||
if (initiator == targetSession)
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for themselves? Votekick cancelled.");
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancels the vote if there's not enough voters; only the person initiating the vote gets a return message.
|
||||
if (eligibleVoterNumber < eligibleVoterNumberRequirement)
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but there were not enough ghost roles! {eligibleVoterNumberRequirement} required, {eligibleVoterNumber} found.");
|
||||
if (initiator != null)
|
||||
{
|
||||
var message = Loc.GetString("ui-vote-votekick-not-enough-eligible", ("voters", eligibleVoterNumber.ToString()), ("requirement", eligibleVoterNumberRequirement.ToString()));
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, initiator.Channel);
|
||||
}
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for stuff like the target being an admin. These targets shouldn't show up in the UI, but it's necessary to doublecheck in case someone writes the command in console.
|
||||
if (_votingSystem != null && !_votingSystem.CheckVotekickTargetEligibility(targetSession))
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick attempted by {initiator} for player {targetSession}, but they are not eligible to be votekicked!");
|
||||
DirtyCanCallVoteAll();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the vote object
|
||||
|
||||
string voteTitle = "";
|
||||
NetEntity? targetNetEntity = _entityManager.GetNetEntity(targetSession.AttachedEntity);
|
||||
var initiatorName = initiator != null ? initiator.Name : Loc.GetString("ui-vote-votekick-unknown-initiator");
|
||||
|
||||
voteTitle = Loc.GetString("ui-vote-votekick-title", ("initiator", initiatorName), ("targetEntity", targetEntityName), ("reason", reason));
|
||||
|
||||
var options = new VoteOptions
|
||||
{
|
||||
Title = voteTitle,
|
||||
Options =
|
||||
{
|
||||
(Loc.GetString("ui-vote-votekick-yes"), "yes"),
|
||||
(Loc.GetString("ui-vote-votekick-no"), "no"),
|
||||
(Loc.GetString("ui-vote-votekick-abstain"), "abstain")
|
||||
},
|
||||
Duration = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VotekickTimer)),
|
||||
InitiatorTimeout = TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.VotekickTimeout)),
|
||||
VoterEligibility = _cfg.GetCVar(CCVars.VotekickVoterGhostRequirement) ? VoterEligibility.GhostMinimumPlaytime : VoterEligibility.MinimumPlaytime,
|
||||
DisplayVotes = false,
|
||||
TargetEntity = targetNetEntity
|
||||
};
|
||||
|
||||
WirePresetVoteInitiator(options, initiator);
|
||||
|
||||
var vote = CreateVote(options);
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} ({targetEntityName}) due to {reason} started, initiated by {initiator}.");
|
||||
|
||||
// Time out the vote now that we know it will happen
|
||||
TimeoutStandardVote(StandardVoteType.Votekick);
|
||||
|
||||
vote.OnFinished += (_, _) =>
|
||||
{
|
||||
|
||||
var votesYes = vote.VotesPerOption["yes"];
|
||||
var votesNo = vote.VotesPerOption["no"];
|
||||
var total = votesYes + votesNo;
|
||||
|
||||
// Get the voters, for logging purposes.
|
||||
List<ICommonSession> yesVoters = new();
|
||||
List<ICommonSession> noVoters = new();
|
||||
foreach (var (voter, castVote) in vote.CastVotes)
|
||||
{
|
||||
if (castVote == 0)
|
||||
{
|
||||
yesVoters.Add(voter);
|
||||
}
|
||||
if (castVote == 1)
|
||||
{
|
||||
noVoters.Add(voter);
|
||||
}
|
||||
}
|
||||
var yesVotersString = string.Join(", ", yesVoters);
|
||||
var noVotersString = string.Join(", ", noVoters);
|
||||
|
||||
var ratioRequired = _cfg.GetCVar(CCVars.VotekickRequiredRatio);
|
||||
if (total > 0 && votesYes / (float)total >= ratioRequired)
|
||||
{
|
||||
// Some conditions that cancel the vote want to let the vote run its course first and then cancel it
|
||||
// so we check for that here
|
||||
|
||||
// Check if an admin is online, and ignore the vote if the cvar is enabled
|
||||
if (_cfg.GetCVar(CCVars.VotekickNotAllowedWhenAdminOnline) && _adminMgr.ActiveAdmins.Count() != 0)
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} attempted to pass, but an admin was online. Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
|
||||
AnnounceCancelledVotekickForVoters(targetEntityName);
|
||||
return;
|
||||
}
|
||||
// Check if the target is an antag and the vote reason is raiding (this is to prevent false positives)
|
||||
else if (isAntagSafe && reason == VotekickReasonType.Raiding.ToString())
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being an antagonist.");
|
||||
AnnounceCancelledVotekickForVoters(targetEntityName);
|
||||
return;
|
||||
}
|
||||
// Check if the target is an admin/de-admined admin
|
||||
else if (targetSession.AttachedEntity != null && _adminMgr.IsAdmin(targetSession.AttachedEntity.Value, true))
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} due to {reason} finished, created by {initiator}, but was cancelled due to the target being a de-admined admin.");
|
||||
AnnounceCancelledVotekickForVoters(targetEntityName);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick for {located.Username} succeeded: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-success", ("target", targetEntityName), ("reason", reason)));
|
||||
|
||||
if (!Enum.TryParse(_cfg.GetCVar(CCVars.VotekickBanDefaultSeverity), out NoteSeverity severity))
|
||||
{
|
||||
_logManager.GetSawmill("admin.votekick")
|
||||
.Warning("Votekick ban severity could not be parsed from config! Defaulting to high.");
|
||||
severity = NoteSeverity.High;
|
||||
}
|
||||
|
||||
uint minutes = (uint)_cfg.GetCVar(CCVars.VotekickBanDuration);
|
||||
|
||||
_bans.CreateServerBan(targetUid, target, null, null, targetHWid, minutes, severity, reason);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"Votekick failed: Yes: {votesYes} / No: {votesNo}. Yes: {yesVotersString} / No: {noVotersString}");
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-votekick-failure", ("target", targetEntityName), ("reason", reason)));
|
||||
}
|
||||
};
|
||||
|
||||
if (initiator != null)
|
||||
{
|
||||
// Cast yes vote if created the vote yourself.
|
||||
vote.CastVote(initiator, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnnounceCancelledVotekickForVoters(string target)
|
||||
{
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (CheckVoterEligibility(player, VoterEligibility.GhostMinimumPlaytime))
|
||||
{
|
||||
var message = Loc.GetString("ui-vote-votekick-server-cancelled", ("target", target));
|
||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TimeoutStandardVote(StandardVoteType type)
|
||||
{
|
||||
var timeout = TimeSpan.FromSeconds(_cfg.GetCVar(CCVars.VoteSameTypeTimeout));
|
||||
|
||||
@@ -11,6 +11,8 @@ using Content.Server.Maps;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -38,6 +40,7 @@ namespace Content.Server.Voting.Managers
|
||||
[Dependency] private readonly IGameMapManager _gameMapManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ISharedPlaytimeManager _playtimeManager = default!;
|
||||
|
||||
private int _nextVoteId = 1;
|
||||
|
||||
@@ -209,7 +212,7 @@ namespace Content.Server.Voting.Managers
|
||||
var start = _timing.RealTime;
|
||||
var end = start + options.Duration;
|
||||
var reg = new VoteReg(id, entries, options.Title, options.InitiatorText,
|
||||
options.InitiatorPlayer, start, end);
|
||||
options.InitiatorPlayer, start, end, options.VoterEligibility, options.DisplayVotes, options.TargetEntity);
|
||||
|
||||
var handle = new VoteHandle(this, reg);
|
||||
|
||||
@@ -245,12 +248,24 @@ namespace Content.Server.Voting.Managers
|
||||
msg.VoteId = v.Id;
|
||||
msg.VoteActive = !v.Finished;
|
||||
|
||||
if (!CheckVoterEligibility(player, v.VoterEligibility))
|
||||
{
|
||||
msg.VoteActive = false;
|
||||
player.Channel.SendMessage(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!v.Finished)
|
||||
{
|
||||
msg.VoteTitle = v.Title;
|
||||
msg.VoteInitiator = v.InitiatorText;
|
||||
msg.StartTime = v.StartTime;
|
||||
msg.EndTime = v.EndTime;
|
||||
|
||||
if (v.TargetEntity != null)
|
||||
{
|
||||
msg.TargetEntity = v.TargetEntity.Value.Id;
|
||||
}
|
||||
}
|
||||
|
||||
if (v.CastVotes.TryGetValue(player, out var cast))
|
||||
@@ -266,11 +281,17 @@ namespace Content.Server.Voting.Managers
|
||||
}
|
||||
}
|
||||
|
||||
// Admin always see the vote count, even if the vote is set to hide it.
|
||||
if (_adminMgr.HasAdminFlag(player, AdminFlags.Moderator))
|
||||
{
|
||||
msg.DisplayVotes = true;
|
||||
}
|
||||
|
||||
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);
|
||||
msg.Options[i] = (msg.DisplayVotes ? (ushort) entry.Votes : (ushort) 0, entry.Text);
|
||||
}
|
||||
|
||||
player.Channel.SendMessage(msg);
|
||||
@@ -362,6 +383,16 @@ namespace Content.Server.Voting.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove ineligible votes that somehow slipped through
|
||||
foreach (var playerVote in v.CastVotes)
|
||||
{
|
||||
if (!CheckVoterEligibility(playerVote.Key, v.VoterEligibility))
|
||||
{
|
||||
v.Entries[playerVote.Value].Votes -= 1;
|
||||
v.CastVotes.Remove(playerVote.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Find winner or stalemate.
|
||||
var winners = v.Entries
|
||||
.GroupBy(e => e.Votes)
|
||||
@@ -395,6 +426,37 @@ namespace Content.Server.Voting.Managers
|
||||
DirtyCanCallVoteAll();
|
||||
}
|
||||
|
||||
public bool CheckVoterEligibility(ICommonSession player, VoterEligibility eligibility)
|
||||
{
|
||||
if (eligibility == VoterEligibility.All)
|
||||
return true;
|
||||
|
||||
if (eligibility == VoterEligibility.Ghost || eligibility == VoterEligibility.GhostMinimumPlaytime)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent(player.AttachedEntity, out GhostComponent? ghostComp))
|
||||
return false;
|
||||
|
||||
if (eligibility == VoterEligibility.GhostMinimumPlaytime)
|
||||
{
|
||||
var playtime = _playtimeManager.GetPlayTimes(player);
|
||||
if (!playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) || overallTime < TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickEligibleVoterPlaytime)))
|
||||
return false;
|
||||
|
||||
if ((int)_timing.RealTime.Subtract(ghostComp.TimeOfDeath).TotalSeconds < _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibility == VoterEligibility.MinimumPlaytime)
|
||||
{
|
||||
var playtime = _playtimeManager.GetPlayTimes(player);
|
||||
if (!playtime.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out TimeSpan overallTime) || overallTime < TimeSpan.FromHours(_cfg.GetCVar(CCVars.VotekickEligibleVoterPlaytime)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<IVoteHandle> ActiveVotes => _voteHandles.Values;
|
||||
|
||||
public bool TryGetVote(int voteId, [NotNullWhen(true)] out IVoteHandle? vote)
|
||||
@@ -442,6 +504,9 @@ namespace Content.Server.Voting.Managers
|
||||
public readonly TimeSpan StartTime;
|
||||
public readonly TimeSpan EndTime;
|
||||
public readonly HashSet<ICommonSession> VotesDirty = new();
|
||||
public readonly VoterEligibility VoterEligibility;
|
||||
public readonly bool DisplayVotes;
|
||||
public readonly NetEntity? TargetEntity;
|
||||
|
||||
public bool Cancelled;
|
||||
public bool Finished;
|
||||
@@ -452,7 +517,7 @@ namespace Content.Server.Voting.Managers
|
||||
public ICommonSession? Initiator { get; }
|
||||
|
||||
public VoteReg(int id, VoteEntry[] entries, string title, string initiatorText,
|
||||
ICommonSession? initiator, TimeSpan start, TimeSpan end)
|
||||
ICommonSession? initiator, TimeSpan start, TimeSpan end, VoterEligibility voterEligibility, bool displayVotes, NetEntity? targetEntity)
|
||||
{
|
||||
Id = id;
|
||||
Entries = entries;
|
||||
@@ -461,6 +526,9 @@ namespace Content.Server.Voting.Managers
|
||||
Initiator = initiator;
|
||||
StartTime = start;
|
||||
EndTime = end;
|
||||
VoterEligibility = voterEligibility;
|
||||
DisplayVotes = displayVotes;
|
||||
TargetEntity = targetEntity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,6 +546,14 @@ namespace Content.Server.Voting.Managers
|
||||
}
|
||||
}
|
||||
|
||||
public enum VoterEligibility
|
||||
{
|
||||
All,
|
||||
Ghost, // Player needs to be a ghost
|
||||
GhostMinimumPlaytime, // Player needs to be a ghost, with a minimum playtime and deathtime as defined by votekick CCvars.
|
||||
MinimumPlaytime //Player needs to have a minimum playtime and deathtime as defined by votekick CCvars.
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IVoteHandle API surface
|
||||
@@ -492,6 +568,7 @@ namespace Content.Server.Voting.Managers
|
||||
public string InitiatorText => _reg.InitiatorText;
|
||||
public bool Finished => _reg.Finished;
|
||||
public bool Cancelled => _reg.Cancelled;
|
||||
public IReadOnlyDictionary<ICommonSession, int> CastVotes => _reg.CastVotes;
|
||||
|
||||
public IReadOnlyDictionary<object, int> VotesPerOption { get; }
|
||||
|
||||
|
||||
@@ -29,11 +29,17 @@ namespace Content.Server.Voting
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
if (args.Length != 1 && args[0] != StandardVoteType.Votekick.ToString())
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
return;
|
||||
}
|
||||
if (args.Length != 3 && args[0] == StandardVoteType.Votekick.ToString())
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number-need-specific", ("properAmount", 3), ("currentAmount", args.Length)));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!Enum.TryParse<StandardVoteType>(args[0], ignoreCase: true, out var type))
|
||||
{
|
||||
@@ -50,7 +56,7 @@ namespace Content.Server.Voting
|
||||
return;
|
||||
}
|
||||
|
||||
mgr.CreateStandardVote(shell.Player, type);
|
||||
mgr.CreateStandardVote(shell.Player, type, args.Skip(1).ToArray());
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Voting.Managers;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
|
||||
namespace Content.Server.Voting
|
||||
{
|
||||
/// <summary>
|
||||
@@ -39,6 +39,21 @@ namespace Content.Server.Voting
|
||||
/// </summary>
|
||||
public List<(string text, object data)> Options { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Which sessions may send a vote. Used when only a subset of players should be able to vote. Defaults to all.
|
||||
/// </summary>
|
||||
public VoteManager.VoterEligibility VoterEligibility = VoteManager.VoterEligibility.All;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vote should send and display the number of votes to the clients. Being an admin defaults this option to true for your client.
|
||||
/// </summary>
|
||||
public bool DisplayVotes = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the vote should have an entity attached to it, to be used for things like letting ghosts follow it.
|
||||
/// </summary>
|
||||
public NetEntity? TargetEntity = null;
|
||||
|
||||
/// <summary>
|
||||
/// Sets <see cref="InitiatorPlayer"/> and <see cref="InitiatorText"/>
|
||||
/// by setting the latter to the player's name.
|
||||
|
||||
122
Content.Server/Voting/VotingSystem.cs
Normal file
122
Content.Server/Voting/VotingSystem.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Voting;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server.Voting;
|
||||
|
||||
public sealed class VotingSystem : EntitySystem
|
||||
{
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IServerDbManager _dbManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<VotePlayerListRequestEvent>(OnVotePlayerListRequestEvent);
|
||||
}
|
||||
|
||||
private async void OnVotePlayerListRequestEvent(VotePlayerListRequestEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (args.SenderSession.AttachedEntity is not { Valid: true } entity
|
||||
|| !await CheckVotekickInitEligibility(args.SenderSession))
|
||||
{
|
||||
var deniedResponse = new VotePlayerListResponseEvent(new (NetUserId, NetEntity, string)[0], true);
|
||||
RaiseNetworkEvent(deniedResponse, args.SenderSession.Channel);
|
||||
return;
|
||||
}
|
||||
|
||||
List<(NetUserId, NetEntity, string)> players = new();
|
||||
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
if (player.AttachedEntity is not { Valid: true } attached)
|
||||
continue;
|
||||
|
||||
if (attached == entity) continue;
|
||||
|
||||
if (_adminManager.IsAdmin(player, false)) continue;
|
||||
|
||||
var playerName = GetPlayerVoteListName(attached);
|
||||
var netEntity = GetNetEntity(attached);
|
||||
|
||||
players.Add((player.UserId, netEntity, playerName));
|
||||
}
|
||||
|
||||
var response = new VotePlayerListResponseEvent(players.ToArray(), false);
|
||||
RaiseNetworkEvent(response, args.SenderSession.Channel);
|
||||
}
|
||||
|
||||
public string GetPlayerVoteListName(EntityUid attached)
|
||||
{
|
||||
TryComp<MindContainerComponent>(attached, out var mind);
|
||||
|
||||
var jobName = _jobs.MindTryGetJobName(mind?.Mind);
|
||||
var playerInfo = $"{Comp<MetaDataComponent>(attached).EntityName} ({jobName})";
|
||||
|
||||
return playerInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether the player initiating a votekick is allowed to do so serverside.
|
||||
/// </summary>
|
||||
/// <param name="initiator">The session initiating the votekick.</param>
|
||||
public async Task<bool> CheckVotekickInitEligibility(ICommonSession? initiator)
|
||||
{
|
||||
if (initiator == null)
|
||||
return false;
|
||||
|
||||
// Being an admin overrides the votekick eligibility
|
||||
if (initiator.AttachedEntity != null && _adminManager.IsAdmin(initiator.AttachedEntity.Value, false))
|
||||
return true;
|
||||
|
||||
if (_cfg.GetCVar(CCVars.VotekickInitiatorGhostRequirement))
|
||||
{
|
||||
// Must be ghost
|
||||
if (!TryComp(initiator.AttachedEntity, out GhostComponent? ghostComp))
|
||||
return false;
|
||||
|
||||
// Must have been dead for x seconds
|
||||
if ((int)_gameTiming.RealTime.Subtract(ghostComp.TimeOfDeath).TotalSeconds < _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be whitelisted
|
||||
if (!await _dbManager.GetWhitelistStatusAsync(initiator.UserId))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether the player being targetted for a votekick is a valid target.
|
||||
/// </summary>
|
||||
/// <param name="target">The session being targetted for a votekick.</param>
|
||||
public bool CheckVotekickTargetEligibility(ICommonSession? target)
|
||||
{
|
||||
if (target == null)
|
||||
return false;
|
||||
|
||||
// Admins can't be votekicked
|
||||
if (target.AttachedEntity != null && _adminManager.IsAdmin(target.AttachedEntity.Value))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1404,7 +1404,6 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<float> VoteSameTypeTimeout =
|
||||
CVarDef.Create("vote.same_type_timeout", 240f, CVar.SERVERONLY);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets the duration of the map vote timer.
|
||||
/// </summary>
|
||||
@@ -1429,6 +1428,87 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<int>
|
||||
VoteTimerAlone = CVarDef.Create("vote.timeralone", 10, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* VOTEKICK
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Allows enabling/disabling player-started votekick for ultimate authority
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickEnabled =
|
||||
CVarDef.Create("votekick.enabled", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Config for when the votekick should be allowed to be called based on number of eligible voters.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickEligibleNumberRequirement =
|
||||
CVarDef.Create("votekick.eligible_number", 10, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a votekick initiator must be a ghost or not.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickInitiatorGhostRequirement =
|
||||
CVarDef.Create("votekick.initiator_ghost_requirement", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a votekick voter must be a ghost or not.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickVoterGhostRequirement =
|
||||
CVarDef.Create("votekick.voter_ghost_requirement", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Config for how many hours playtime a player must have to be able to vote on a votekick.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickEligibleVoterPlaytime =
|
||||
CVarDef.Create("votekick.voter_playtime", 100, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Config for how many seconds a player must have been dead to initiate a votekick / be able to vote on a votekick.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickEligibleVoterDeathtime =
|
||||
CVarDef.Create("votekick.voter_deathtime", 180, CVar.REPLICATED | CVar.SERVER);
|
||||
|
||||
/// <summary>
|
||||
/// The required ratio of eligible voters that must agree for a votekick to go through.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> VotekickRequiredRatio =
|
||||
CVarDef.Create("votekick.required_ratio", 0.6f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to prevent the votekick from having any effect when there is an online admin.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> VotekickNotAllowedWhenAdminOnline =
|
||||
CVarDef.Create("votekick.not_allowed_when_admin_online", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The delay for which two votekicks are allowed to be made by separate people, in seconds.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> VotekickTimeout =
|
||||
CVarDef.Create("votekick.timeout", 120f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the duration of the votekick vote timer.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int>
|
||||
VotekickTimer = CVarDef.Create("votekick.timer", 60, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Config for how many hours playtime a player must have to get protection from the Raider votekick type when playing as an antag.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickAntagRaiderProtection =
|
||||
CVarDef.Create("votekick.antag_raider_protection", 10, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Default severity for votekick bans
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> VotekickBanDefaultSeverity =
|
||||
CVarDef.Create("votekick.ban_default_severity", "High", CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Duration of a ban caused by a votekick (in minutes).
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> VotekickBanDuration =
|
||||
CVarDef.Create("votekick.ban_duration", 180, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* BAN
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lidgren.Network;
|
||||
using Lidgren.Network;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace Content.Shared.Voting
|
||||
public (ushort votes, string name)[] Options = default!;
|
||||
public bool IsYourVoteDirty;
|
||||
public byte? YourVote;
|
||||
public bool DisplayVotes;
|
||||
public int TargetEntity;
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
|
||||
{
|
||||
@@ -31,6 +33,8 @@ namespace Content.Shared.Voting
|
||||
VoteInitiator = buffer.ReadString();
|
||||
StartTime = TimeSpan.FromTicks(buffer.ReadInt64());
|
||||
EndTime = TimeSpan.FromTicks(buffer.ReadInt64());
|
||||
DisplayVotes = buffer.ReadBoolean();
|
||||
TargetEntity = buffer.ReadVariableInt32();
|
||||
|
||||
Options = new (ushort votes, string name)[buffer.ReadByte()];
|
||||
for (var i = 0; i < Options.Length; i++)
|
||||
@@ -58,6 +62,8 @@ namespace Content.Shared.Voting
|
||||
buffer.Write(VoteInitiator);
|
||||
buffer.Write(StartTime.Ticks);
|
||||
buffer.Write(EndTime.Ticks);
|
||||
buffer.Write(DisplayVotes);
|
||||
buffer.WriteVariableInt32(TargetEntity);
|
||||
|
||||
buffer.Write((byte) Options.Length);
|
||||
foreach (var (votes, name) in Options)
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
namespace Content.Shared.Voting
|
||||
namespace Content.Shared.Voting;
|
||||
|
||||
/// <summary>
|
||||
/// Standard vote types that players can initiate themselves from the escape menu.
|
||||
/// </summary>
|
||||
public enum StandardVoteType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard vote types that players can initiate themselves from the escape menu.
|
||||
/// Vote to restart the round.
|
||||
/// </summary>
|
||||
public enum StandardVoteType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Vote to restart the round.
|
||||
/// </summary>
|
||||
Restart,
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Vote to change the game preset for next round.
|
||||
/// </summary>
|
||||
Preset,
|
||||
/// <summary>
|
||||
/// Vote to change the game preset for next round.
|
||||
/// </summary>
|
||||
Preset,
|
||||
|
||||
/// <summary>
|
||||
/// Vote to change the map for the next round.
|
||||
/// </summary>
|
||||
Map
|
||||
}
|
||||
/// <summary>
|
||||
/// Vote to change the map for the next round.
|
||||
/// </summary>
|
||||
Map,
|
||||
|
||||
/// <summary>
|
||||
/// Vote to kick a player.
|
||||
/// </summary>
|
||||
Votekick
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reasons available to initiate a votekick.
|
||||
/// </summary>
|
||||
public enum VotekickReasonType : byte
|
||||
{
|
||||
Raiding,
|
||||
Cheating,
|
||||
Spam
|
||||
}
|
||||
|
||||
30
Content.Shared/Voting/VotingEvents.cs
Normal file
30
Content.Shared/Voting/VotingEvents.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Voting;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class VotePlayerListRequestEvent : EntityEventArgs
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class VotePlayerListResponseEvent : EntityEventArgs
|
||||
{
|
||||
public VotePlayerListResponseEvent((NetUserId, NetEntity, string)[] players, bool denied)
|
||||
{
|
||||
Players = players;
|
||||
Denied = denied;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The players available to have a votekick started for them.
|
||||
/// </summary>
|
||||
public (NetUserId, NetEntity, string)[] Players { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the server will allow the user to start a votekick or not.
|
||||
/// </summary>
|
||||
public bool Denied;
|
||||
}
|
||||
@@ -20,3 +20,16 @@ ui-vote-map-tie = Tie for map vote! Picking... { $picked }
|
||||
ui-vote-map-win = { $winner } won the map vote!
|
||||
ui-vote-map-notlobby = Voting for maps is only valid in the pre-round lobby!
|
||||
ui-vote-map-notlobby-time = Voting for maps is only valid in the pre-round lobby with { $time } remaining!
|
||||
|
||||
|
||||
# Votekick votes
|
||||
ui-vote-votekick-unknown-initiator = A player
|
||||
ui-vote-votekick-unknown-target = Unknown Player
|
||||
ui-vote-votekick-title = { $initiator } has called a votekick for user: { $targetEntity }. Reason: { $reason }
|
||||
ui-vote-votekick-yes = Yes
|
||||
ui-vote-votekick-no = No
|
||||
ui-vote-votekick-abstain = Abstain
|
||||
ui-vote-votekick-success = Votekick for { $target } succeeded. Votekick reason: { $reason }
|
||||
ui-vote-votekick-failure = Votekick for { $target } failed. Votekick reason: { $reason }
|
||||
ui-vote-votekick-not-enough-eligible = Not enough eligible voters online to start a votekick: { $voters }/{ $requirement }
|
||||
ui-vote-votekick-server-cancelled = Votekick for { $target } was cancelled by the server.
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
ui-vote-type-restart = Restart round
|
||||
ui-vote-type-gamemode = Next gamemode
|
||||
ui-vote-type-map = Next map
|
||||
ui-vote-type-votekick = Votekick
|
||||
|
||||
# Votekick reasons
|
||||
ui-vote-votekick-type-raiding = Raiding
|
||||
ui-vote-votekick-type-cheating = Cheating
|
||||
ui-vote-votekick-type-spamming = Spamming
|
||||
|
||||
# Window title of the vote create menu
|
||||
ui-vote-create-title = Call Vote
|
||||
@@ -8,12 +14,25 @@ ui-vote-create-title = Call Vote
|
||||
# Submit button in the vote create button
|
||||
ui-vote-create-button = Call Vote
|
||||
|
||||
# Follow button in the vote create menu
|
||||
ui-vote-follow-button = Follow User
|
||||
|
||||
# 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
|
||||
|
||||
# Vote option only available for specific users.
|
||||
ui-vote-trusted-users-notice =
|
||||
This vote option is only available to whitelisted players.
|
||||
In addition, you must have been a ghost for { $timeReq } minutes.
|
||||
|
||||
# Warning to not abuse a specific vote option.
|
||||
ui-vote-abuse-warning =
|
||||
Warning!
|
||||
Abuse of the votekick system may result in an indefinite ban!
|
||||
|
||||
# Hue hue hue
|
||||
ui-vote-fluff = Powered by Robust™ Anti-Tamper Technology
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
ui-vote-created = { $initiator } has called a vote:
|
||||
ui-vote-button = { $text } ({ $votes })
|
||||
ui-vote-button = { $text } ({ $votes })
|
||||
ui-vote-button-no-votes = { $text }
|
||||
ui-vote-follow-button-popup = Follow User
|
||||
|
||||
Reference in New Issue
Block a user