Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 42 additions & 19 deletions apps/counterstrikesharp/src/FiveStack.Events/PlayerConnected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
using CounterStrikeSharp.API.Modules.Utils;
using FiveStack.Entities;
using FiveStack.Utilities;
using Microsoft.Extensions.Logging;

namespace FiveStack;

public partial class FiveStackPlugin
{
private readonly HashSet<ulong> _overCapacityKicks = new();

[GameEventHandler]
public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo info)
{
Expand All @@ -27,17 +30,52 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo i
return HookResult.Continue;
}

_surrenderSystem.CancelDisconnectTimer(@event.Userid.SteamID);
CCSPlayerController player = @event.Userid;

_overCapacityKicks.Remove(player.SteamID);

Guid? lineup_id = MatchUtility.GetPlayerLineup(matchData, player);

CsTeam placementTeam = match.GetPlacementSide(match.GetExpectedTeam(player));
int capacity = match.GetExpectedPlayerCount() / 2;

// Decided before the disconnect timer and the roster resume: the player
// is about to be kicked, so they must neither count towards a whole
// roster nor have the kick treated as them leaving the match.
if (
LineupCapacityUtility.IsOverCapacity(
MatchUtility
.Players()
.Select(connected =>
(
connected.SteamID.ToString(),
MatchUtility.GetPlayerLineup(matchData, connected),
(int)connected.Team
)
),
player.SteamID.ToString(),
lineup_id,
(int)placementTeam,
capacity
)
)
{
_logger.LogInformation(
$"Kicking {player.PlayerName} ({player.SteamID}): their lineup already has {capacity} playing"
);
_overCapacityKicks.Add(player.SteamID);
Server.ExecuteCommand($"kickid {player.UserId}");
return HookResult.Continue;
}

_surrenderSystem.CancelDisconnectTimer(player.SteamID);

// CancelDisconnectTimer only resumes when that player actually had a
// timer, which is never the case for someone who left during warmup or
// knife, or when the pause came from RoundStart going short-handed. If
// the roster is whole again there is nothing left to wait for.
_surrenderSystem.ResumeIfRosterWhole();

CCSPlayerController player = @event.Userid;

Guid? lineup_id = MatchUtility.GetPlayerLineup(matchData, player);
List<MatchMember> players = matchData
.lineup_1.lineup_players.Concat(matchData.lineup_2.lineup_players)
.ToList();
Expand Down Expand Up @@ -74,21 +112,6 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo i
}
}

CsTeam placementTeam = match.GetPlacementSide(match.GetExpectedTeam(player));
int expectedTeamCount = match.GetExpectedPlayerCount() / 2;
int teamCount = TeamUtility.GetTeamCount(placementTeam);

if (player.Team == placementTeam)
{
teamCount--;
}

if (teamCount > expectedTeamCount)
{
Server.ExecuteCommand($"kickid {player.UserId}");
return HookResult.Continue;
}

match.EnforceMemberTeam(player, CsTeam.None);

_matchEvents.PublishGameEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo

CCSPlayerController player = @event.Userid;

if (_overCapacityKicks.Remove(player.SteamID))
{
return HookResult.Continue;
}

MatchMember? member = MatchUtility.GetMemberFromLineup(
matchData,
player.SteamID.ToString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,5 @@ int round

return totalCash;
}

public static int GetTeamCount(CsTeam csTeam)
{
return MatchUtility
.Teams()
.Count(matchTeam =>
matchTeam.PlayerControllers.Count > 0 && matchTeam.TeamNum == (int)csTeam
);
}
}
}
124 changes: 124 additions & 0 deletions apps/counterstrikesharp/test/LineupCapacityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using FiveStack.Utilities;
using Xunit;

