@@ -86,8 +86,9 @@ namespace OpenRA
|
|||||||
|
|
||||||
static void JoinInner(OrderManager om)
|
static void JoinInner(OrderManager om)
|
||||||
{
|
{
|
||||||
// Refresh TextNotificationsManager before the game starts.
|
// Refresh static classes before the game starts.
|
||||||
TextNotificationsManager.Clear();
|
TextNotificationsManager.Clear();
|
||||||
|
UnitOrders.Clear();
|
||||||
|
|
||||||
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
|
// HACK: The shellmap World and OrderManager are owned by the main menu's WorldRenderer instead of Game.
|
||||||
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
|
// This allows us to switch Game.OrderManager from the shellmap to the new network connection when joining
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ namespace OpenRA.Network
|
|||||||
{
|
{
|
||||||
public const int ChatMessageMaxLength = 2500;
|
public const int ChatMessageMaxLength = 2500;
|
||||||
|
|
||||||
|
public static int? KickVoteTarget { get; internal set; }
|
||||||
|
|
||||||
static Player FindPlayerByClient(this World world, Session.Client c)
|
static Player FindPlayerByClient(this World world, Session.Client c)
|
||||||
{
|
{
|
||||||
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
|
return world.Players.FirstOrDefault(p => p.ClientIndex == c.Index && p.PlayerReference.Playable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool OrderNotFromServerOrWorldIsReplay(int clientId, World world) => clientId != 0 || (world != null && world.IsReplay);
|
||||||
|
|
||||||
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
|
internal static void ProcessOrder(OrderManager orderManager, World world, int clientId, Order order)
|
||||||
{
|
{
|
||||||
switch (order.OrderString)
|
switch (order.OrderString)
|
||||||
@@ -52,9 +56,7 @@ namespace OpenRA.Network
|
|||||||
|
|
||||||
case "DisableChatEntry":
|
case "DisableChatEntry":
|
||||||
{
|
{
|
||||||
// Order must originate from the server
|
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||||
// Don't disable chat in replays
|
|
||||||
if (clientId != 0 || (world != null && world.IsReplay))
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Server may send MaxValue to indicate that it is disabled until further notice
|
// Server may send MaxValue to indicate that it is disabled until further notice
|
||||||
@@ -66,6 +68,26 @@ namespace OpenRA.Network
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "StartKickVote":
|
||||||
|
{
|
||||||
|
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||||
|
break;
|
||||||
|
|
||||||
|
KickVoteTarget = (int)order.ExtraData;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "EndKickVote":
|
||||||
|
{
|
||||||
|
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (KickVoteTarget == (int)order.ExtraData)
|
||||||
|
KickVoteTarget = null;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case "Chat":
|
case "Chat":
|
||||||
{
|
{
|
||||||
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
|
||||||
@@ -365,5 +387,10 @@ namespace OpenRA.Network
|
|||||||
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
|
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
|
||||||
order.Subject.ResolveOrder(order);
|
order.Subject.ResolveOrder(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
KickVoteTarget = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ namespace OpenRA.Server
|
|||||||
GameInformation gameInfo;
|
GameInformation gameInfo;
|
||||||
readonly List<GameInformation.Player> worldPlayers = new();
|
readonly List<GameInformation.Player> worldPlayers = new();
|
||||||
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
readonly Stopwatch pingUpdated = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
public readonly VoteKickTracker VoteKickTracker;
|
||||||
readonly PlayerMessageTracker playerMessageTracker;
|
readonly PlayerMessageTracker playerMessageTracker;
|
||||||
|
|
||||||
public ServerState State
|
public ServerState State
|
||||||
@@ -318,6 +320,7 @@ namespace OpenRA.Server
|
|||||||
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);
|
||||||
|
|
||||||
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
|
playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
|
||||||
|
VoteKickTracker = new VoteKickTracker(this);
|
||||||
|
|
||||||
LobbyInfo = new Session
|
LobbyInfo = new Session
|
||||||
{
|
{
|
||||||
@@ -1163,15 +1166,8 @@ namespace OpenRA.Server
|
|||||||
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
|
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Does not check if client is admin.</summary>
|
public bool HasClientWonOrLost(Session.Client client) =>
|
||||||
public bool CanKickClient(Session.Client kickee)
|
worldPlayers.FirstOrDefault(p => p?.ClientIndex == client.Index)?.Outcome != WinState.Undefined;
|
||||||
{
|
|
||||||
if (State != ServerState.GameStarted || kickee.IsObserver)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
var player = worldPlayers.FirstOrDefault(p => p?.ClientIndex == kickee.Index);
|
|
||||||
return player != null && player.Outcome != WinState.Undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DropClient(Connection toDrop)
|
public void DropClient(Connection toDrop)
|
||||||
{
|
{
|
||||||
|
|||||||
223
OpenRA.Game/Server/VoteKickTracker.cs
Normal file
223
OpenRA.Game/Server/VoteKickTracker.cs
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
#region Copyright & License Information
|
||||||
|
/*
|
||||||
|
* Copyright (c) The OpenRA Developers and Contributors
|
||||||
|
* This file is part of OpenRA, which is free software. It is made
|
||||||
|
* available to you under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation, either version 3 of
|
||||||
|
* the License, or (at your option) any later version. For more
|
||||||
|
* information, see COPYING.
|
||||||
|
*/
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using OpenRA.Network;
|
||||||
|
|
||||||
|
namespace OpenRA.Server
|
||||||
|
{
|
||||||
|
public sealed class VoteKickTracker
|
||||||
|
{
|
||||||
|
[TranslationReference("kickee")]
|
||||||
|
const string InsufficientVotes = "notification-insufficient-votes-to-kick";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string AlreadyVoted = "notification-kick-already-voted";
|
||||||
|
|
||||||
|
[TranslationReference("kicker", "kickee")]
|
||||||
|
const string VoteKickStarted = "notification-vote-kick-started";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string UnableToStartAVote = "notification-unable-to-start-a-vote";
|
||||||
|
|
||||||
|
[TranslationReference("kickee", "percentage")]
|
||||||
|
const string VoteKickProgress = "notification-vote-kick-in-progress";
|
||||||
|
|
||||||
|
[TranslationReference("kickee")]
|
||||||
|
const string VoteKickEnded = "notification-vote-kick-ended";
|
||||||
|
|
||||||
|
readonly Dictionary<int, bool> voteTracker = new();
|
||||||
|
readonly Dictionary<Session.Client, long> failedVoteKickers = new();
|
||||||
|
readonly Server server;
|
||||||
|
|
||||||
|
Stopwatch voteKickTimer;
|
||||||
|
(Session.Client Client, Connection Conn) kickee;
|
||||||
|
(Session.Client Client, Connection Conn) voteKickerStarter;
|
||||||
|
|
||||||
|
public VoteKickTracker(Server server)
|
||||||
|
{
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admins and alive players can participate in a vote kick.
|
||||||
|
bool ClientHasPower(Session.Client client) => client.IsAdmin || (!client.IsObserver && !server.HasClientWonOrLost(client));
|
||||||
|
|
||||||
|
public void Tick()
|
||||||
|
{
|
||||||
|
if (voteKickTimer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!server.Conns.Contains(kickee.Conn))
|
||||||
|
{
|
||||||
|
EndKickVote();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (voteKickTimer.ElapsedMilliseconds > server.Settings.VoteKickTimer)
|
||||||
|
EndKickVoteAndBlockKicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VoteKick(Connection conn, Session.Client kicker, Connection kickeeConn, Session.Client kickee, int kickeeID, bool vote)
|
||||||
|
{
|
||||||
|
var voteInProgress = voteKickTimer != null;
|
||||||
|
|
||||||
|
if (server.State != ServerState.GameStarted
|
||||||
|
|| (kickee.IsAdmin && server.Type != ServerType.Dedicated)
|
||||||
|
|| (!voteInProgress && !vote) // Disallow starting a vote with a downvote
|
||||||
|
|| (voteInProgress && this.kickee.Client != kickee) // Disallow starting new votes when one is already ongoing.
|
||||||
|
|| !ClientHasPower(kicker))
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
short eligiblePlayers = 0;
|
||||||
|
var isKickeeOnline = false;
|
||||||
|
var adminIsDeadButOnline = false;
|
||||||
|
foreach (var c in server.Conns)
|
||||||
|
{
|
||||||
|
var client = server.GetClient(c);
|
||||||
|
if (client != kickee && ClientHasPower(client))
|
||||||
|
eligiblePlayers++;
|
||||||
|
|
||||||
|
if (c == kickeeConn)
|
||||||
|
isKickeeOnline = true;
|
||||||
|
|
||||||
|
if (client.IsAdmin && (client.IsObserver || server.HasClientWonOrLost(client)))
|
||||||
|
adminIsDeadButOnline = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isKickeeOnline)
|
||||||
|
{
|
||||||
|
EndKickVote();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eligiblePlayers < 2 || (adminIsDeadButOnline && !kickee.IsAdmin && eligiblePlayers < 3))
|
||||||
|
{
|
||||||
|
if (!kickee.IsObserver && !server.HasClientWonOrLost(kickee))
|
||||||
|
{
|
||||||
|
// Vote kick cannot be the sole deciding factor for a game.
|
||||||
|
server.SendLocalizedMessageTo(conn, InsufficientVotes, Translation.Arguments("kickee", kickee.Name));
|
||||||
|
EndKickVote();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (vote)
|
||||||
|
{
|
||||||
|
// If only a single player is playing, allow him to kick observers.
|
||||||
|
EndKickVote(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!voteInProgress)
|
||||||
|
{
|
||||||
|
// Prevent vote kick spam abuse.
|
||||||
|
if (failedVoteKickers.TryGetValue(kicker, out var time))
|
||||||
|
{
|
||||||
|
if (time + server.Settings.VoteKickerCooldown > kickeeConn.ConnectionTimer.ElapsedMilliseconds)
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, UnableToStartAVote);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
failedVoteKickers.Remove(kicker);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Write("server", $"Vote kick started on {kickeeID}.");
|
||||||
|
voteKickTimer = Stopwatch.StartNew();
|
||||||
|
server.SendLocalizedMessage(VoteKickStarted, Translation.Arguments("kicker", kicker.Name, "kickee", kickee.Name));
|
||||||
|
server.DispatchServerOrdersToClients(new Order("StartKickVote", null, false) { ExtraData = (uint)kickeeID }.Serialize());
|
||||||
|
this.kickee = (kickee, kickeeConn);
|
||||||
|
voteKickerStarter = (kicker, conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!voteTracker.ContainsKey(conn.PlayerIndex))
|
||||||
|
voteTracker[conn.PlayerIndex] = vote;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, AlreadyVoted, null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
short votesFor = 0;
|
||||||
|
short votesAgainst = 0;
|
||||||
|
foreach (var c in voteTracker)
|
||||||
|
{
|
||||||
|
if (c.Value)
|
||||||
|
votesFor++;
|
||||||
|
else
|
||||||
|
votesAgainst++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the kickee in eligeablePlayers, so that in a 2v2 or any other even team
|
||||||
|
// matchup one team could not vote out the other team's player.
|
||||||
|
if (ClientHasPower(kickee))
|
||||||
|
{
|
||||||
|
eligiblePlayers++;
|
||||||
|
votesAgainst++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var votesNeeded = eligiblePlayers / 2 + 1;
|
||||||
|
server.SendLocalizedMessage(VoteKickProgress, Translation.Arguments(
|
||||||
|
"kickee", kickee.Name,
|
||||||
|
"percentage", votesFor * 100 / eligiblePlayers));
|
||||||
|
|
||||||
|
// If a player or players during a vote lose or disconnect, it is possible that a downvote will
|
||||||
|
// kick a client. Guard against that situation.
|
||||||
|
if (vote && (votesFor >= votesNeeded))
|
||||||
|
{
|
||||||
|
EndKickVote(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End vote if it can never succeed.
|
||||||
|
if (eligiblePlayers - votesAgainst < votesNeeded)
|
||||||
|
{
|
||||||
|
EndKickVoteAndBlockKicker();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
voteKickTimer.Restart();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndKickVoteAndBlockKicker()
|
||||||
|
{
|
||||||
|
// Make sure vote kick is in progress.
|
||||||
|
if (voteKickTimer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (server.Conns.Contains(voteKickerStarter.Conn))
|
||||||
|
failedVoteKickers[voteKickerStarter.Client] = voteKickerStarter.Conn.ConnectionTimer.ElapsedMilliseconds;
|
||||||
|
|
||||||
|
EndKickVote();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EndKickVote(bool sendMessage = true)
|
||||||
|
{
|
||||||
|
// Make sure vote kick is in progress.
|
||||||
|
if (voteKickTimer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (sendMessage)
|
||||||
|
server.SendLocalizedMessage(VoteKickEnded, Translation.Arguments("kickee", kickee.Client.Name));
|
||||||
|
|
||||||
|
server.DispatchServerOrdersToClients(new Order("EndKickVote", null, false) { ExtraData = (uint)kickee.Client.Index }.Serialize());
|
||||||
|
|
||||||
|
voteKickTimer = null;
|
||||||
|
voteKickerStarter = (null, null);
|
||||||
|
kickee = (null, null);
|
||||||
|
voteTracker.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,6 +114,15 @@ namespace OpenRA
|
|||||||
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
|
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
|
||||||
public int FloodLimitCooldown = 15000;
|
public int FloodLimitCooldown = 15000;
|
||||||
|
|
||||||
|
[Desc("Can players vote to kick other players?")]
|
||||||
|
public bool EnableVoteKick = true;
|
||||||
|
|
||||||
|
[Desc("After how much time in miliseconds should the vote kick fail after idling?")]
|
||||||
|
public int VoteKickTimer = 30000;
|
||||||
|
|
||||||
|
[Desc("If a vote kick was unsuccessful for how long should the player who started the vote not be able to start new votes?")]
|
||||||
|
public int VoteKickerCooldown = 120000;
|
||||||
|
|
||||||
public ServerSettings Clone()
|
public ServerSettings Clone()
|
||||||
{
|
{
|
||||||
return (ServerSettings)MemberwiseClone();
|
return (ServerSettings)MemberwiseClone();
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ using S = OpenRA.Server.Server;
|
|||||||
|
|
||||||
namespace OpenRA.Mods.Common.Server
|
namespace OpenRA.Mods.Common.Server
|
||||||
{
|
{
|
||||||
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined
|
public class LobbyCommands : ServerTrait, IInterpretCommand, INotifyServerStart, INotifyServerEmpty, IClientJoined, OpenRA.Server.ITick
|
||||||
{
|
{
|
||||||
[TranslationReference]
|
[TranslationReference]
|
||||||
const string CustomRules = "notification-custom-rules";
|
const string CustomRules = "notification-custom-rules";
|
||||||
@@ -55,6 +55,9 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
const string NoKickGameStarted = "notification-no-kick-game-started";
|
const string NoKickGameStarted = "notification-no-kick-game-started";
|
||||||
|
|
||||||
[TranslationReference("admin", "player")]
|
[TranslationReference("admin", "player")]
|
||||||
|
const string AdminKicked = "notification-admin-kicked";
|
||||||
|
|
||||||
|
[TranslationReference("player")]
|
||||||
const string Kicked = "notification-kicked";
|
const string Kicked = "notification-kicked";
|
||||||
|
|
||||||
[TranslationReference("admin", "player")]
|
[TranslationReference("admin", "player")]
|
||||||
@@ -156,6 +159,9 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
[TranslationReference]
|
[TranslationReference]
|
||||||
const string YouWereKicked = "notification-you-were-kicked";
|
const string YouWereKicked = "notification-you-were-kicked";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickDisabled = "notification-vote-kick-disabled";
|
||||||
|
|
||||||
readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
|
readonly IDictionary<string, Func<S, Connection, Session.Client, string, bool>> commandHandlers = new Dictionary<string, Func<S, Connection, Session.Client, string, bool>>
|
||||||
{
|
{
|
||||||
{ "state", State },
|
{ "state", State },
|
||||||
@@ -170,6 +176,7 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
{ "option", Option },
|
{ "option", Option },
|
||||||
{ "assignteams", AssignTeams },
|
{ "assignteams", AssignTeams },
|
||||||
{ "kick", Kick },
|
{ "kick", Kick },
|
||||||
|
{ "vote_kick", VoteKick },
|
||||||
{ "make_admin", MakeAdmin },
|
{ "make_admin", MakeAdmin },
|
||||||
{ "make_spectator", MakeSpectator },
|
{ "make_spectator", MakeSpectator },
|
||||||
{ "name", Name },
|
{ "name", Name },
|
||||||
@@ -207,7 +214,7 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
lock (server.LobbyInfo)
|
lock (server.LobbyInfo)
|
||||||
{
|
{
|
||||||
// Kick command is always valid for the host
|
// Kick command is always valid for the host
|
||||||
if (command.StartsWith("kick "))
|
if (command.StartsWith("kick ", StringComparison.Ordinal) || command.StartsWith("vote_kick ", StringComparison.Ordinal))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (server.State == ServerState.GameStarted)
|
if (server.State == ServerState.GameStarted)
|
||||||
@@ -804,14 +811,14 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!server.CanKickClient(kickClient))
|
if (server.State == ServerState.GameStarted && !kickClient.IsObserver && !server.HasClientWonOrLost(kickClient))
|
||||||
{
|
{
|
||||||
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
|
server.SendLocalizedMessageTo(conn, NoKickGameStarted);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Write("server", $"Kicking client {kickClientID}.");
|
Log.Write("server", $"Kicking client {kickClientID}.");
|
||||||
server.SendLocalizedMessage(Kicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
|
server.SendLocalizedMessage(AdminKicked, Translation.Arguments("admin", client.Name, "player", kickClient.Name));
|
||||||
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
|
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
|
||||||
server.DropClient(kickConn);
|
server.DropClient(kickConn);
|
||||||
|
|
||||||
@@ -829,6 +836,62 @@ namespace OpenRA.Mods.Common.Server
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool VoteKick(S server, Connection conn, Session.Client client, string s)
|
||||||
|
{
|
||||||
|
lock (server.LobbyInfo)
|
||||||
|
{
|
||||||
|
var split = s.Split(' ');
|
||||||
|
if (split.Length != 2)
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.Settings.EnableVoteKick)
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, VoteKickDisabled);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var kickConn = Exts.TryParseInt32Invariant(split[0], out var kickClientID)
|
||||||
|
? server.Conns.SingleOrDefault(c => server.GetClient(c)?.Index == kickClientID) : null;
|
||||||
|
|
||||||
|
if (kickConn == null)
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, KickNone);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var kickClient = server.GetClient(kickConn);
|
||||||
|
if (client == kickClient)
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, NoKickSelf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bool.TryParse(split[1], out var vote))
|
||||||
|
{
|
||||||
|
server.SendLocalizedMessageTo(conn, MalformedCommand, Translation.Arguments("command", "vote_kick"));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.VoteKickTracker.VoteKick(conn, client, kickConn, kickClient, kickClientID, vote))
|
||||||
|
{
|
||||||
|
Log.Write("server", $"Kicking client {kickClientID}.");
|
||||||
|
server.SendLocalizedMessage(Kicked, Translation.Arguments("player", kickClient.Name));
|
||||||
|
server.SendOrderTo(kickConn, "ServerError", YouWereKicked);
|
||||||
|
server.DropClient(kickConn);
|
||||||
|
|
||||||
|
server.SyncLobbyClients();
|
||||||
|
server.SyncLobbySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenRA.Server.ITick.Tick(S server) => server.VoteKickTracker.Tick();
|
||||||
|
|
||||||
static bool MakeAdmin(S server, Connection conn, Session.Client client, string s)
|
static bool MakeAdmin(S server, Connection conn, Session.Client client, string s)
|
||||||
{
|
{
|
||||||
lock (server.LobbyInfo)
|
lock (server.LobbyInfo)
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
[TranslationReference]
|
[TranslationReference]
|
||||||
const string Gone = "label-client-state-disconnected";
|
const string Gone = "label-client-state-disconnected";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string KickTooltip = "button-kick-player";
|
||||||
|
|
||||||
[TranslationReference("player")]
|
[TranslationReference("player")]
|
||||||
const string KickTitle = "dialog-kick.title";
|
const string KickTitle = "dialog-kick.title";
|
||||||
|
|
||||||
@@ -58,6 +61,30 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
[TranslationReference]
|
[TranslationReference]
|
||||||
const string KickAccept = "dialog-kick.confirm";
|
const string KickAccept = "dialog-kick.confirm";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string KickVoteTooltip = "button-vote-kick-player";
|
||||||
|
|
||||||
|
[TranslationReference("player")]
|
||||||
|
const string VoteKickTitle = "dialog-vote-kick.title";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickPrompt = "dialog-vote-kick.prompt";
|
||||||
|
|
||||||
|
[TranslationReference("bots")]
|
||||||
|
const string VoteKickPromptBreakBots = "dialog-vote-kick.prompt-break-bots";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickVoteStart = "dialog-vote-kick.vote-start";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickVoteFor = "dialog-vote-kick.vote-for";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickVoteAgainst = "dialog-vote-kick.vote-against";
|
||||||
|
|
||||||
|
[TranslationReference]
|
||||||
|
const string VoteKickVoteCancel = "dialog-vote-kick.vote-cancel";
|
||||||
|
|
||||||
[ObjectCreator.UseCtor]
|
[ObjectCreator.UseCtor]
|
||||||
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu)
|
public GameInfoStatsLogic(Widget widget, ModData modData, World world, OrderManager orderManager, WorldRenderer worldRenderer, Action<bool> hideMenu)
|
||||||
{
|
{
|
||||||
@@ -106,6 +133,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
var spectatorTemplate = playerPanel.Get("SPECTATOR_TEMPLATE");
|
var spectatorTemplate = playerPanel.Get("SPECTATOR_TEMPLATE");
|
||||||
var unmuteTooltip = TranslationProvider.GetString(Unmute);
|
var unmuteTooltip = TranslationProvider.GetString(Unmute);
|
||||||
var muteTooltip = TranslationProvider.GetString(Mute);
|
var muteTooltip = TranslationProvider.GetString(Mute);
|
||||||
|
var kickTooltip = TranslationProvider.GetString(KickTooltip);
|
||||||
|
var voteKickTooltip = TranslationProvider.GetString(KickVoteTooltip);
|
||||||
playerPanel.RemoveChildren();
|
playerPanel.RemoveChildren();
|
||||||
|
|
||||||
var teams = world.Players.Where(p => !p.NonCombatant && p.Playable)
|
var teams = world.Players.Where(p => !p.NonCombatant && p.Playable)
|
||||||
@@ -114,22 +143,78 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
.GroupBy(p => (world.LobbyInfo.ClientWithIndex(p.Player.ClientIndex) ?? new Session.Client()).Team)
|
.GroupBy(p => (world.LobbyInfo.ClientWithIndex(p.Player.ClientIndex) ?? new Session.Client()).Team)
|
||||||
.OrderByDescending(g => g.Sum(gg => gg.PlayerStatistics?.Experience ?? 0));
|
.OrderByDescending(g => g.Sum(gg => gg.PlayerStatistics?.Experience ?? 0));
|
||||||
|
|
||||||
void KickAction(Session.Client client)
|
void KickAction(Session.Client client, Func<bool> isVoteKick)
|
||||||
{
|
{
|
||||||
hideMenu(true);
|
hideMenu(true);
|
||||||
ConfirmationDialogs.ButtonPrompt(modData,
|
if (isVoteKick())
|
||||||
title: KickTitle,
|
{
|
||||||
titleArguments: Translation.Arguments("player", client.Name),
|
var botsCount = 0;
|
||||||
text: KickPrompt,
|
if (client.IsAdmin)
|
||||||
onConfirm: () =>
|
botsCount = world.Players.Count(p => p.IsBot && p.WinState == WinState.Undefined);
|
||||||
|
|
||||||
|
if (UnitOrders.KickVoteTarget == null)
|
||||||
{
|
{
|
||||||
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
|
ConfirmationDialogs.ButtonPrompt(modData,
|
||||||
hideMenu(false);
|
title: VoteKickTitle,
|
||||||
},
|
titleArguments: Translation.Arguments("player", client.Name),
|
||||||
onCancel: () => hideMenu(false),
|
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
|
||||||
confirmText: KickAccept);
|
textArguments: Translation.Arguments("bots", botsCount),
|
||||||
|
onConfirm: () =>
|
||||||
|
{
|
||||||
|
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
|
||||||
|
hideMenu(false);
|
||||||
|
},
|
||||||
|
confirmText: VoteKickVoteStart,
|
||||||
|
onCancel: () => hideMenu(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmationDialogs.ButtonPrompt(modData,
|
||||||
|
title: VoteKickTitle,
|
||||||
|
titleArguments: Translation.Arguments("player", client.Name),
|
||||||
|
text: botsCount > 0 ? VoteKickPromptBreakBots : VoteKickPrompt,
|
||||||
|
textArguments: Translation.Arguments("bots", botsCount),
|
||||||
|
onConfirm: () =>
|
||||||
|
{
|
||||||
|
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {true}"));
|
||||||
|
hideMenu(false);
|
||||||
|
},
|
||||||
|
confirmText: VoteKickVoteFor,
|
||||||
|
onOther: () =>
|
||||||
|
{
|
||||||
|
Ui.CloseWindow();
|
||||||
|
orderManager.IssueOrder(Order.Command($"vote_kick {client.Index} {false}"));
|
||||||
|
hideMenu(false);
|
||||||
|
},
|
||||||
|
otherText: VoteKickVoteAgainst,
|
||||||
|
onCancel: () => hideMenu(false),
|
||||||
|
cancelText: VoteKickVoteCancel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfirmationDialogs.ButtonPrompt(modData,
|
||||||
|
title: KickTitle,
|
||||||
|
titleArguments: Translation.Arguments("player", client.Name),
|
||||||
|
text: KickPrompt,
|
||||||
|
onConfirm: () =>
|
||||||
|
{
|
||||||
|
orderManager.IssueOrder(Order.Command($"kick {client.Index} {false}"));
|
||||||
|
hideMenu(false);
|
||||||
|
},
|
||||||
|
confirmText: KickAccept,
|
||||||
|
onCancel: () => hideMenu(false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var localClient = orderManager.LocalClient;
|
||||||
|
var localPlayer = localClient == null ? null : world.Players.FirstOrDefault(player => player.ClientIndex == localClient.Index);
|
||||||
|
bool LocalPlayerCanKick() => localClient != null
|
||||||
|
&& (Game.IsHost || ((!orderManager.LocalClient.IsObserver) && localPlayer.WinState == WinState.Undefined));
|
||||||
|
bool CanClientBeKicked(Session.Client client, Func<bool> isVoteKick) =>
|
||||||
|
client.Index != localClient.Index && client.State != Session.ClientState.Disconnected
|
||||||
|
&& (!client.IsAdmin || orderManager.LobbyInfo.GlobalSettings.Dedicated)
|
||||||
|
&& (!isVoteKick() || UnitOrders.KickVoteTarget == null || UnitOrders.KickVoteTarget == client.Index);
|
||||||
|
|
||||||
foreach (var t in teams)
|
foreach (var t in teams)
|
||||||
{
|
{
|
||||||
if (teams.Count() > 1)
|
if (teams.Count() > 1)
|
||||||
@@ -182,8 +267,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
muteCheckbox.GetTooltipText = () => muteCheckbox.IsChecked() ? unmuteTooltip : muteTooltip;
|
muteCheckbox.GetTooltipText = () => muteCheckbox.IsChecked() ? unmuteTooltip : muteTooltip;
|
||||||
|
|
||||||
var kickButton = item.Get<ButtonWidget>("KICK");
|
var kickButton = item.Get<ButtonWidget>("KICK");
|
||||||
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected && pp.WinState != WinState.Undefined && !pp.IsBot;
|
bool IsVoteKick() => !Game.IsHost || pp.WinState == WinState.Undefined;
|
||||||
kickButton.OnClick = () => KickAction(client);
|
kickButton.IsVisible = () => !pp.IsBot && LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
|
||||||
|
kickButton.OnClick = () => KickAction(client, IsVoteKick);
|
||||||
|
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
|
||||||
|
|
||||||
playerPanel.AddChild(item);
|
playerPanel.AddChild(item);
|
||||||
}
|
}
|
||||||
@@ -217,8 +304,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
|
|||||||
};
|
};
|
||||||
|
|
||||||
var kickButton = item.Get<ButtonWidget>("KICK");
|
var kickButton = item.Get<ButtonWidget>("KICK");
|
||||||
kickButton.IsVisible = () => Game.IsHost && client.Index != orderManager.LocalClient?.Index && client.State != Session.ClientState.Disconnected;
|
bool IsVoteKick() => !Game.IsHost;
|
||||||
kickButton.OnClick = () => KickAction(client);
|
kickButton.IsVisible = () => LocalPlayerCanKick() && CanClientBeKicked(client, IsVoteKick);
|
||||||
|
kickButton.OnClick = () => KickAction(client, IsVoteKick);
|
||||||
|
kickButton.GetTooltipText = () => IsVoteKick() ? voteKickTooltip : kickTooltip;
|
||||||
|
|
||||||
var muteCheckbox = item.Get<CheckboxWidget>("MUTE");
|
var muteCheckbox = item.Get<CheckboxWidget>("MUTE");
|
||||||
muteCheckbox.IsChecked = () => TextNotificationsManager.MutedPlayers[client.Index];
|
muteCheckbox.IsChecked = () => TextNotificationsManager.MutedPlayers[client.Index];
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ Container@SKIRMISH_STATS:
|
|||||||
Height: 25
|
Height: 25
|
||||||
Background: checkbox-toggle
|
Background: checkbox-toggle
|
||||||
TooltipContainer: TOOLTIP_CONTAINER
|
TooltipContainer: TOOLTIP_CONTAINER
|
||||||
TooltipText: Kick this player
|
|
||||||
Children:
|
Children:
|
||||||
Image:
|
Image:
|
||||||
ImageCollection: lobby-bits
|
ImageCollection: lobby-bits
|
||||||
@@ -182,7 +181,6 @@ Container@SKIRMISH_STATS:
|
|||||||
Height: 25
|
Height: 25
|
||||||
Background: checkbox-toggle
|
Background: checkbox-toggle
|
||||||
TooltipContainer: TOOLTIP_CONTAINER
|
TooltipContainer: TOOLTIP_CONTAINER
|
||||||
TooltipText: Kick this player
|
|
||||||
Children:
|
Children:
|
||||||
Image:
|
Image:
|
||||||
ImageCollection: lobby-bits
|
ImageCollection: lobby-bits
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ Container@SKIRMISH_STATS:
|
|||||||
Height: 25
|
Height: 25
|
||||||
Background: checkbox-toggle
|
Background: checkbox-toggle
|
||||||
TooltipContainer: TOOLTIP_CONTAINER
|
TooltipContainer: TOOLTIP_CONTAINER
|
||||||
TooltipText: Kick this player
|
|
||||||
Children:
|
Children:
|
||||||
Image:
|
Image:
|
||||||
ImageCollection: lobby-bits
|
ImageCollection: lobby-bits
|
||||||
@@ -179,7 +178,6 @@ Container@SKIRMISH_STATS:
|
|||||||
Height: 25
|
Height: 25
|
||||||
Background: checkbox-toggle
|
Background: checkbox-toggle
|
||||||
TooltipContainer: TOOLTIP_CONTAINER
|
TooltipContainer: TOOLTIP_CONTAINER
|
||||||
TooltipText: Kick this player
|
|
||||||
Children:
|
Children:
|
||||||
Image:
|
Image:
|
||||||
ImageCollection: lobby-bits
|
ImageCollection: lobby-bits
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ notification-admin-change-configuration = Only the host can change the configura
|
|||||||
notification-changed-map = { $player } changed the map to { $map }
|
notification-changed-map = { $player } changed the map to { $map }
|
||||||
notification-option-changed = { $player } changed { $name } to { $value }.
|
notification-option-changed = { $player } changed { $name } to { $value }.
|
||||||
notification-you-were-kicked = You have been kicked from the server.
|
notification-you-were-kicked = You have been kicked from the server.
|
||||||
notification-kicked = { $admin } kicked { $player } from the server.
|
notification-admin-kicked = { $admin } kicked { $player } from the server.
|
||||||
|
notification-kicked = { $player } was kicked from the server.
|
||||||
notification-temp-ban = { $admin } temporarily banned { $player } from the server.
|
notification-temp-ban = { $admin } temporarily banned { $player } from the server.
|
||||||
notification-admin-transfer-admin = Only admins can transfer admin to another player.
|
notification-admin-transfer-admin = Only admins can transfer admin to another player.
|
||||||
notification-admin-move-spectators = Only the host can move players to spectators.
|
notification-admin-move-spectators = Only the host can move players to spectators.
|
||||||
@@ -95,6 +96,14 @@ notification-chat-temp-disabled =
|
|||||||
*[other] Chat is disabled. Please try again in { $remaining } seconds.
|
*[other] Chat is disabled. Please try again in { $remaining } seconds.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## VoteKickTracker
|
||||||
|
notification-unable-to-start-a-vote = Unable to start a vote.
|
||||||
|
notification-insufficient-votes-to-kick = Insufficient votes to kick player { $kickee }.
|
||||||
|
notification-kick-already-voted = You have already voted.
|
||||||
|
notification-vote-kick-started = Player { $kicker } has started a vote to kick player { $kickee }.
|
||||||
|
notification-vote-kick-in-progress = { $percentage }% of players have voted to kick player { $kickee }.
|
||||||
|
notification-vote-kick-ended = Vote to kick player { $kickee } has failed.
|
||||||
|
|
||||||
## ActorEditLogic
|
## ActorEditLogic
|
||||||
label-duplicate-actor-id = Duplicate Actor ID
|
label-duplicate-actor-id = Duplicate Actor ID
|
||||||
label-actor-id = Enter an Actor ID
|
label-actor-id = Enter an Actor ID
|
||||||
@@ -149,12 +158,29 @@ label-mission-failed = Failed
|
|||||||
label-client-state-disconnected = Gone
|
label-client-state-disconnected = Gone
|
||||||
label-mute-player = Mute this player
|
label-mute-player = Mute this player
|
||||||
label-unmute-player = Unmute this player
|
label-unmute-player = Unmute this player
|
||||||
|
button-kick-player = Kick this player
|
||||||
|
button-vote-kick-player = Vote to kick this player
|
||||||
|
|
||||||
dialog-kick =
|
dialog-kick =
|
||||||
.title = Kick { $player }?
|
.title = Kick { $player }?
|
||||||
.prompt = They will not be able to rejoin this game.
|
.prompt = This player will not be able to rejoin the game.
|
||||||
.confirm = Kick
|
.confirm = Kick
|
||||||
|
|
||||||
|
dialog-vote-kick =
|
||||||
|
.title = Vote to kick { $player }?
|
||||||
|
.prompt = This player will not be able to rejoin the game.
|
||||||
|
.prompt-break-bots =
|
||||||
|
{ $bots ->
|
||||||
|
[one] Kicking the game admin will also kick 1 bot.
|
||||||
|
*[other] Kicking the game admin will also kick { $bots } bots.
|
||||||
|
}
|
||||||
|
.vote-start = Start Vote
|
||||||
|
.vote-for = Vote For
|
||||||
|
.vote-against = Vote Against
|
||||||
|
.vote-cancel = Abstain
|
||||||
|
|
||||||
|
notification-vote-kick-disabled = Vote kick is disabled on this server.
|
||||||
|
|
||||||
## GameTimerLogic
|
## GameTimerLogic
|
||||||
label-paused = Paused
|
label-paused = Paused
|
||||||
label-max-speed = Max Speed
|
label-max-speed = Max Speed
|
||||||
|
|||||||
Reference in New Issue
Block a user