Skip to content
Draft
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
4 changes: 2 additions & 2 deletions Docs/source/admin/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Player Commands
+---------------+--------------+-----------------------------------------------------------------------------------+
| ``!<number>`` | | Select something in the current menu. |
+---------------+--------------+-----------------------------------------------------------------------------------+
| ``!stay`` | | Vote to stay at the current team. |
| ``!stay`` | | Vote to stay at the current team, or stay on current side after knife round. |
+---------------+--------------+-----------------------------------------------------------------------------------+
| ``!switch`` | | Vote to switch the current team. |
| ``!switch`` | | Vote to switch the current team, or switch sides after knife round. |
+---------------+--------------+-----------------------------------------------------------------------------------+


Expand Down
4 changes: 4 additions & 0 deletions Docs/source/admin/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ Matchconfig Fields
+--------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------+
| team_mode | 0 | Change how teams are defined. 0: Default (Teams are fix defined) 1: Scramble (Teams are scrambled when all players are ready) |
+--------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------+
| knife_round | false | Flag to determine if a knife round should be played to decide team sides. Winning team chooses to stay or switch. |
+--------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------+
| skip_veto | false | Flag to skip the map and team voting phases and start the match directly with the configured settings. |
+--------------------------+-----------------+-------------------------------------------------------------------------------------------------------------------------------+

