diff --git a/Docs/source/admin/commands.rst b/Docs/source/admin/commands.rst index 43d6016..f29ff07 100644 --- a/Docs/source/admin/commands.rst +++ b/Docs/source/admin/commands.rst @@ -26,9 +26,9 @@ Player Commands +---------------+--------------+-----------------------------------------------------------------------------------+ | ``!`` | | 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. | +---------------+--------------+-----------------------------------------------------------------------------------+ diff --git a/Docs/source/admin/configuration.rst b/Docs/source/admin/configuration.rst index 8b6da0c..c316e2f 100644 --- a/Docs/source/admin/configuration.rst +++ b/Docs/source/admin/configuration.rst @@ -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 ''''''''''''''''''''' diff --git a/PugSharp.Api.Contract/IApiProvider.cs b/PugSharp.Api.Contract/IApiProvider.cs index 468ee94..eb02917 100644 --- a/PugSharp.Api.Contract/IApiProvider.cs +++ b/PugSharp.Api.Contract/IApiProvider.cs @@ -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); } diff --git a/PugSharp.Api.Contract/KnifeRoundStartedParams.cs b/PugSharp.Api.Contract/KnifeRoundStartedParams.cs new file mode 100644 index 0000000..074faff --- /dev/null +++ b/PugSharp.Api.Contract/KnifeRoundStartedParams.cs @@ -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; } +} \ No newline at end of file diff --git a/PugSharp.Api.Contract/KnifeRoundWonParams.cs b/PugSharp.Api.Contract/KnifeRoundWonParams.cs new file mode 100644 index 0000000..12cfdd5 --- /dev/null +++ b/PugSharp.Api.Contract/KnifeRoundWonParams.cs @@ -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; } +} \ No newline at end of file diff --git a/PugSharp.Api.Contract/MultiApiProvider.cs b/PugSharp.Api.Contract/MultiApiProvider.cs index f349a7e..db480c9 100644 --- a/PugSharp.Api.Contract/MultiApiProvider.cs +++ b/PugSharp.Api.Contract/MultiApiProvider.cs @@ -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 } diff --git a/PugSharp.Api.Json/JsonApiProvider.cs b/PugSharp.Api.Json/JsonApiProvider.cs index 1dab2ca..5b02947 100644 --- a/PugSharp.Api.Json/JsonApiProvider.cs +++ b/PugSharp.Api.Json/JsonApiProvider.cs @@ -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() { diff --git a/PugSharp.ApiStats/ApiStats.cs b/PugSharp.ApiStats/ApiStats.cs index 47f402a..03ab864 100644 --- a/PugSharp.ApiStats/ApiStats.cs +++ b/PugSharp.ApiStats/ApiStats.cs @@ -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) diff --git a/PugSharp.Config/MatchConfig.cs b/PugSharp.Config/MatchConfig.cs index 2298a2b..68f003e 100644 --- a/PugSharp.Config/MatchConfig.cs +++ b/PugSharp.Config/MatchConfig.cs @@ -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 CVars { get; init; } = new Dictionary(StringComparer.Ordinal); diff --git a/PugSharp.Match.Contract/MatchCommand.cs b/PugSharp.Match.Contract/MatchCommand.cs index f09935e..2b69171 100644 --- a/PugSharp.Match.Contract/MatchCommand.cs +++ b/PugSharp.Match.Contract/MatchCommand.cs @@ -10,6 +10,10 @@ public enum MatchCommand VoteTeam, SwitchMap, StartMatch, + StartKnifeRound, + CompleteKnifeRound, + StayAfterKnifeRound, + SwitchAfterKnifeRound, CompleteMatch, CompleteMap, Pause, diff --git a/PugSharp.Match.Contract/MatchState.cs b/PugSharp.Match.Contract/MatchState.cs index 7def2ec..ccc67fc 100644 --- a/PugSharp.Match.Contract/MatchState.cs +++ b/PugSharp.Match.Contract/MatchState.cs @@ -9,6 +9,8 @@ public enum MatchState SwitchMap, WaitingForPlayersReady, MatchStarting, + KnifeRound, + WaitingForKnifeRoundDecision, MatchRunning, MatchPaused, MapCompleted, diff --git a/PugSharp.Match.Tests/MatchTests.cs b/PugSharp.Match.Tests/MatchTests.cs index da66192..72006d7 100644 --- a/PugSharp.Match.Tests/MatchTests.cs +++ b/PugSharp.Match.Tests/MatchTests.cs @@ -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(); + var csServer = serviceProvider.GetRequiredService(); + csServer.LoadAllPlayers().Returns(matchPlayers); + + var matchFactory = serviceProvider.GetRequiredService(); + 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(); + var csServer = serviceProvider.GetRequiredService(); + csServer.LoadAllPlayers().Returns(matchPlayers); + + var matchFactory = serviceProvider.GetRequiredService(); + 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) { @@ -244,6 +325,73 @@ private static MatchConfig CreateExampleConfig(IEnumerable? 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 { "de_dust2" }, + Team1 = new Config.Team + { + Id = "1", + Name = "Team1", + Players = new Dictionary() + { + { 0,"Abc" }, + }, + }, + Team2 = new Config.Team + { + Id = "2", + Name = "Team2", + Players = new Dictionary() + { + { 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 { "de_dust2" }, + Team1 = new Config.Team + { + Id = "1", + Name = "Team1", + Players = new Dictionary() + { + { 0,"Abc" }, + }, + }, + Team2 = new Config.Team + { + Id = "2", + Name = "Team2", + Players = new Dictionary() + { + { 1,"Def" }, + }, + }, + }; + + return matchConfig; + } + private static IPlayer CreatePlayerSub(ulong steamId, int playerId) { var playerTeam = Contract.Team.None; diff --git a/PugSharp.Match/Match.cs b/PugSharp.Match/Match.cs index 9d24e3e..c939fe5 100644 --- a/PugSharp.Match/Match.cs +++ b/PugSharp.Match/Match.cs @@ -122,7 +122,7 @@ private void InitializeStateMachine() .OnExit(StopReadyReminder); _MatchStateMachine.Configure(MatchState.DefineTeams) - .Permit(MatchCommand.TeamsDefined, MatchState.MapVote) + .PermitDynamicIf(MatchCommand.TeamsDefined, () => MatchInfo.Config.SkipVeto ? MatchState.SwitchMap : MatchState.MapVote) .OnEntry(ContinueIfDefault) .OnEntry(ContinueIfPlayerSelect) .OnEntry(ScrambleTeams); @@ -150,9 +150,19 @@ private void InitializeStateMachine() .OnExit(StopReadyReminder); _MatchStateMachine.Configure(MatchState.MatchStarting) - .Permit(MatchCommand.StartMatch, MatchState.MatchRunning) + .PermitDynamicIf(MatchCommand.StartMatch, () => MatchInfo.Config.KnifeRound ? MatchState.KnifeRound : MatchState.MatchRunning) .OnEntry(StartMatch); + _MatchStateMachine.Configure(MatchState.KnifeRound) + .Permit(MatchCommand.CompleteKnifeRound, MatchState.WaitingForKnifeRoundDecision) + .OnEntry(StartKnifeRound); + + _MatchStateMachine.Configure(MatchState.WaitingForKnifeRoundDecision) + .Permit(MatchCommand.StayAfterKnifeRound, MatchState.MatchRunning) + .Permit(MatchCommand.SwitchAfterKnifeRound, MatchState.MatchRunning) + .OnEntry(WaitForKnifeRoundDecision) + .OnExit(CompleteKnifeRoundDecision); + _MatchStateMachine.Configure(MatchState.MatchRunning) .Permit(MatchCommand.DisconnectPlayer, MatchState.MatchPaused) .Permit(MatchCommand.Pause, MatchState.MatchPaused) @@ -298,11 +308,99 @@ private void StartMatch() string demoDirectory = Path.Combine(_CsServer.GameDirectory, "csgo", "PugSharp", "Demo"); MatchInfo.DemoFile = _CsServer.StartDemoRecording(demoDirectory, demoFileName); + if (MatchInfo.Config.KnifeRound) + { + _CsServer.PrintToChatAll("Knife round starting..."); + } + else + { + _CsServer.PrintToChatAll(_TextHelper.GetText(nameof(Resources.PugSharp_Match_Info_StartMatch), MatchInfo.MatchTeam1.TeamConfig.Name, MatchInfo.MatchTeam1.CurrentTeamSide, MatchInfo.MatchTeam2.TeamConfig.Name, MatchInfo.MatchTeam2.CurrentTeamSide)); + _ = _ApiProvider.GoingLiveAsync(new GoingLiveParams(MatchInfo.Config.MatchId, MatchInfo.CurrentMap.MapName, MatchInfo.CurrentMap.MapNumber), CancellationToken.None); + } + + TryFireState(MatchCommand.StartMatch); + } + + private void StartKnifeRound() + { + if (MatchInfo == null) + { + _Logger.LogError("Can not start knife round without a matchInfo!"); + return; + } + + // Load knife round config + _CsServer.LoadAndExecuteConfig("knife.cfg"); + + // Give all players knives only + _CsServer.SetupKnifeRound(); + + _CsServer.PrintToChatAll("Knife round started! Fight for the side selection!"); + + _ = _ApiProvider.SendKnifeRoundStartedAsync(new KnifeRoundStartedParams(MatchInfo.Config.MatchId, MatchInfo.CurrentMap.MapNumber), CancellationToken.None); + } + + private void WaitForKnifeRoundDecision() + { + if (MatchInfo == null) + { + _Logger.LogError("Can not wait for knife round decision without a matchInfo!"); + return; + } + + _CsServer.PrintToChatAll("Knife round ended! Winning team, please choose to !stay or !switch sides."); + + // Show menu to winning team + var winningTeam = GetKnifeRoundWinningTeam(); + if (winningTeam != null) + { + var knifeRoundOptions = new List() + { + new("stay", (opt, player) => VoteStayAfterKnifeRound(player)), + new("switch", (opt, player) => VoteSwitchAfterKnifeRound(player)), + }; + + ShowMenuToTeam(winningTeam, "Choose your side:", knifeRoundOptions); + } + } + + private void CompleteKnifeRoundDecision() + { + if (MatchInfo == null) + { + _Logger.LogError("Can not complete knife round decision without a matchInfo!"); + return; + } + + // Load live config for the actual match + _CsServer.LoadAndExecuteConfig("live.cfg"); + + // Restart Game to reset everything + _CsServer.RestartGame(); + _CsServer.PrintToChatAll(_TextHelper.GetText(nameof(Resources.PugSharp_Match_Info_StartMatch), MatchInfo.MatchTeam1.TeamConfig.Name, MatchInfo.MatchTeam1.CurrentTeamSide, MatchInfo.MatchTeam2.TeamConfig.Name, MatchInfo.MatchTeam2.CurrentTeamSide)); _ = _ApiProvider.GoingLiveAsync(new GoingLiveParams(MatchInfo.Config.MatchId, MatchInfo.CurrentMap.MapName, MatchInfo.CurrentMap.MapNumber), CancellationToken.None); + } - TryFireState(MatchCommand.StartMatch); + private MatchTeam? GetKnifeRoundWinningTeam() + { + // Logic to determine which team won the knife round + // This would typically be determined by which team has more players alive + // or other knife round winning conditions + var (ctScore, tScore) = _CsServer.LoadTeamsScore(); + + if (ctScore > tScore) + { + return MatchInfo.MatchTeam1.CurrentTeamSide == Team.CounterTerrorist ? MatchInfo.MatchTeam1 : MatchInfo.MatchTeam2; + } + else if (tScore > ctScore) + { + return MatchInfo.MatchTeam1.CurrentTeamSide == Team.Terrorist ? MatchInfo.MatchTeam1 : MatchInfo.MatchTeam2; + } + + // If tied, default to team 1 (could be randomized) + return MatchInfo.MatchTeam1; } private void SendMapResults() @@ -931,7 +1029,7 @@ private bool MapIsNotSelected() return !MapIsSelected(); } - private bool TryFireState(MatchCommand command) + public bool TryFireState(MatchCommand command) { if (_MatchStateMachine.CanFire(command)) { @@ -1301,6 +1399,78 @@ public bool VoteTeam(IPlayer player, string teamName) return true; } + public bool VoteStayAfterKnifeRound(IPlayer player) + { + // Not in correct state + if (CurrentState != MatchState.WaitingForKnifeRoundDecision) + { + player.PrintToChat("No knife round decision is expected at this time."); + return false; + } + + var winningTeam = GetKnifeRoundWinningTeam(); + if (winningTeam == null) + { + return false; + } + + // Player not permitted to vote - only winning team can vote + if (!winningTeam.Players.Select(x => x.Player.UserId).Contains(player.UserId)) + { + player.PrintToChat("You are not permitted to vote after the knife round. Only the winning team can vote."); + return false; + } + + player.PrintToChat("You voted to stay on the current side."); + + _ = _ApiProvider.SendKnifeRoundWonAsync(new KnifeRoundWonParams(MatchInfo.Config.MatchId, MatchInfo.CurrentMap.MapNumber, (int)winningTeam.CurrentTeamSide, false), CancellationToken.None); + + TryFireState(MatchCommand.StayAfterKnifeRound); + + return true; + } + + public bool VoteSwitchAfterKnifeRound(IPlayer player) + { + // Not in correct state + if (CurrentState != MatchState.WaitingForKnifeRoundDecision) + { + player.PrintToChat("No knife round decision is expected at this time."); + return false; + } + + var winningTeam = GetKnifeRoundWinningTeam(); + if (winningTeam == null) + { + return false; + } + + // Player not permitted to vote - only winning team can vote + if (!winningTeam.Players.Select(x => x.Player.UserId).Contains(player.UserId)) + { + player.PrintToChat("You are not permitted to vote after the knife round. Only the winning team can vote."); + return false; + } + + player.PrintToChat("You voted to switch sides."); + + // Switch the teams + SwitchTeamSides(); + + _ = _ApiProvider.SendKnifeRoundWonAsync(new KnifeRoundWonParams(MatchInfo.Config.MatchId, MatchInfo.CurrentMap.MapNumber, (int)winningTeam.CurrentTeamSide, true), CancellationToken.None); + + TryFireState(MatchCommand.SwitchAfterKnifeRound); + + return true; + } + + private void SwitchTeamSides() + { + var team1OriginalSide = MatchInfo.MatchTeam1.CurrentTeamSide; + MatchInfo.MatchTeam1.CurrentTeamSide = MatchInfo.MatchTeam2.CurrentTeamSide; + MatchInfo.MatchTeam2.CurrentTeamSide = team1OriginalSide; + } + public void Pause(IPlayer player) { if (!TryFireState(MatchCommand.Pause)) diff --git a/PugSharp.Server.Contract/ICsServer.cs b/PugSharp.Server.Contract/ICsServer.cs index a92f566..9a342a9 100644 --- a/PugSharp.Server.Contract/ICsServer.cs +++ b/PugSharp.Server.Contract/ICsServer.cs @@ -21,6 +21,7 @@ public interface ICsServer void RestartGame(); void RestoreBackup(string roundBackupFile); void SetupRoundBackup(string prefix); + void SetupKnifeRound(); string StartDemoRecording(string demoDirectory, string demoFileName); void StopDemoRecording(); void SwitchMap(string selectedMap); diff --git a/PugSharp.Translation/Properties/Resources.resx b/PugSharp.Translation/Properties/Resources.resx index 08384af..90ec116 100644 --- a/PugSharp.Translation/Properties/Resources.resx +++ b/PugSharp.Translation/Properties/Resources.resx @@ -339,4 +339,28 @@ [Voting] + + Knife round starting... + + + Knife round started! Fight for the side selection! + + + Knife round ended! Winning team, please choose to !stay or !switch sides. + + + Choose your side: + + + No knife round decision is expected at this time. + + + You are not permitted to vote after the knife round. Only the winning team can vote. + + + You voted to stay on the current side. + + + You voted to switch sides. + \ No newline at end of file diff --git a/PugSharp/Application.cs b/PugSharp/Application.cs index 35bcf69..3f2d678 100644 --- a/PugSharp/Application.cs +++ b/PugSharp/Application.cs @@ -397,6 +397,19 @@ private HookResult OnRoundEnd(EventRoundEnd eventRoundEnd, GameEventInfo info) return HookResult.Continue; } + if (_Match.CurrentState == MatchState.KnifeRound) + { + _Logger.LogInformation("Knife round ended"); + + // Use a timer to fire the CompleteKnifeRound command after a brief delay + _Plugin.AddTimer(2.0f, () => + { + _Match?.TryFireState(MatchCommand.CompleteKnifeRound); + }); + + return HookResult.Continue; + } + if (_Match.CurrentState == MatchState.MatchRunning) { _RoundStopwatch.Stop(); @@ -1823,8 +1836,16 @@ public void OnCommandSwitch(CCSPlayerController? player, CommandInfo command) return; } - var voteSite = p.TeamNum == (int)Match.Contract.Team.Terrorist ? "CT" : "T"; - _Match.VoteTeam(new Player(p.SteamID), voteSite); + // Check if this is a knife round decision + if (_Match.CurrentState == MatchState.WaitingForKnifeRoundDecision) + { + _Match.VoteSwitchAfterKnifeRound(new Player(p.SteamID)); + } + else + { + var voteSite = p.TeamNum == (int)Match.Contract.Team.Terrorist ? "CT" : "T"; + _Match.VoteTeam(new Player(p.SteamID), voteSite); + } }, command, player); @@ -1848,8 +1869,16 @@ public void OnCommandStay(CCSPlayerController? player, CommandInfo command) return; } - var voteSite = p.TeamNum == (int)Match.Contract.Team.Terrorist ? "T" : "CT"; - _Match.VoteTeam(new Player(p.SteamID), voteSite); + // Check if this is a knife round decision + if (_Match.CurrentState == MatchState.WaitingForKnifeRoundDecision) + { + _Match.VoteStayAfterKnifeRound(new Player(p.SteamID)); + } + else + { + var voteSite = p.TeamNum == (int)Match.Contract.Team.Terrorist ? "T" : "CT"; + _Match.VoteTeam(new Player(p.SteamID), voteSite); + } }, command, player); diff --git a/PugSharp/CsServer.cs b/PugSharp/CsServer.cs index e66999f..418fc95 100644 --- a/PugSharp/CsServer.cs +++ b/PugSharp/CsServer.cs @@ -155,6 +155,31 @@ public void SetupRoundBackup(string prefix) ExecuteCommand($"mp_backup_round_file {prefix}"); } + public void SetupKnifeRound() + { + _Logger.LogInformation("Setting up knife round"); + ExecuteCommand("mp_give_player_c4 0"); + ExecuteCommand("mp_weapons_allow_typecount -1"); + ExecuteCommand("mp_ct_default_primary \"\""); + ExecuteCommand("mp_ct_default_secondary \"\""); + ExecuteCommand("mp_t_default_primary \"\""); + ExecuteCommand("mp_t_default_secondary \"\""); + ExecuteCommand("mp_equipment_reset_rounds 1"); + ExecuteCommand("mp_solid_teammates 1"); + ExecuteCommand("sv_infinite_ammo 0"); + ExecuteCommand("ammo_grenade_limit_default 0"); + ExecuteCommand("ammo_grenade_limit_flashbang 0"); + ExecuteCommand("ammo_grenade_limit_total 0"); + ExecuteCommand("mp_weapons_allow_zeus 0"); + ExecuteCommand("mp_buy_anywhere 0"); + ExecuteCommand("mp_buytime 0"); + ExecuteCommand("mp_freezetime 3"); + ExecuteCommand("mp_roundtime 1.92"); + ExecuteCommand("mp_roundtime_defuse 1.92"); + ExecuteCommand("mp_maxrounds 1"); + ExecuteCommand("mp_restartgame 1"); + } + public string StartDemoRecording(string demoDirectory, string demoFileName) { try diff --git a/PugSharp/G5ApiProvider.cs b/PugSharp/G5ApiProvider.cs index b248f08..cceef5c 100644 --- a/PugSharp/G5ApiProvider.cs +++ b/PugSharp/G5ApiProvider.cs @@ -183,5 +183,28 @@ public Task FreeServerAsync(CancellationToken cancellationToken) return Task.CompletedTask; } + public Task SendKnifeRoundStartedAsync(KnifeRoundStartedParams knifeRoundStartedParams, CancellationToken cancellationToken) + { + var knifeRoundStartedEvent = new KnifeRoundStartedEvent + { + MatchId = knifeRoundStartedParams.MatchId, + MapNumber = knifeRoundStartedParams.MapNumber + }; + return _G5Stats.SendEventAsync(knifeRoundStartedEvent, cancellationToken); + } + + public Task SendKnifeRoundWonAsync(KnifeRoundWonParams knifeRoundWonParams, CancellationToken cancellationToken) + { + var knifeRoundWonEvent = new KnifeRoundWonEvent + { + MatchId = knifeRoundWonParams.MatchId, + MapNumber = knifeRoundWonParams.MapNumber, + TeamNumber = 1, // Assuming team 1 won; this would need to be calculated based on the winning side + Side = knifeRoundWonParams.WinningSide, + Swapped = knifeRoundWonParams.Swapped + }; + return _G5Stats.SendEventAsync(knifeRoundWonEvent, cancellationToken); + } + #endregion }