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
29 changes: 1 addition & 28 deletions apps/counterstrikesharp/src/FiveStack.Events/PlayerConnected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,7 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event, GameEventInfo i
// 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.
//
// Counted across the roster rather than Players(), which also returns
// casters, admins and the client kicked further down this handler -- a
// spectator joining would otherwise resume a match still a man down. A
// pause the teams have to release themselves is never auto-resumed.
if (
match.IsPaused()
&& !match.timeoutSystem.ShouldRequireTeamResume()
&& ConnectedRosterCount(matchData) >= match.GetExpectedPlayerCount()
)
{
match.ResumeMatch();
}
_surrenderSystem.ResumeIfRosterWhole();

CCSPlayerController player = @event.Userid;

Expand Down Expand Up @@ -192,19 +180,4 @@ public HookResult HandleJoinTeam(CCSPlayerController? player, CommandInfo info)

return HookResult.Continue;
}

// How many of the two lineups are actually in the server right now.
private static int ConnectedRosterCount(MatchData matchData)
{
HashSet<string> roster = matchData
.lineup_1.lineup_players.Concat(matchData.lineup_2.lineup_players)
.Select(member => member.steam_id)
.Where(steamId => !string.IsNullOrEmpty(steamId))
.Select(steamId => steamId!)
.ToHashSet();

return MatchUtility
.Players()
.Count(controller => roster.Contains(controller.SteamID.ToString()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo
match.PauseMatch(
"Player disconnected, pausing match",
stillRequired: () =>
ConnectedRosterCount(match.GetMatchData() ?? matchData)
MatchUtility.ConnectedRosterCount(match.GetMatchData() ?? matchData)
< match.GetExpectedPlayerCount()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,8 @@ public void SetupMatch(MatchData match)

_rankSystem.OnMatchSetup(_matchData);

_surrenderSystem.OnMatchSetup(_matchData);

MatchMap? _currentMap = GetCurrentMap();

if (_currentMap == null)
Expand Down
99 changes: 76 additions & 23 deletions apps/counterstrikesharp/src/FiveStack.Services/SurrenderSystem.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using FiveStack.Entities;
Expand All @@ -21,6 +22,10 @@ public class SurrenderSystem
private Dictionary<CsTeam, Dictionary<ulong, Timer>> _disconnectTimers =
new Dictionary<CsTeam, Dictionary<ulong, Timer>>();

// Survives Reset(), which runs on every map of a series.
private readonly HashSet<ulong> _reportedAbandons = new HashSet<ulong>();
private Guid? _reportedAbandonsMatchId;

private Guid? winningLineupId;

public SurrenderSystem(
Expand Down Expand Up @@ -59,6 +64,10 @@ public void SetupDisconnectTimer(CsTeam team, ulong steamId)
return;
}

// Overwriting the slot without killing what is in it leaves a live timer
// nothing can reach, and it fires its own abandon three minutes later.
KillDisconnectTimer(steamId);

if (!_disconnectTimers.ContainsKey(team))
{
_disconnectTimers[team] = new Dictionary<ulong, Timer>();
Expand All @@ -77,39 +86,61 @@ public void SetupDisconnectTimer(CsTeam team, ulong steamId)
// we dont pass the team in because they may not be on the team immediately after reconnecting
public void CancelDisconnectTimer(ulong steamId)
{
bool canceledTimer = false;
foreach (var _team in MatchUtility.Teams())
if (!KillDisconnectTimer(steamId))
{
return;
}

ResumeIfRosterWhole();
}

// player_connect_full runs before the connecting client is on the runtime's
// player list, so the roster is only whole a frame later.
public void ResumeIfRosterWhole()
{
Server.NextFrame(() =>
{
CsTeam team = TeamUtility.TeamNumToCSTeam(_team.TeamNum);
MatchManager? match = _matchService.GetCurrentMatch();
MatchData? matchData = match?.GetMatchData();

if (_disconnectTimers.ContainsKey(team))
if (match == null || matchData == null || !match.IsPaused())
{
if (_disconnectTimers[team].ContainsKey(steamId))
{
_disconnectTimers[team][steamId].Kill();
_disconnectTimers[team].Remove(steamId);
canceledTimer = true;
}
return;
}
}

if (!canceledTimer)
{
return;
}
// A tactical or technical pause is released by the teams themselves.
if (match.timeoutSystem.ShouldRequireTeamResume())
{
return;
}

int currentPlayers = MatchUtility.Players().Count;
if (MatchUtility.ConnectedRosterCount(matchData) < match.GetExpectedPlayerCount())
{
return;
}

int expectedPlayers = _matchService.GetCurrentMatch()?.GetExpectedPlayerCount() ?? 10;
Reset();
match.ResumeMatch();
});
}

if (
_matchService.GetCurrentMatch()?.IsPaused() == true
&& currentPlayers == expectedPlayers
)
// Swept across every bucket, not just the team the player is on now: a
// player who reconnects onto the other side leaves a timer filed under the
// side they left from.
private bool KillDisconnectTimer(ulong steamId)
{
bool killed = false;

foreach (Dictionary<ulong, Timer> timers in _disconnectTimers.Values)
{
Reset();
_matchService.GetCurrentMatch()?.ResumeMatch();
if (timers.Remove(steamId, out Timer? timer))
{
timer?.Kill();
killed = true;
}
}

return killed;
}

public void SetupSurrender(CsTeam team, CCSPlayerController? player = null)
Expand Down Expand Up @@ -164,6 +195,19 @@ public void SetupSurrender(CsTeam team, CCSPlayerController? player = null)
);
}

// Abandons are reported once per match, never once per map, so the reported
// set is only cleared when a different match loads.
public void OnMatchSetup(MatchData matchData)
{
if (_reportedAbandonsMatchId == matchData.id)
{
return;
}

_reportedAbandonsMatchId = matchData.id;
_reportedAbandons.Clear();
}

public void Reset()
{
surrenderingVote = null;
Expand Down Expand Up @@ -254,6 +298,15 @@ public void Surrender(Guid lineupId)

public void PlayerAbandonedMatch(ulong steamId)
{
// The api escalates the ban on every report, so one leave reported twice
// -- after a reconnect, or again on the next map -- costs the player a
// longer ban than they earned.
if (!_reportedAbandons.Add(steamId))
{
_logger.LogInformation($"{steamId} was already reported as abandoned this match");
return;
}

_matchEvents.PublishGameEvent(
"abandoned",
new Dictionary<string, object>
Expand Down
20 changes: 20 additions & 0 deletions apps/counterstrikesharp/src/FiveStack.Utilities/MatchUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,26 @@ public static List<CCSPlayerController> Players()
return validPlayers;
}

public static HashSet<string> RosterSteamIds(MatchData matchData)
{
return matchData
.lineup_1.lineup_players.Concat(matchData.lineup_2.lineup_players)
.Select(member => member.steam_id)
.Where(steamId => !string.IsNullOrEmpty(steamId))
.Select(steamId => steamId!)
.ToHashSet();
}

// How many of the two lineups are actually in the server right now.
// Players() also returns casters and admins, who must never count
// towards a match being whole again.
public static int ConnectedRosterCount(MatchData matchData)
{
HashSet<string> roster = RosterSteamIds(matchData);

return Players().Count(controller => roster.Contains(controller.SteamID.ToString()));
}

public static IEnumerable<CCSTeam> Teams()
{
return CounterStrikeSharp.API.Utilities.FindAllEntitiesByDesignerName<CCSTeam>(
Expand Down
28 changes: 28 additions & 0 deletions apps/counterstrikesharp/test/MatchUtilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,32 @@ public void MapStatus_UnknownStringThrows()
{
Assert.Throws<ArgumentException>(() => MatchUtility.MapStatusStringToEnum("Bogus"));
}

[Fact]
public void RosterSteamIds_SpansBothLineups()
{
MatchData match = BuildMatch();
match.lineup_2.lineup_players[0].steam_id = "76561198000000002";
match.lineup_2.lineup_players[0].placeholder_name = "";

HashSet<string> roster = MatchUtility.RosterSteamIds(match);

Assert.Equal(new HashSet<string> { "76561198000000001", "76561198000000002" }, roster);
}

[Fact]
public void RosterSteamIds_SkipsPlaceholderSeats()
{
HashSet<string> roster = MatchUtility.RosterSteamIds(BuildMatch());

Assert.Equal(new HashSet<string> { "76561198000000001" }, roster);
}

[Fact]
public void RosterSteamIds_ExcludesNonRosterSteamIds()
{
HashSet<string> roster = MatchUtility.RosterSteamIds(BuildMatch());

Assert.DoesNotContain("76561198000009999", roster);
}
}
30 changes: 1 addition & 29 deletions apps/swiftly/src/FiveStack.Events/PlayerConnected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,7 @@ public HookResult OnPlayerConnect(EventPlayerConnectFull @event)
// 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.
//
// Counted across the roster rather than Players(), which also returns
// casters, admins and the client kicked further down this handler -- a
// spectator joining would otherwise resume a match still a man down. A
// pause the teams have to release themselves is never auto-resumed.
if (
match.IsPaused()
&& !match.timeoutSystem.ShouldRequireTeamResume()
&& ConnectedRosterCount(matchData) >= match.GetExpectedPlayerCount()
)
{
match.ResumeMatch();
}
_surrenderSystem.ResumeIfRosterWhole();

IPlayer player = @event.UserIdPlayer;

Expand Down Expand Up @@ -196,20 +184,4 @@ public HookResult HandleJoinTeam(IPlayer? player, string[] args)

return HookResult.Continue;
}

// How many of the two lineups are actually in the server right now.
private static int ConnectedRosterCount(MatchData matchData)
{
HashSet<string> roster = matchData
.lineup_1.lineup_players.Concat(matchData.lineup_2.lineup_players)
.Select(member => member.steam_id)
.Where(steamId => !string.IsNullOrEmpty(steamId))
.Select(steamId => steamId!)
.ToHashSet();

return MatchUtility
.Players()
.Count(player => roster.Contains(player.SteamID.ToString()));
}

}
2 changes: 1 addition & 1 deletion apps/swiftly/src/FiveStack.Events/PlayerDisconnected.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public HookResult OnPlayerDisconnect(EventPlayerDisconnect @event)
match.PauseMatch(
"Player disconnected, pausing match",
stillRequired: () =>
ConnectedRosterCount(match.GetMatchData() ?? matchData)
MatchUtility.ConnectedRosterCount(match.GetMatchData() ?? matchData)
< match.GetExpectedPlayerCount()
);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/swiftly/src/FiveStack.Services/MatchManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ public void SetupMatch(MatchData match)

_rankSystem.OnMatchSetup(_matchData);

_surrenderSystem.OnMatchSetup(_matchData);

MatchMap? _currentMap = GetCurrentMap();

if (_currentMap == null)
Expand Down
Loading
Loading