public class LineupCapacityTests
{
private const int Spectator = 1;
private const int T = 2;
private const int CT = 3;
private const int Capacity = 5;
private const string Joiner = "joiner";

private static readonly Guid LineupA = Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
private static readonly Guid LineupB = Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");

private static List<(string SteamId, Guid? LineupId, int TeamNum)> Players(
Guid? lineupId,
int teamNum,
int count,
string prefix
)
{
return Enumerable
.Range(0, count)
.Select(index => ($"{prefix}{index}", lineupId, teamNum))
.ToList();
}

[Fact]
public void SixthMemberOfAPlayingLineupIsOverCapacity()
{
var connected = Players(LineupA, CT, 5, "a");
connected.Add((Joiner, LineupA, 0));

Assert.True(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);
}

[Fact]
public void LineupStillOnTheOtherSideThroughHalftimeCounts()
{
var connected = Players(LineupA, T, 5, "a");
connected.AddRange(Players(LineupB, CT, 5, "b"));
connected.Add((Joiner, LineupA, 0));

Assert.True(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);
}

[Fact]
public void FifthMemberFitsWhetherOrNotTheyAreAlreadyPlaced()
{
var connected = Players(LineupA, CT, 4, "a");
connected.Add((Joiner, LineupA, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);

connected[^1] = (Joiner, LineupA, CT);

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);
}

[Fact]
public void OtherLineupOnTheSameSideDoesNotCount()
{
var connected = Players(LineupB, CT, 5, "b");
connected.AddRange(Players(LineupA, T, 4, "a"));
connected.Add((Joiner, LineupA, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);
}

[Fact]
public void BenchedMembersDoNotCount()
{
var connected = Players(LineupA, CT, 4, "a");
connected.AddRange(Players(LineupA, Spectator, 2, "bench"));
connected.Add((Joiner, LineupA, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, CT, Capacity)
);
}

[Fact]
public void MemberNotHeadedForTOrCTIsNotOverCapacity()
{
var connected = Players(LineupA, CT, 5, "a");
connected.Add((Joiner, LineupA, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, LineupA, 0, Capacity)
);
}

[Fact]
public void PlayerOutsideEveryLineupIsNeverOverCapacity()
{
var connected = Players(null, CT, 5, "x");
connected.Add((Joiner, null, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, null, CT, Capacity)
);
}

[Fact]
public void SpectatorsAreNeverOverCapacity()
{
var connected = Players(null, Spectator, 8, "caster");
connected.Add((Joiner, null, 0));

Assert.False(
LineupCapacityUtility.IsOverCapacity(connected, Joiner, null, Spectator, Capacity)
);
}
}
61 changes: 42 additions & 19 deletions apps/swiftly/src/FiveStack.Events/PlayerConnected.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FiveStack.Entities;
using FiveStack.Utilities;
using Microsoft.Extensions.Logging;
using SwiftlyS2.Shared.GameEventDefinitions;
using SwiftlyS2.Shared.GameEvents;
using SwiftlyS2.Shared.Misc;
Expand All @@ -10,6 +11,8 @@ namespace FiveStack;

public partial class FiveStackPlugin
{
private readonly HashSet<ulong> _overCapacityKicks = new();

[GameEventHandler(HookMode.Post)]
public HookResult OnPlayerConnect(EventPlayerConnectFull @event)
{
Expand All @@ -27,17 +30,52 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event)
return HookResult.Continue;
}

_surrenderSystem.CancelDisconnectTimer(@event.UserIdPlayer.SteamID);
IPlayer player = @event.UserIdPlayer;

_overCapacityKicks.Remove(player.SteamID);

Guid? lineup_id = MatchUtility.GetPlayerLineup(matchData, player);

Team placementTeam = match.GetPlacementSide(match.GetExpectedTeam(player));
int capacity = match.GetExpectedPlayerCount() / 2;

// Decided before the disconnect timer and the roster resume: the player
// is about to be kicked, so they must neither count towards a whole
// roster nor have the kick treated as them leaving the match.
if (
LineupCapacityUtility.IsOverCapacity(
MatchUtility
.Players()
.Select(connected =>
(
connected.SteamID.ToString(),
MatchUtility.GetPlayerLineup(matchData, connected),
(int)connected.Controller.Team
)
),
player.SteamID.ToString(),
lineup_id,
(int)placementTeam,
capacity
)
)
{
_logger.LogInformation(
$"Kicking {player.Name} ({player.SteamID}): their lineup already has {capacity} playing"
);
_overCapacityKicks.Add(player.SteamID);
_core.Engine.ExecuteCommand($"kickid {player.UserID}");
return HookResult.Continue;
}

_surrenderSystem.CancelDisconnectTimer(player.SteamID);

// CancelDisconnectTimer only resumes when that player actually had a
// timer, which is never the case for someone who left during warmup or
// knife, or when the pause came from RoundStart going short-handed. If
// the roster is whole again there is nothing left to wait for.
_surrenderSystem.ResumeIfRosterWhole();

IPlayer player = @event.UserIdPlayer;

Guid? lineup_id = MatchUtility.GetPlayerLineup(matchData, player);
List<MatchMember> players = matchData
.lineup_1.lineup_players.Concat(matchData.lineup_2.lineup_players)
.ToList();
Expand Down Expand Up @@ -75,21 +113,6 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event)
}
}

Team placementTeam = match.GetPlacementSide(match.GetExpectedTeam(player));
int expectedTeamCount = match.GetExpectedPlayerCount() / 2;
int teamCount = TeamUtility.GetTeamCount(placementTeam);

if (player.Controller.Team == placementTeam)
{
teamCount--;
}

if (teamCount > expectedTeamCount)
{
_core.Engine.ExecuteCommand($"kickid {player.UserID}");
return HookResult.Continue;
}

match.EnforceMemberTeam(player, Team.None);

_matchEvents.PublishGameEvent(
Expand Down
5 changes: 5 additions & 0 deletions apps/swiftly/src/FiveStack.Events/PlayerDisconnected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event)

IPlayer player = @event.UserIdPlayer;

if (_overCapacityKicks.Remove(player.SteamID))
{
return HookResult.Continue;
}

MatchMember? member = MatchUtility.GetMemberFromLineup(
matchData,
player.SteamID.ToString(),
Expand Down
5 changes: 0 additions & 5 deletions apps/swiftly/src/FiveStack.Utilities/TeamUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,5 @@ int round

return totalCash;
}

public static int GetTeamCount(Team team)
{
return MatchUtility.Core.PlayerManager.GetInTeam(team).Any() ? 1 : 0;
}
}
}
Loading
Loading