Team Fields
'''''''''''''''''''''
Expand Down
2 changes: 2 additions & 0 deletions PugSharp.Api.Contract/IApiProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface IApiProvider
Task FinalizeMapAsync(MapResultParams finalizeMapParams, CancellationToken cancellationToken);
Task FinalizeAsync(SeriesResultParams seriesResultParams, CancellationToken cancellationToken);
Task FreeServerAsync(CancellationToken cancellationToken);
Task SendKnifeRoundStartedAsync(KnifeRoundStartedParams knifeRoundStartedParams, CancellationToken cancellationToken);
Task SendKnifeRoundWonAsync(KnifeRoundWonParams knifeRoundWonParams, CancellationToken cancellationToken);
}
13 changes: 13 additions & 0 deletions PugSharp.Api.Contract/KnifeRoundStartedParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace PugSharp.Api.Contract;

public class KnifeRoundStartedParams
{
public KnifeRoundStartedParams(string matchId, int mapNumber)
{
MatchId = matchId;
MapNumber = mapNumber;
}

public string MatchId { get; }
public int MapNumber { get; }
}
17 changes: 17 additions & 0 deletions PugSharp.Api.Contract/KnifeRoundWonParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace PugSharp.Api.Contract;

public class KnifeRoundWonParams
{
public KnifeRoundWonParams(string matchId, int mapNumber, int winningSide, bool swapped)
{
MatchId = matchId;
MapNumber = mapNumber;
WinningSide = winningSide;
Swapped = swapped;
}

public string MatchId { get; }
public int MapNumber { get; }
public int WinningSide { get; }
public bool Swapped { get; }
}
10 changes: 10 additions & 0 deletions PugSharp.Api.Contract/MultiApiProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,15 @@ public Task FreeServerAsync(CancellationToken cancellationToken)
return Task.WhenAll(_ApiProviders.Select(a => a.FreeServerAsync(cancellationToken)));
}

public Task SendKnifeRoundStartedAsync(KnifeRoundStartedParams knifeRoundStartedParams, CancellationToken cancellationToken)
{
return Task.WhenAll(_ApiProviders.Select(a => a.SendKnifeRoundStartedAsync(knifeRoundStartedParams, cancellationToken)));
}

public Task SendKnifeRoundWonAsync(KnifeRoundWonParams knifeRoundWonParams, CancellationToken cancellationToken)
{
return Task.WhenAll(_ApiProviders.Select(a => a.SendKnifeRoundWonAsync(knifeRoundWonParams, cancellationToken)));
}

#endregion
}
10 changes: 10 additions & 0 deletions PugSharp.Api.Json/JsonApiProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ public Task FreeServerAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
}

public Task SendKnifeRoundStartedAsync(KnifeRoundStartedParams knifeRoundStartedParams, CancellationToken cancellationToken)
{
return SerializeAndSaveData($"Match_{knifeRoundStartedParams.MatchId}_kniferoundstarted.json", knifeRoundStartedParams, cancellationToken);
}

public Task SendKnifeRoundWonAsync(KnifeRoundWonParams knifeRoundWonParams, CancellationToken cancellationToken)
{
return SerializeAndSaveData($"Match_{knifeRoundWonParams.MatchId}_kniferoundwon.json", knifeRoundWonParams, cancellationToken);
}


private void CreateStatsDirectoryIfNotExists()
{
Expand Down
12 changes: 12 additions & 0 deletions PugSharp.ApiStats/ApiStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ public async Task FreeServerAsync(CancellationToken cancellationToken)
await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false);
}

public Task SendKnifeRoundStartedAsync(KnifeRoundStartedParams knifeRoundStartedParams, CancellationToken cancellationToken)
{
// Knife round started events are not required for ApiStats
return Task.CompletedTask;
}

public Task SendKnifeRoundWonAsync(KnifeRoundWonParams knifeRoundWonParams, CancellationToken cancellationToken)
{
// Knife round won events are not required for ApiStats
return Task.CompletedTask;
}

#endregion

private async Task UpdatePlayerStatsInternalAsync(int mapNumber, ITeamInfo teamInfo1, ITeamInfo teamInfo2, IMap currentMap, CancellationToken cancellationToken)
Expand Down
6 changes: 6 additions & 0 deletions PugSharp.Config/MatchConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public class MatchConfig
[JsonPropertyName("team_mode")]
public TeamMode TeamMode { get; set; }

[JsonPropertyName("knife_round")]
public bool KnifeRound { get; init; } = false;

[JsonPropertyName("skip_veto")]
public bool SkipVeto { get; init; } = false;

[JsonPropertyName("cvars")]
public IDictionary<string, string> CVars { get; init; } = new Dictionary<string, string>(StringComparer.Ordinal);

Expand Down
4 changes: 4 additions & 0 deletions PugSharp.Match.Contract/MatchCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public enum MatchCommand
VoteTeam,
SwitchMap,
StartMatch,
StartKnifeRound,
CompleteKnifeRound,
StayAfterKnifeRound,
SwitchAfterKnifeRound,
CompleteMatch,
CompleteMap,
Pause,
Expand Down
2 changes: 2 additions & 0 deletions PugSharp.Match.Contract/MatchState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public enum MatchState
SwitchMap,
WaitingForPlayersReady,
MatchStarting,
KnifeRound,
WaitingForKnifeRoundDecision,
MatchRunning,
MatchPaused,
MapCompleted,
Expand Down
148 changes: 148 additions & 0 deletions PugSharp.Match.Tests/MatchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,87 @@ public void MatchTestWithOneMap()
PauseUnpauseMatch(csServer, match, player1);
}

[Fact]
public void KnifeRoundTest()
{
var config = CreateKnifeRoundTestConfig();

var serviceProvider = CreateTestProvider();

var matchPlayers = new List<IPlayer>();
var csServer = serviceProvider.GetRequiredService<ICsServer>();
csServer.LoadAllPlayers().Returns(matchPlayers);

var matchFactory = serviceProvider.GetRequiredService<MatchFactory>();
var match = matchFactory.CreateMatch(config);

Assert.Equal(MatchState.WaitingForPlayersConnectedReady, match.CurrentState);

IPlayer player1 = CreatePlayerSub(0, 0);
IPlayer player2 = CreatePlayerSub(1, 1);

ConnectPlayers(matchPlayers, match, player1, player2);
SetPlayersReady(match, player1, player2, MatchState.SwitchMap);

// Switch to match map
Assert.True(match.TryFireState(MatchCommand.SwitchMap));
Assert.Equal(MatchState.WaitingForPlayersReady, match.CurrentState);

// Set players ready again
match.TogglePlayerIsReady(player1);
match.TogglePlayerIsReady(player2);
Assert.Equal(MatchState.MatchStarting, match.CurrentState);

// Start match should go to knife round
Assert.True(match.TryFireState(MatchCommand.StartMatch));
Assert.Equal(MatchState.KnifeRound, match.CurrentState);

// Complete knife round
Assert.True(match.TryFireState(MatchCommand.CompleteKnifeRound));
Assert.Equal(MatchState.WaitingForKnifeRoundDecision, match.CurrentState);

// Vote to stay
Assert.True(match.VoteStayAfterKnifeRound(player1));
Assert.Equal(MatchState.MatchRunning, match.CurrentState);
}

[Fact]
public void SkipVetoTest()
{
var config = CreateSkipVetoTestConfig();

var serviceProvider = CreateTestProvider();

var matchPlayers = new List<IPlayer>();
var csServer = serviceProvider.GetRequiredService<ICsServer>();
csServer.LoadAllPlayers().Returns(matchPlayers);

var matchFactory = serviceProvider.GetRequiredService<MatchFactory>();
var match = matchFactory.CreateMatch(config);

Assert.Equal(MatchState.WaitingForPlayersConnectedReady, match.CurrentState);

IPlayer player1 = CreatePlayerSub(0, 0);
IPlayer player2 = CreatePlayerSub(1, 1);

// Add players
Assert.True(match.TryAddPlayer(player1));
Assert.True(match.TryAddPlayer(player2));
matchPlayers.Add(player1);
matchPlayers.Add(player2);
Assert.Equal(MatchState.WaitingForPlayersConnectedReady, match.CurrentState);

// Set first player ready
match.TogglePlayerIsReady(player1);
Assert.Equal(MatchState.WaitingForPlayersConnectedReady, match.CurrentState);

// Set second player ready - this should trigger state change
match.TogglePlayerIsReady(player2);

// Should now be in DefineTeams, which should auto-progress to SwitchMap
Assert.Equal(MatchState.SwitchMap, match.CurrentState);
}


private static void PauseUnpauseMatch(ICsServer csServer, Match match, IPlayer player1)
{
Expand Down Expand Up @@ -244,6 +325,73 @@ private static MatchConfig CreateExampleConfig(IEnumerable<string>? mapList = nu
return matchConfig;
}

private static MatchConfig CreateKnifeRoundTestConfig()
{
var matchConfig = new MatchConfig
{
MatchId = "1337",
PlayersPerTeam = 1,
MinPlayersToReady = 1,
NumMaps = 1,
KnifeRound = true,
SkipVeto = true,
Maplist = new List<string> { "de_dust2" },
Team1 = new Config.Team
{
Id = "1",
Name = "Team1",
Players = new Dictionary<ulong, string>()
{
{ 0,"Abc" },
},
},
Team2 = new Config.Team
{
Id = "2",
Name = "Team2",
Players = new Dictionary<ulong, string>()
{
{ 1,"Def" },
},
},
};

return matchConfig;
}

private static MatchConfig CreateSkipVetoTestConfig()
{
var matchConfig = new MatchConfig
{
MatchId = "1337",
PlayersPerTeam = 1,
MinPlayersToReady = 1,
NumMaps = 1,
SkipVeto = true,
Maplist = new List<string> { "de_dust2" },
Team1 = new Config.Team
{
Id = "1",
Name = "Team1",
Players = new Dictionary<ulong, string>()
{
{ 0,"Abc" },
},
},
Team2 = new Config.Team
{
Id = "2",
Name = "Team2",
Players = new Dictionary<ulong, string>()
{
{ 1,"Def" },
},
},
};

return matchConfig;
}

private static IPlayer CreatePlayerSub(ulong steamId, int playerId)
{
var playerTeam = Contract.Team.None;
Expand Down
Loading