Skip to content

Commit

Permalink
Merge pull request #138 from dnqbob/rebase
Browse files Browse the repository at this point in the history
Rebase
  • Loading branch information
MustaphaTR authored Oct 11, 2023
2 parents 725f926 + 5ace54b commit b676693
Show file tree
Hide file tree
Showing 20 changed files with 503 additions and 53 deletions.
3 changes: 2 additions & 1 deletion OpenRA.Game/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,9 @@ public static string TimestampedFilename(bool includemilliseconds = false, strin

static void JoinInner(OrderManager om)
{
// Refresh TextNotificationsManager before the game starts.
// Refresh static classes before the game starts.
TextNotificationsManager.Clear();
UnitOrders.Clear();

// 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
Expand Down
33 changes: 30 additions & 3 deletions OpenRA.Game/Network/UnitOrders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ public static class UnitOrders
[TranslationReference("player")]
const string GameUnpaused = "notification-game-unpaused";

public static int? KickVoteTarget { get; internal set; }

static Player FindPlayerByClient(this World world, Session.Client c)
{
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)
{
switch (order.OrderString)
Expand Down Expand Up @@ -75,9 +79,7 @@ internal static void ProcessOrder(OrderManager orderManager, World world, int cl

case "DisableChatEntry":
{
// Order must originate from the server
// Don't disable chat in replays
if (clientId != 0 || (world != null && world.IsReplay))
if (OrderNotFromServerOrWorldIsReplay(clientId, world))
break;

// Server may send MaxValue to indicate that it is disabled until further notice
Expand All @@ -89,6 +91,26 @@ internal static void ProcessOrder(OrderManager orderManager, World world, int cl
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":
{
var client = orderManager.LobbyInfo.ClientWithIndex(clientId);
Expand Down Expand Up @@ -385,5 +407,10 @@ static void ResolveOrder(Order order, World world, OrderManager orderManager, in
if (world.OrderValidators.All(vo => vo.OrderValidation(orderManager, world, clientId, order)))
order.Subject.ResolveOrder(order);
}

public static void Clear()
{
KickVoteTarget = null;
}
}
}
2 changes: 1 addition & 1 deletion OpenRA.Game/OpenRA.Game.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Linguini.Bundle" Version="0.5.0" />
<PackageReference Include="Linguini.Bundle" Version="0.6.0" />
<PackageReference Include="OpenRA-Eluant" Version="1.0.22" />
<PackageReference Include="Mono.NAT" Version="3.0.4" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
Expand Down
14 changes: 5 additions & 9 deletions OpenRA.Game/Server/Server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public sealed class Server
GameInformation gameInfo;
readonly List<GameInformation.Player> worldPlayers = new();
readonly Stopwatch pingUpdated = Stopwatch.StartNew();

public readonly VoteKickTracker VoteKickTracker;
readonly PlayerMessageTracker playerMessageTracker;

public ServerState State
Expand Down Expand Up @@ -318,6 +320,7 @@ public Server(List<IPEndPoint> endpoints, ServerSettings settings, ModData modDa
MapStatusCache = new MapStatusCache(modData, MapStatusChanged, type == ServerType.Dedicated && settings.EnableLintChecks);

playerMessageTracker = new PlayerMessageTracker(this, DispatchOrdersToClient, SendLocalizedMessageTo);
VoteKickTracker = new VoteKickTracker(this);

LobbyInfo = new Session
{
Expand Down Expand Up @@ -1162,15 +1165,8 @@ public Session.Client GetClient(Connection conn)
return LobbyInfo.ClientWithIndex(conn.PlayerIndex);
}

/// <summary>Does not check if client is admin.</summary>
public bool CanKickClient(Session.Client kickee)
{
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 bool HasClientWonOrLost(Session.Client client) =>
worldPlayers.FirstOrDefault(p => p?.ClientIndex == client.Index)?.Outcome != WinState.Undefined;

public void DropClient(Connection toDrop)
{
Expand Down
223 changes: 223 additions & 0 deletions OpenRA.Game/Server/VoteKickTracker.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
9 changes: 9 additions & 0 deletions OpenRA.Game/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ public class ServerSettings
[Desc("Delay in milliseconds before players can send chat messages after flood was detected.")]
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()
{
return (ServerSettings)MemberwiseClone();
Expand Down
3 changes: 2 additions & 1 deletion OpenRA.Mods.Common/Activities/UnloadCargo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,12 @@ public override bool Tick(Actor self)
var move = actor.Trait<IMove>();
var pos = actor.Trait<IPositionable>();
var passenger = actor.Trait<Passenger>();
pos.SetPosition(actor, exitSubCell.Value.Cell, exitSubCell.Value.SubCell);
pos.SetCenterPosition(actor, spawn);
actor.CancelActivity();
passenger.OnBeforeAddedToWorld(actor);
w.Add(actor);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public LuaTable ReinforceWithTransport(Player owner, string actorType, string[]
// Scripted cargo aircraft must turn to default position before unloading.
// TODO: pass facing through UnloadCargo instead.
if (aircraft != null)
transport.QueueActivity(new Land(transport, Target.FromCell(transport.World, entryPath.Last()), WDist.FromCells(dropRange), aircraft.Info.InitialFacing));
transport.QueueActivity(new Land(transport, Target.FromCell(transport.World, entryPath.Last()), WDist.FromCells(dropRange)));

if (cargo != null)
transport.QueueActivity(new UnloadCargo(transport, WDist.FromCells(dropRange)));
Expand Down
2 changes: 1 addition & 1 deletion OpenRA.Mods.Common/Scripting/LuaScript.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace OpenRA.Mods.Common.Scripting
{
[TraitLocation(SystemActors.World)]
[Desc("Part of the new Lua API.")]
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>
public class LuaScriptInfo : TraitInfo, Requires<SpawnMapActorsInfo>, NotBefore<SpawnStartingUnitsInfo>
{
[Desc("File names with location relative to the map.")]
public readonly HashSet<string> Scripts = new();
Expand Down
Loading

0 comments on commit b676693

Please sign in to comment.