From 8ec0927e3358e14c51da08acd0c263b212db28ff Mon Sep 17 00:00:00 2001 From: Shobhit Pathak Date: Sat, 10 Feb 2024 21:34:32 +0530 Subject: [PATCH] v0.7.0 | feat: Rethrow in prac and much more --- ConfigConvars.cs | 25 +++ ConsoleCommands.cs | 4 +- Constants.cs | 24 +++ EventHandlers.cs | 333 +++++++++++++++++++++++++++++ Events.cs | 16 ++ GrenadeThrownData.cs | 78 +++++++ MatchManagement.cs | 53 +++-- MatchZy.cs | 281 +++++++----------------- MatchZy.csproj | 2 +- PlayerPracticeTimer.cs | 40 ++++ PracticeMode.cs | 340 ++++++++++++++++++++++++++++-- ReadySystem.cs | 52 ++++- SmokeGrenadeProjectile.cs | 29 +++ Teams.cs | 9 +- Utility.cs | 108 +++++++--- cfg/MatchZy/config.cfg | 6 + documentation/docs/match_setup.md | 2 +- 17 files changed, 1124 insertions(+), 278 deletions(-) create mode 100644 Constants.cs create mode 100644 EventHandlers.cs create mode 100644 GrenadeThrownData.cs create mode 100644 PlayerPracticeTimer.cs create mode 100644 SmokeGrenadeProjectile.cs diff --git a/ConfigConvars.cs b/ConfigConvars.cs index 0c8ad69..2c24242 100644 --- a/ConfigConvars.cs +++ b/ConfigConvars.cs @@ -224,5 +224,30 @@ public void MatchZyAutoStartConvar(CCSPlayerController? player, CommandInfo comm } + [ConsoleCommand("matchzy_allow_force_ready", "Whether force ready using !forceready is enabled or not (Currently works in Match Setup only). Default value: True")] + [ConsoleCommand("get5_allow_force_ready", "Whether force ready using !forceready is enabled or not (Currently works in Match Setup only). Default value: True")] + public void MatchZyAllowForceReadyConvar(CCSPlayerController? player, CommandInfo command) + { + if (player != null) return; + string args = command.ArgString; + + allowForceReady = bool.TryParse(args, out bool allowForceReadyValue) ? allowForceReadyValue : args != "0" && allowForceReady; + } + + [ConsoleCommand("matchzy_max_saved_last_grenades", "Maximum number of grenade history that may be saved per-map, per-client. Set to 0 to disable. Default value: 512")] + public void MatchZyMaxSavedLastGrenadesConvar(CCSPlayerController? player, CommandInfo command) + { + if (player != null) return; + string args = command.ArgString; + + if (int.TryParse(args, out int maxLastGrenadesSavedLimitValue)) + { + maxLastGrenadesSavedLimit = maxLastGrenadesSavedLimitValue; + } + else + { + command.ReplyToCommand("Usage: matchzy_max_saved_last_grenades "); + } + } } } diff --git a/ConsoleCommands.cs b/ConsoleCommands.cs index a60550e..1324f31 100644 --- a/ConsoleCommands.cs +++ b/ConsoleCommands.cs @@ -227,7 +227,7 @@ public void OnTacCommand(CCSPlayerController? player, CommandInfo? command) { [ConsoleCommand("css_roundknife", "Toggles knife round for the match")] [ConsoleCommand("css_rk", "Toggles knife round for the match")] - public void OnKifeCommand(CCSPlayerController? player, CommandInfo? command) { + public void OnKnifeCommand(CCSPlayerController? player, CommandInfo? command) { if (IsPlayerAdmin(player, "css_roundknife", "@css/config")) { isKnifeRequired = !isKnifeRequired; string knifeStatus = isKnifeRequired ? "Enabled" : "Disabled"; @@ -324,8 +324,10 @@ private void OnMapReloadCommand(CCSPlayerController? player, CommandInfo? comman } string currentMapName = Server.MapName; if (long.TryParse(currentMapName, out _)) { // Check if mapName is a long for workshop map ids + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"host_workshop_map \"{currentMapName}\""); } else if (Server.IsMapValid(currentMapName)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"changelevel \"{currentMapName}\""); } else { ReplyToUserCommand(player, "Invalid map name!"); diff --git a/Constants.cs b/Constants.cs new file mode 100644 index 0000000..15e8176 --- /dev/null +++ b/Constants.cs @@ -0,0 +1,24 @@ +namespace MatchZy; + +class Constants +{ + public static readonly Dictionary ProjectileTypeMap = + new(StringComparer.OrdinalIgnoreCase) + { + { "smokegrenade_projectile", "smoke" }, + { "flashbang_projectile", "flash" }, + { "hegrenade_projectile", "hegrenade" }, + { "decoy_projectile", "decoy" }, + { "molotov_projectile", "molotov" } + }; + + public static readonly Dictionary NadeProjectileMap = + new(StringComparer.OrdinalIgnoreCase) + { + { "smoke", "smokegrenade_projectile" }, + { "flash", "flashbang_projectile" }, + { "hegrenade", "hegrenade_projectile" }, + { "decoy", "decoy_projectile" }, + { "molotov", "molotov_projectile" } + }; +} diff --git a/EventHandlers.cs b/EventHandlers.cs new file mode 100644 index 0000000..a0a8c4e --- /dev/null +++ b/EventHandlers.cs @@ -0,0 +1,333 @@ + +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Utils; + +namespace MatchZy; +public partial class MatchZy +{ + public HookResult EventPlayerConnectFullHandler(EventPlayerConnectFull @event, GameEventInfo info) + { + try + { + Log($"[FULL CONNECT] Player ID: {@event.Userid.UserId}, Name: {@event.Userid.PlayerName} has connected!"); + CCSPlayerController player = @event.Userid; + + // Handling whitelisted players + if (!player.IsBot || !player.IsHLTV) + { + var steamId = player.SteamID; + + string whitelistfileName = "MatchZy/whitelist.cfg"; + string whitelistPath = Path.Join(Server.GameDirectory + "/csgo/cfg", whitelistfileName); + string? directoryPath = Path.GetDirectoryName(whitelistPath); + if (directoryPath != null) + { + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + if (!File.Exists(whitelistPath)) File.WriteAllLines(whitelistPath, new[] { "Steamid1", "Steamid2" }); + + var whiteList = File.ReadAllLines(whitelistPath); + + if (isWhitelistRequired == true) + { + if (!whiteList.Contains(steamId.ToString())) + { + Log($"[EventPlayerConnectFull] KICKING PLAYER STEAMID: {steamId}, Name: {player.PlayerName} (Not whitelisted!)"); + KickPlayer(player); + + return HookResult.Continue; + } + } + if (isMatchSetup || matchModeOnly) + { + CsTeam team = GetPlayerTeam(player); + Log($"[EventPlayerConnectFull] KICKING PLAYER STEAMID: {steamId}, Name: {player.PlayerName} (NOT ALLOWED!)"); + if (team == CsTeam.None) + { + KickPlayer(player); + return HookResult.Continue; + } + } + } + + if (player.UserId.HasValue) + { + playerData[player.UserId.Value] = player; + connectedPlayers++; + if (readyAvailable && !matchStarted) + { + playerReadyStatus[player.UserId.Value] = false; + } + else + { + playerReadyStatus[player.UserId.Value] = true; + } + } + // May not be required, but just to be on safe side so that player data is properly updated in dictionaries + UpdatePlayersMap(); + + if (readyAvailable && !matchStarted) + { + // Start Warmup when first player connect and match is not started. + if (GetRealPlayersCount() == 1) + { + Log($"[FULL CONNECT] First player has connected, starting warmup!"); + ExecUnpracCommands(); + AutoStart(); + } + } + return HookResult.Continue; + + } + catch (Exception e) + { + Log($"[EventPlayerConnectFull FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + + } + public HookResult EventPlayerDisconnectHandler(EventPlayerDisconnect @event, GameEventInfo info) + { + try + { + CCSPlayerController player = @event.Userid; + if (!player.UserId.HasValue) return HookResult.Continue; + int userId = player.UserId.Value; + + if (playerReadyStatus.ContainsKey(userId)) + { + playerReadyStatus.Remove(userId); + connectedPlayers--; + } + if (playerData.ContainsKey(userId)) + { + playerData.Remove(userId); + } + + if (matchzyTeam1.coach == player) + { + matchzyTeam1.coach = null; + player.Clan = ""; + } + else if (matchzyTeam2.coach == player) + { + matchzyTeam2.coach = null; + player.Clan = ""; + } + if (noFlashList.Contains(userId)) + { + noFlashList.Remove(userId); + } + lastGrenadesData.Remove(userId); + nadeSpecificLastGrenadeData.Remove(userId); + + return HookResult.Continue; + } + catch (Exception e) + { + Log($"[EventPlayerDisconnect FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + } + + public HookResult EventCsWinPanelRoundHandler(EventCsWinPanelRound @event, GameEventInfo info) + { + // EventCsWinPanelRound has stopped firing after Arms Race update, hence we handle knife round winner in EventRoundEnd. + + // Log($"[EventCsWinPanelRound PRE] finalEvent: {@event.FinalEvent}"); + // if (isKnifeRound && matchStarted) + // { + // HandleKnifeWinner(@event); + // } + return HookResult.Continue; + } + + public HookResult EventCsWinPanelMatchHandler(EventCsWinPanelMatch @event, GameEventInfo info) + { + try + { + HandleMatchEnd(); + // ResetMatch(); + return HookResult.Continue; + } + catch (Exception e) + { + Log($"[EventCsWinPanelMatch FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + } + + public HookResult EventRoundStartHandler(EventRoundStart @event, GameEventInfo info) + { + try + { + HandlePostRoundStartEvent(@event); + return HookResult.Continue; + } + catch (Exception e) + { + Log($"[EventRoundStart FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + } + + public void OnEntitySpawnedHandler(CEntityInstance entity) + { + try + { + if (entity == null || entity.Entity == null || !isPractice) return; + if (!Constants.ProjectileTypeMap.ContainsKey(entity.Entity.DesignerName)) return; + + Server.NextFrame(() => { + CBaseCSGrenadeProjectile projectile = new CBaseCSGrenadeProjectile(entity.Handle); + + if (!projectile.IsValid || + !projectile.Thrower.IsValid || + projectile.Thrower.Value == null || + projectile.Thrower.Value.Controller.Value == null || + projectile.Globalname == "custom" + ) return; + + CCSPlayerController player = new(projectile.Thrower.Value.Controller.Value.Handle); + if(!player.IsValid || player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) return; + int client = player.UserId!.Value; + + Vector position = new(projectile.AbsOrigin!.X, projectile.AbsOrigin.Y, projectile.AbsOrigin.Z); + QAngle angle = new(projectile.AbsRotation!.X, projectile.AbsRotation.Y, projectile.AbsRotation.Z); + Vector velocity = new(projectile.AbsVelocity.X, projectile.AbsVelocity.Y, projectile.AbsVelocity.Z); + string nadeType = Constants.ProjectileTypeMap[entity.Entity.DesignerName]; + + if (!lastGrenadesData.ContainsKey(client)) { + lastGrenadesData[client] = new(); + } + + if (!nadeSpecificLastGrenadeData.ContainsKey(client)) + { + nadeSpecificLastGrenadeData[client] = new(){}; + } + + GrenadeThrownData lastGrenadeThrown = new( + position, + angle, + velocity, + player.PlayerPawn.Value.CBodyComponent!.SceneNode!.AbsOrigin, + player.PlayerPawn.Value.CBodyComponent!.SceneNode!.AbsRotation, + nadeType, + DateTime.Now + ); + + nadeSpecificLastGrenadeData[client][nadeType] = lastGrenadeThrown; + lastGrenadesData[client].Add(lastGrenadeThrown); + + if (maxLastGrenadesSavedLimit != 0 && lastGrenadesData[client].Count > maxLastGrenadesSavedLimit) + { + lastGrenadesData[client].RemoveAt(0); + } + + lastGrenadeThrownTime[(int)projectile.Index] = DateTime.Now; + }); + } + catch (Exception e) + { + Log($"[OnEntitySpawnedHandler FATAL] An error occurred: {e.Message}"); + } + } + + public HookResult EventPlayerDeathPreHandler(EventPlayerDeath @event, GameEventInfo info) + { + try + { + // We do not broadcast the suicide of the coach + if (!matchStarted) return HookResult.Continue; + + if (@event.Attacker == @event.Userid) + { + if (matchzyTeam1.coach == @event.Attacker || matchzyTeam2.coach == @event.Attacker) + { + info.DontBroadcast = true; + } + } + + return HookResult.Continue; + } + catch (Exception e) + { + Log($"[EventPlayerDeathPreHandler FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + } + + public HookResult EventRoundFreezeEndHandler(EventRoundFreezeEnd @event, GameEventInfo info) + { + try + { + HandlePostRoundFreezeEndEvent(@event); + return HookResult.Continue; + } + catch (Exception e) + { + Log($"[EventRoundFreezeEnd FATAL] An error occurred: {e.Message}"); + return HookResult.Continue; + } + } + + public HookResult EventSmokegrenadeDetonateHandler(EventSmokegrenadeDetonate @event, GameEventInfo info) + { + if (!isPractice || isDryRun) return HookResult.Continue; + if(lastGrenadeThrownTime.TryGetValue(@event.Entityid, out var thrownTime)) + { + PrintToPlayerChat(@event.Userid, $"Smoke thrown by {@event.Userid.PlayerName} took {(DateTime.Now - thrownTime).TotalSeconds:0.00}s to detonate"); + lastGrenadeThrownTime.Remove(@event.Entityid); + } + return HookResult.Continue; + } + + public HookResult EventFlashbangDetonateHandler(EventFlashbangDetonate @event, GameEventInfo info) + { + if (!isPractice || isDryRun) return HookResult.Continue; + if(lastGrenadeThrownTime.TryGetValue(@event.Entityid, out var thrownTime)) + { + PrintToPlayerChat(@event.Userid, $"Flash thrown by {@event.Userid.PlayerName} took {(DateTime.Now - thrownTime).TotalSeconds:0.00}s to detonate"); + lastGrenadeThrownTime.Remove(@event.Entityid); + } + return HookResult.Continue; + } + + public HookResult EventHegrenadeDetonateHandler(EventHegrenadeDetonate @event, GameEventInfo info) + { + if (!isPractice || isDryRun) return HookResult.Continue; + if(lastGrenadeThrownTime.TryGetValue(@event.Entityid, out var thrownTime)) + { + PrintToPlayerChat(@event.Userid, $"Grenade thrown by {@event.Userid.PlayerName} took {(DateTime.Now - thrownTime).TotalSeconds:0.00}s to detonate"); + lastGrenadeThrownTime.Remove(@event.Entityid); + } + return HookResult.Continue; + } + + + public HookResult EventMolotovDetonateHandler(EventMolotovDetonate @event, GameEventInfo info) + { + if (!isPractice || isDryRun) return HookResult.Continue; + if(lastGrenadeThrownTime.TryGetValue(@event.Get("entityid"), out var thrownTime)) + { + PrintToPlayerChat(@event.Userid, $"Molotov thrown by {@event.Userid.PlayerName} took {(DateTime.Now - thrownTime).TotalSeconds:0.00}s to detonate"); + lastGrenadeThrownTime.Remove(@event.Get("entityid")); + } + return HookResult.Continue; + } + + public HookResult EventDecoyDetonateHandler(EventDecoyDetonate @event, GameEventInfo info) + { + if (!isPractice || isDryRun) return HookResult.Continue; + if(lastGrenadeThrownTime.TryGetValue(@event.Entityid, out var thrownTime)) + { + PrintToPlayerChat(@event.Userid, $"Decoy thrown by {@event.Userid.PlayerName} took {(DateTime.Now - thrownTime).TotalSeconds:0.00}s to detonate"); + lastGrenadeThrownTime.Remove(@event.Entityid); + } + return HookResult.Continue; + } +} diff --git a/Events.cs b/Events.cs index 2a480d7..0f69dff 100644 --- a/Events.cs +++ b/Events.cs @@ -103,6 +103,22 @@ public MatchZyPlayerDisconnectedEvent() : base("player_disconnect") } } +public class MatchZySeriesStartedEvent : MatchZyMatchEvent +{ + [JsonPropertyName("team1")] + public required MatchZyTeamWrapper Team1 { get; init; } + + [JsonPropertyName("team2")] + public required MatchZyTeamWrapper Team2 { get; init; } + + [JsonPropertyName("num_maps")] + public required int NumberOfMaps { get; init; } + + public MatchZySeriesStartedEvent() : base("series_start") + { + } +} + public class MatchZySeriesResultEvent : MatchZyMatchEvent { [JsonPropertyName("time_until_restore")] diff --git a/GrenadeThrownData.cs b/GrenadeThrownData.cs new file mode 100644 index 0000000..eae004b --- /dev/null +++ b/GrenadeThrownData.cs @@ -0,0 +1,78 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Utils; + +namespace MatchZy; +public class GrenadeThrownData +{ + public Vector Position { get; private set; } + + public QAngle Angle { get; private set; } + + public Vector Velocity { get; private set; } + + public Vector PlayerPosition { get; private set; } + + public QAngle PlayerAngle { get; private set; } + + public string Type { get; private set; } + + public DateTime ThrownTime { get; private set; } + + public float Delay { get; set; } + + public GrenadeThrownData(Vector nadePosition, QAngle nadeAngle, Vector nadeVelocity, Vector playerPosition, QAngle playerAngle, string grenadeType, DateTime thrownTime) + { + Position = new Vector(nadePosition.X, nadePosition.Y, nadePosition.Z); + Angle = new QAngle(nadeAngle.X, nadeAngle.Y, nadeAngle.Z); + Velocity = new Vector(nadeVelocity.X, nadeVelocity.Y, nadeVelocity.Z); + PlayerPosition = new Vector(playerPosition.X, playerPosition.Y, playerPosition.Z); + PlayerAngle = new QAngle(playerAngle.X, playerAngle.Y, playerAngle.Z); + Type = grenadeType; + ThrownTime = thrownTime; + Delay = 0; + } + + public void LoadPosition(CCSPlayerController player) + { + if (player == null || player.PlayerPawn.Value == null) return; + player.PlayerPawn.Value.Teleport(PlayerPosition, PlayerAngle, new Vector(0, 0, 0)); + } + + public void Throw(CCSPlayerController player) + { + if (Type == "smoke") + { + SmokeGrenadeProjectile.Create(Position, Angle, Velocity, player); + } + else if (Type == "flash" || Type == "hegrenade" || Type == "decoy" || Type == "molotov") + { + var entity = Utilities.CreateEntityByName(Constants.NadeProjectileMap[Type]); + if (entity == null) + { + Console.WriteLine($"[GrenadeThrownData Fatal] Failed to create entity!"); + return; + } + if (Type == "molotov") entity.SetModel("weapons/models/grenade/incendiary/weapon_incendiarygrenade.vmdl"); + entity.Elasticity = 0.33f; + entity.IsLive = false; + entity.DmgRadius = 350.0f; + entity.Damage = 99.0f; + entity.InitialPosition.X = Position.X; + entity.InitialPosition.Y = Position.Y; + entity.InitialPosition.Z = Position.Z; + entity.InitialVelocity.X = Velocity.X; + entity.InitialVelocity.Y = Velocity.Y; + entity.InitialVelocity.Z = Velocity.Z; + entity.Teleport(Position, Angle, Velocity); + entity.DispatchSpawn(); + entity.Globalname = "custom"; + entity.AcceptInput("FireUser1", player, player); + entity.AcceptInput("InitializeSpawnFromWorld"); + entity.TeamNum = player.TeamNum; + entity.Thrower.Raw = player.PlayerPawn.Raw; + entity.OriginalThrower.Raw = player.PlayerPawn.Raw; + entity.OwnerEntity.Raw = player.PlayerPawn.Raw; + } + } +} diff --git a/MatchManagement.cs b/MatchManagement.cs index ba671a2..1c81acf 100644 --- a/MatchManagement.cs +++ b/MatchManagement.cs @@ -49,6 +49,7 @@ public void LoadMatch(CCSPlayerController? player, CommandInfo command) if (player != null) return; if (isMatchSetup) { + command.ReplyToCommand($"[LoadMatch] A match is already setup with id: {liveMatchId}, cannot load a new match!"); Log($"[LoadMatch] A match is already setup with id: {liveMatchId}, cannot load a new match!"); return; } @@ -56,6 +57,7 @@ public void LoadMatch(CCSPlayerController? player, CommandInfo command) string filePath = Path.Join(Server.GameDirectory + "/csgo", fileName); if (!File.Exists(filePath)) { + command.ReplyToCommand($"[LoadMatch] Provided file does not exist! Usage: matchzy_loadmatch "); Log($"[LoadMatch] Provided file does not exist! Usage: matchzy_loadmatch "); return; } @@ -81,6 +83,7 @@ public void LoadMatchFromURL(CCSPlayerController? player, CommandInfo command) if (player != null) return; if (isMatchSetup) { + command.ReplyToCommand($"[LoadMatchDataCommand] A match is already setup with id: {liveMatchId}, cannot load a new match!"); Log($"[LoadMatchDataCommand] A match is already setup with id: {liveMatchId}, cannot load a new match!"); return; } @@ -93,6 +96,7 @@ public void LoadMatchFromURL(CCSPlayerController? player, CommandInfo command) if (!IsValidUrl(url)) { + command.ReplyToCommand($"[LoadMatchDataCommand] Invalid URL: {url}. Please provide a valid URL to load the match!"); Log($"[LoadMatchDataCommand] Invalid URL: {url}. Please provide a valid URL to load the match!"); return; } @@ -119,6 +123,7 @@ public void LoadMatchFromURL(CCSPlayerController? player, CommandInfo command) } else { + command.ReplyToCommand($"[LoadMatchFromURL] HTTP request failed with status code: {response.StatusCode}"); Log($"[LoadMatchFromURL] HTTP request failed with status code: {response.StatusCode}"); } } @@ -154,12 +159,12 @@ static string ValidateMatchJsonStructure(JObject jsonData) case "min_spectators_to_ready": case "num_maps": int numMaps; - if (!int.TryParse(jsonData[field].ToString(), out numMaps)) + if (!int.TryParse(jsonData[field]!.ToString(), out numMaps)) { return $"{field} should be an integer!"; } - if (field == "num_maps" && numMaps > jsonData["maplist"].ToObject>().Count) + if (field == "num_maps" && numMaps > jsonData["maplist"]!.ToObject>()!.Count) { return $"{field} should be equal to or greater than maplist!"; } @@ -167,7 +172,7 @@ static string ValidateMatchJsonStructure(JObject jsonData) break; case "cvars": - if (jsonData[field].Type != JTokenType.Object) + if (jsonData[field]!.Type != JTokenType.Object) { return $"{field} should be a JSON structure!"; } @@ -176,54 +181,54 @@ static string ValidateMatchJsonStructure(JObject jsonData) case "team1": case "team2": case "spectators": - if (jsonData[field].Type != JTokenType.Object) + if (jsonData[field]!.Type != JTokenType.Object) { return $"{field} should be a JSON structure!"; } - if ((field != "spectators") && (jsonData[field]["players"] == null || jsonData[field]["players"].Type != JTokenType.Object)) + if ((field != "spectators") && (jsonData[field]!["players"] == null || jsonData[field]!["players"]!.Type != JTokenType.Object)) { return $"{field} should have 'players' JSON!"; } break; case "veto_mode": - if (jsonData[field].Type != JTokenType.Array) + if (jsonData[field]!.Type != JTokenType.Array) { return $"{field} should be an Array!"; } break; case "maplist": - if (jsonData[field].Type != JTokenType.Array) + if (jsonData[field]!.Type != JTokenType.Array) { return $"{field} should be an Array!"; } - if (!jsonData[field].Any()) + if (!jsonData[field]!.Any()) { return $"{field} should contain atleast 1 map!"; } break; case "map_sides": - if (jsonData[field].Type != JTokenType.Array) + if (jsonData[field]!.Type != JTokenType.Array) { return $"{field} should be an Array!"; } string[] allowedValues = { "team1_ct", "team1_t", "team2_ct", "team2_t", "knife" }; - bool allElementsValid = jsonData[field].All(element => allowedValues.Contains(element.ToString())); + bool allElementsValid = jsonData[field]!.All(element => allowedValues.Contains(element.ToString())); if (!allElementsValid) { return $"{field} should be \"team1_ct\", \"team1_t\", or \"knife\"!"; } - if (jsonData[field].ToObject>().Count < jsonData["num_maps"].Value()) { + if (jsonData[field]!.ToObject>()!.Count < jsonData["num_maps"]!.Value()) { return $"{field} should be equal to or greater than num_maps!"; } break; case "skip_veto": case "clinch_series": - if (!bool.TryParse(jsonData[field].ToString(), out bool result)) + if (!bool.TryParse(jsonData[field]!.ToString(), out bool result)) { return $"{field} should be a boolean!"; } @@ -255,11 +260,11 @@ public bool LoadMatchFromJSON(string jsonData) JToken team2 = jsonDataObject["team2"]!; JToken maplist = jsonDataObject["maplist"]!; - if (team1["id"] != null) matchzyTeam1.id = team1["id"].ToString(); - if (team2["id"] != null) matchzyTeam2.id = team2["id"].ToString(); + if (team1["id"] != null) matchzyTeam1.id = team1["id"]!.ToString(); + if (team2["id"] != null) matchzyTeam2.id = team2["id"]!.ToString(); - matchzyTeam1.teamName = RemoveSpecialCharacters(team1["name"].ToString()); - matchzyTeam2.teamName = RemoveSpecialCharacters(team2["name"].ToString()); + matchzyTeam1.teamName = RemoveSpecialCharacters(team1["name"]!.ToString()); + matchzyTeam2.teamName = RemoveSpecialCharacters(team2["name"]!.ToString()); matchzyTeam1.teamPlayers = team1["players"]; matchzyTeam2.teamPlayers = team2["players"]; @@ -325,8 +330,10 @@ public bool LoadMatchFromJSON(string jsonData) string mapName = matchConfig.Maplist[0].ToString(); if (long.TryParse(mapName, out _)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"host_workshop_map \"{mapName}\""); } else if (Server.IsMapValid(mapName)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"changelevel \"{mapName}\""); } else { Log($"[LoadMatchFromJSON] Invalid map name: {mapName}, cannot setup match!"); @@ -354,6 +361,18 @@ public bool LoadMatchFromJSON(string jsonData) UpdatePlayersMap(); + var seriesStartedEvent = new MatchZySeriesStartedEvent + { + MatchId = liveMatchId.ToString(), + NumberOfMaps = matchConfig.NumMaps, + Team1 = new(matchzyTeam1.id, matchzyTeam1.teamName), + Team2 = new(matchzyTeam2.id, matchzyTeam2.teamName), + }; + + Task.Run(async () => { + await SendEventAsync(seriesStartedEvent); + }); + Log($"[LoadMatchFromJSON] Success with matchid: {liveMatchId}!"); return true; } @@ -396,7 +415,7 @@ public void GetCvarValues(JObject jsonDataObject) { if (jsonDataObject["cvars"] != null) { - foreach (JProperty cvarData in jsonDataObject["cvars"]) + foreach (JProperty cvarData in jsonDataObject["cvars"]!) { string cvarName = cvarData.Name; string cvarValue = cvarData.Value.ToString(); diff --git a/MatchZy.cs b/MatchZy.cs index 2f86ca1..a1242d9 100644 --- a/MatchZy.cs +++ b/MatchZy.cs @@ -106,21 +106,24 @@ public override void Load(bool hotReload) { commandActions = new Dictionary> { { ".ready", OnPlayerReady }, { ".r", OnPlayerReady }, + { ".forceready", OnForceReadyCommandCommand }, { ".unready", OnPlayerUnReady }, { ".ur", OnPlayerUnReady }, { ".stay", OnTeamStay }, { ".switch", OnTeamSwitch }, { ".swap", OnTeamSwitch }, { ".tech", OnTechCommand }, + { ".p", OnPauseCommand }, { ".pause", OnPauseCommand }, { ".unpause", OnUnpauseCommand }, + { ".up", OnUnpauseCommand }, { ".forcepause", OnForcePauseCommand }, { ".fp", OnForcePauseCommand }, { ".forceunpause", OnForceUnpauseCommand }, { ".fup", OnForceUnpauseCommand }, { ".tac", OnTacCommand }, - { ".roundknife", OnKifeCommand }, - { ".rk", OnKifeCommand }, + { ".roundknife", OnKnifeCommand }, + { ".rk", OnKnifeCommand }, { ".playout", OnPlayoutCommand }, { ".start", OnStartCommand }, { ".restart", OnRestartMatchCommand }, @@ -152,175 +155,36 @@ public override void Load(bool hotReload) { { ".ct", OnCTCommand }, { ".spec", OnSpecCommand }, { ".fas", OnFASCommand }, - { ".watchme", OnFASCommand } + { ".watchme", OnFASCommand }, + { ".last", OnLastCommand }, + { ".throw", OnRethrowCommand }, + { ".rethrow", OnRethrowCommand }, + { ".throwsmoke", OnRethrowSmokeCommand }, + { ".rethrowsmoke", OnRethrowSmokeCommand }, + { ".thrownade", OnRethrowGrenadeCommand }, + { ".rethrownade", OnRethrowGrenadeCommand }, + { ".rethrowgrenade", OnRethrowGrenadeCommand }, + { ".throwgrenade", OnRethrowGrenadeCommand }, + { ".rethrowflash", OnRethrowFlashCommand }, + { ".throwflash", OnRethrowFlashCommand }, + { ".rethrowdecoy", OnRethrowDecoyCommand }, + { ".throwdecoy", OnRethrowDecoyCommand }, + { ".throwmolotov", OnRethrowMolotovCommand }, + { ".rethrowmolotov", OnRethrowMolotovCommand } }; - RegisterEventHandler((@event, info) => { - try - { - Log($"[FULL CONNECT] Player ID: {@event.Userid.UserId}, Name: {@event.Userid.PlayerName} has connected!"); - CCSPlayerController player = @event.Userid; - - // Handling whitelisted players - if(!player.IsBot || !player.IsHLTV) - { - var steamId = player.SteamID; - - string whitelistfileName = "MatchZy/whitelist.cfg"; - string whitelistPath = Path.Join(Server.GameDirectory + "/csgo/cfg", whitelistfileName); - string? directoryPath = Path.GetDirectoryName(whitelistPath); - if (directoryPath != null) - { - if (!Directory.Exists(directoryPath)) - { - Directory.CreateDirectory(directoryPath); - } - } - if(!File.Exists(whitelistPath)) File.WriteAllLines(whitelistPath, new []{"Steamid1", "Steamid2"}); - - var whiteList = File.ReadAllLines(whitelistPath); - - if (isWhitelistRequired == true) - { - if (!whiteList.Contains(steamId.ToString())) - { - Log($"[EventPlayerConnectFull] KICKING PLAYER STEAMID: {steamId}, Name: {player.PlayerName} (Not whitelisted!)"); - Server.ExecuteCommand($"kickid {(ushort)player.UserId}"); - return HookResult.Continue; - } - } - if (isMatchSetup || matchModeOnly) { - CsTeam team = GetPlayerTeam(player); - Log($"[EventPlayerConnectFull] KICKING PLAYER STEAMID: {steamId}, Name: {player.PlayerName} (NOT ALLOWED!)"); - if (team == CsTeam.None) { - Server.ExecuteCommand($"kickid {(ushort)player.UserId}"); - return HookResult.Continue; - } - } - } - - if (player.UserId.HasValue) { - - playerData[player.UserId.Value] = player; - connectedPlayers++; - if (readyAvailable && !matchStarted) { - playerReadyStatus[player.UserId.Value] = false; - } else { - playerReadyStatus[player.UserId.Value] = true; - } - } - // May not be required, but just to be on safe side so that player data is properly updated in dictionaries - UpdatePlayersMap(); - - if (readyAvailable && !matchStarted) { - // Start Warmup when first player connect and match is not started. - if (GetRealPlayersCount() == 1) { - Log($"[FULL CONNECT] First player has connected, starting warmup!"); - ExecUnpracCommands(); - AutoStart(); - } - } - return HookResult.Continue; - - } - catch (Exception e) - { - Log($"[EventPlayerConnectFull FATAL] An error occurred: {e.Message}"); - return HookResult.Continue; - } - }); - - RegisterEventHandler((@event, info) => { - try - { - CCSPlayerController player = @event.Userid; - if (player.UserId.HasValue) { - if (playerReadyStatus.ContainsKey(player.UserId.Value)) { - playerReadyStatus.Remove(player.UserId.Value); - connectedPlayers--; - } - if (playerData.ContainsKey(player.UserId.Value)) { - playerData.Remove(player.UserId.Value); - } - - if (matchzyTeam1.coach == player) { - matchzyTeam1.coach = null; - player.Clan = ""; - } else if (matchzyTeam2.coach == player) { - matchzyTeam2.coach = null; - player.Clan = ""; - } - if (noFlashList.Contains(player.UserId.Value)) - { - noFlashList.Remove(player.UserId.Value); - } - } - - return HookResult.Continue; - } - catch (Exception e) - { - Log($"[EventPlayerDisconnect FATAL] An error occurred: {e.Message}"); - return HookResult.Continue; - } - }); - + RegisterEventHandler(EventPlayerConnectFullHandler); + RegisterEventHandler(EventPlayerDisconnectHandler); + RegisterEventHandler(EventCsWinPanelRoundHandler, hookMode: HookMode.Pre); + RegisterEventHandler(EventCsWinPanelMatchHandler); + RegisterEventHandler(EventRoundStartHandler); + RegisterEventHandler(EventPlayerDeathPreHandler, hookMode: HookMode.Pre); + RegisterEventHandler(EventRoundFreezeEndHandler); RegisterListener(playerSlot => { // May not be required, but just to be on safe side so that player data is properly updated in dictionaries UpdatePlayersMap(); }); - - RegisterEventHandler((@event, info) => { - Log($"[EventCsWinPanelRound PRE] finalEvent: {@event.FinalEvent}"); - if (isKnifeRound && matchStarted) { - HandleKnifeWinner(@event); - } - return HookResult.Continue; - }, HookMode.Pre); - - RegisterEventHandler((@event, info) => { - try - { - Log($"[EventCsWinPanelMatch]"); - HandleMatchEnd(); - // ResetMatch(); - return HookResult.Continue; - } - catch (Exception e) - { - Log($"[EventCsWinPanelMatch FATAL] An error occurred: {e.Message}"); - return HookResult.Continue; - } - - }); - - RegisterEventHandler((@event, info) => { - try - { - HandlePostRoundStartEvent(@event); - return HookResult.Continue; - } - catch (Exception e) - { - Log($"[EventRoundStart FATAL] An error occurred: {e.Message}"); - return HookResult.Continue; - } - - }); - - RegisterEventHandler((@event, info) => { - try - { - HandlePostRoundFreezeEndEvent(@event); - return HookResult.Continue; - } - catch (Exception e) - { - Log($"[EventRoundFreezeEnd FATAL] An error occurred: {e.Message}"); - return HookResult.Continue; - } - - }); + RegisterListener(OnEntitySpawnedHandler); RegisterEventHandler((@event, info) => { CCSPlayerController player = @event.Userid; @@ -343,8 +207,6 @@ public override void Load(bool hotReload) { CsTeam playerTeam = GetPlayerTeam(player); - Log($"[EventPlayerTeam] PLAYER TEAM DETERMINED: {(int)playerTeam}"); - if (@event.Team != (int)playerTeam) { if (player.IsValid) @@ -353,10 +215,6 @@ public override void Load(bool hotReload) { Server.NextFrame(() => { player.SwitchTeam(playerTeam); - // Server.NextFrame(() => - // { - // player.PlayerPawn.Value.CommitSuicide(explode: true, force: true); - // }); }); } } @@ -368,33 +226,32 @@ public override void Load(bool hotReload) { if (isMatchSetup && player != null && player.IsValid) { if (int.TryParse(info.ArgByIndex(1), out int joiningTeam)) { int playerTeam = (int)GetPlayerTeam(player); - Log($"[jointeam] PLAYER TEAM DETERMINED: PlayerName: {player.PlayerName}, PlayerTeam: {playerTeam}"); if (joiningTeam != playerTeam) { return HookResult.Stop; } } - } return HookResult.Continue; }); - RegisterEventHandler((@event, info) => { - if (isKnifeRound) { - Log($"[EventRoundEnd PRE] Winner: {@event.Winner}, Reason: {@event.Reason}"); - @event.Winner = knifeWinner; - int finalEvent = 10; - if (knifeWinner == 3) { - finalEvent = 8; - } else if (knifeWinner == 2) { - finalEvent = 9; - } - @event.Reason = finalEvent; - Log($"[EventRoundEnd Updated] Winner: {@event.Winner}, Reason: {@event.Reason}"); - isSideSelectionPhase = true; - isKnifeRound = false; - StartAfterKnifeWarmup(); - } - return HookResult.Continue; + RegisterEventHandler((@event, info) => + { + if (!isKnifeRound) return HookResult.Continue; + + DetermineKnifeWinner(); + @event.Winner = knifeWinner; + int finalEvent = 10; + if (knifeWinner == 3) { + finalEvent = 8; + } else if (knifeWinner == 2) { + finalEvent = 9; + } + @event.Reason = finalEvent; + isSideSelectionPhase = true; + isKnifeRound = false; + StartAfterKnifeWarmup(); + + return HookResult.Changed; }, HookMode.Pre); RegisterEventHandler((@event, info) => { @@ -407,7 +264,6 @@ public override void Load(bool hotReload) { return HookResult.Continue; } if (!isMatchLive) return HookResult.Continue; - Log($"[EventRoundEnd POST] Winner: {@event.Winner}, Reason: {@event.Reason}"); HandlePostRoundEndEvent(@event); return HookResult.Continue; } @@ -426,7 +282,11 @@ public override void Load(bool hotReload) { // }); RegisterListener(mapName => { - Log($"[Listeners.OnMapStart]"); + if (!isMatchSetup) + { + AutoStart(); + return; + } if (isWarmup) StartWarmup(); if (isPractice) StartPracticeMode(); }); @@ -485,7 +345,6 @@ public override void Load(bool hotReload) { index += 1; } var playerUserId = NativeAPI.GetUseridFromIndex(index); - Log($"[EventPlayerChat] UserId(Index): {index} playerUserId: {playerUserId} Message: {@event.Text}"); var originalMessage = @event.Text.Trim(); var message = @event.Text.Trim().ToLower(); @@ -550,6 +409,12 @@ public override void Load(bool hotReload) { string commandArg = message.Substring(command.Length).Trim(); HandleDeleteNadeCommand(player, commandArg); } + if (message.StartsWith(".deletenade")) + { + string command = ".deletenade"; + string commandArg = message.Substring(command.Length).Trim(); + HandleDeleteNadeCommand(player, commandArg); + } if (message.StartsWith(".importnade")) { string command = ".importnade"; @@ -633,23 +498,29 @@ public override void Load(bool hotReload) { RegisterEventHandler((@event, info) => { CCSPlayerController player = @event.Userid; - if (isPractice) + if (!isPractice) return HookResult.Continue; + + if (@event.Attacker.IsValid && player.SteamID != @event.Attacker.SteamID) { - if (player.SteamID != @event.Attacker.SteamID) - { - double roundedBlindDuration = Math.Round(@event.BlindDuration, 2); - @event.Attacker.PrintToChat($"{chatPrefix} Flashed {@event.Userid.PlayerName}. Blind time: {roundedBlindDuration} seconds"); - } - var userId = player.UserId; - if (userId != null && noFlashList.Contains((int)userId)) - { - Server.NextFrame(() => KillFlashEffect(player)); - } + double roundedBlindDuration = Math.Round(@event.BlindDuration, 2); + @event.Attacker.PrintToChat($"{chatPrefix} Flashed {@event.Userid.PlayerName}. Blind time: {roundedBlindDuration} seconds"); + } + var userId = player.UserId; + if (userId != null && noFlashList.Contains((int)userId)) + { + Server.NextFrame(() => KillFlashEffect(player)); } + return HookResult.Continue; }); - Console.WriteLine("[MatchZy LOADED] MatchZy by WD- (https://github.com/shobhit-pathak/)"); + RegisterEventHandler(EventSmokegrenadeDetonateHandler); + RegisterEventHandler(EventFlashbangDetonateHandler); + RegisterEventHandler(EventHegrenadeDetonateHandler); + RegisterEventHandler(EventMolotovDetonateHandler); + RegisterEventHandler(EventDecoyDetonateHandler); + + Console.WriteLine($"[{ModuleName} {ModuleVersion} LOADED] MatchZy by WD- (https://github.com/shobhit-pathak/)"); } } } diff --git a/MatchZy.csproj b/MatchZy.csproj index c332dd9..aafa6b7 100644 --- a/MatchZy.csproj +++ b/MatchZy.csproj @@ -7,7 +7,7 @@ - + none runtime compile; build; native; contentfiles; analyzers; buildtransitive diff --git a/PlayerPracticeTimer.cs b/PlayerPracticeTimer.cs new file mode 100644 index 0000000..14ecef6 --- /dev/null +++ b/PlayerPracticeTimer.cs @@ -0,0 +1,40 @@ +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Utils; + +namespace MatchZy; + +public enum PracticeTimerType +{ + OnMovement, + Immediate +} + +public class PlayerPracticeTimer +{ + public DateTime StartTime { get; set; } + + public PracticeTimerType TimerType { get; set; } + + public CounterStrikeSharp.API.Modules.Timers.Timer? Timer { get; set; } + + public PlayerPracticeTimer(PracticeTimerType timerType) + { + TimerType = timerType; + } + + public void DisplayTimerCenter(CCSPlayerController player) + { + player.PrintToCenter($"Timer: {GetTimerResult()}s"); + } + + public double GetTimerResult() + { + double totalSeconds = (DateTime.Now - StartTime).TotalSeconds; + return Math.Round(totalSeconds, 2); + } + + public void KillTimer() + { + Timer?.Kill(); + } +} diff --git a/PracticeMode.cs b/PracticeMode.cs index 3dd82b0..32310a3 100644 --- a/PracticeMode.cs +++ b/PracticeMode.cs @@ -8,7 +8,6 @@ using System.Text.Json; - namespace MatchZy { public class Position @@ -26,6 +25,12 @@ public Position(Vector playerPosition, QAngle playerAngle) public partial class MatchZy { + int maxLastGrenadesSavedLimit = 512; + Dictionary> lastGrenadesData = new(); + Dictionary> nadeSpecificLastGrenadeData = new(); + Dictionary lastGrenadeThrownTime = new(); + Dictionary playerTimers = new(); + public Dictionary> spawnsData = new Dictionary> { { (byte)CsTeam.CounterTerrorist, new List() }, { (byte)CsTeam.Terrorist, new List() } @@ -114,7 +119,7 @@ public void GetSpawns() private void HandleSpawnCommand(CCSPlayerController? player, string commandArg, byte teamNum, string command) { - if (!isPractice || player == null) return; + if (!isPractice || !IsPlayerValid(player)) return; if (teamNum != 2 && teamNum != 3) return; if (!string.IsNullOrWhiteSpace(commandArg)) { @@ -123,7 +128,7 @@ private void HandleSpawnCommand(CCSPlayerController? player, string commandArg, // Adjusting the spawnNumber according to the array index. spawnNumber -= 1; if (spawnsData.ContainsKey(teamNum) && spawnsData[teamNum].Count <= spawnNumber) return; - player.PlayerPawn.Value.Teleport(spawnsData[teamNum][spawnNumber].PlayerPosition, spawnsData[teamNum][spawnNumber].PlayerAngle, new Vector(0, 0, 0)); + player!.PlayerPawn.Value!.Teleport(spawnsData[teamNum][spawnNumber].PlayerPosition, spawnsData[teamNum][spawnNumber].PlayerAngle, new Vector(0, 0, 0)); ReplyToUserCommand(player, $"Moved to spawn: {spawnNumber+1}/{spawnsData[teamNum].Count}"); } else @@ -161,7 +166,7 @@ private string GetNadeType(string nadeName) private void HandleSaveNadeCommand(CCSPlayerController? player, string saveNadeName) { - if (!isPractice || player == null) return; + if (!isPractice || !IsPlayerValid(player)) return; if (!string.IsNullOrWhiteSpace(saveNadeName)) { @@ -174,17 +179,17 @@ private void HandleSaveNadeCommand(CCSPlayerController? player, string saveNadeN string playerSteamID; if(isSaveNadesAsGlobalEnabled == false) { - playerSteamID = player.SteamID.ToString(); + playerSteamID = player!.SteamID.ToString(); } else { playerSteamID = "default"; } - QAngle playerAngle = player.PlayerPawn.Value.EyeAngles; - Vector playerPos = player.Pawn.Value.CBodyComponent!.SceneNode.AbsOrigin; + QAngle playerAngle = player!.PlayerPawn.Value!.EyeAngles; + Vector playerPos = player.Pawn.Value!.CBodyComponent!.SceneNode!.AbsOrigin; string currentMapName = Server.MapName; - string nadeType = GetNadeType(player.PlayerPawn.Value.WeaponServices.ActiveWeapon.Value.DesignerName); + string nadeType = GetNadeType(player.PlayerPawn.Value.WeaponServices!.ActiveWeapon.Value!.DesignerName); // Define the file path string savednadesfileName = "MatchZy/savednades.json"; @@ -730,8 +735,7 @@ private void AddBot(CCSPlayerController? player, bool crouch) } isSpawningBot = true; // !bot/.bot command is made using a lot of workarounds, as there is no direct way to create a bot entity and spawn it in CSSharp - // Hence there can be some issues with this approach. This will be revamped when we will be able to create entities and manipulate them. - // Todo: Now its possible to create entities in CSSharp, hence this approach needs to be improved. + // Hence there can be some issues with this approach. This will be revamped when we will be able to fake clients. if (player.TeamNum == (byte)CsTeam.CounterTerrorist) { Server.ExecuteCommand("bot_join_team T"); @@ -814,9 +818,9 @@ public void TemporarilyDisableCollisions(CCSPlayerController p1, CCSPlayerContro { Log($"[TemporarilyDisableCollisions] Disabling {p1.PlayerName} {p2.PlayerName}"); // Reference collision code: https://github.com/Source2ZE/CS2Fixes/blob/f009e399ff23a81915e5a2b2afda20da2ba93ada/src/events.cpp#L150 - p1.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; + p1.PlayerPawn.Value!.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; p1.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; - p2.PlayerPawn.Value.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; + p2.PlayerPawn.Value!.Collision.CollisionAttribute.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; p2.PlayerPawn.Value.Collision.CollisionGroup = (byte)CollisionGroup.COLLISION_GROUP_DEBRIS; // TODO: call CollisionRulesChanged var p1p = p1.PlayerPawn; @@ -870,6 +874,15 @@ private static void ElevatePlayer(CCSPlayerController? player) public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { var player = @event.Userid; + if (!IsPlayerValid(player)) return HookResult.Continue; + + if (matchStarted && (matchzyTeam1.coach == player || matchzyTeam2.coach == player)) + { + player.InGameMoneyServices!.Account = 0; + + Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices"); + return HookResult.Continue; + } // Respawing a bot where it was actually spawned during practice session if (isPractice && player.IsValid && player.IsBot && player.UserId.HasValue) @@ -878,11 +891,11 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) { if (pracUsedBots[player.UserId.Value]["position"] is Position botPosition) { - player.PlayerPawn.Value.Teleport(botPosition.PlayerPosition, botPosition.PlayerAngle, new Vector(0, 0, 0)); + player.PlayerPawn.Value?.Teleport(botPosition.PlayerPosition, botPosition.PlayerAngle, new Vector(0, 0, 0)); bool isCrouched = (bool)pracUsedBots[player.UserId.Value]["crouchstate"]; if (isCrouched) { - player.PlayerPawn.Value.Flags |= (uint)PlayerFlags.FL_DUCKING; + player.PlayerPawn.Value!.Flags |= (uint)PlayerFlags.FL_DUCKING; CCSPlayer_MovementServices movementService = new(player.PlayerPawn.Value.MovementServices!.Handle); AddTimer(0.1f, () => movementService.DuckAmount = 1); AddTimer(0.2f, () => player.PlayerPawn.Value.Bot.IsCrouching = true); @@ -907,7 +920,6 @@ public HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info) } } - return HookResult.Continue; } @@ -927,8 +939,9 @@ public void OnFFCommand(CCSPlayerController? player, CommandInfo? command) Dictionary preFastForwardMoveTypes = new(); foreach (var key in playerData.Keys) { - preFastForwardMoveTypes[key] = playerData[key].PlayerPawn.Value.MoveType; - playerData[key].PlayerPawn.Value.MoveType = MoveType_t.MOVETYPE_NONE; + if(!IsPlayerValid(playerData[key])) continue; + preFastForwardMoveTypes[key] = playerData[key].PlayerPawn.Value!.MoveType; + playerData[key].PlayerPawn.Value!.MoveType = MoveType_t.MOVETYPE_NONE; } Server.PrintToChatAll($"{chatPrefix} Fastforwarding 20 seconds!"); @@ -949,7 +962,8 @@ public void ResetFastForward(Dictionary preFastForwardMoveTypes if (!isPractice) return; Server.ExecuteCommand("host_timescale 1"); foreach (var key in playerData.Keys) { - playerData[key].PlayerPawn.Value.MoveType = preFastForwardMoveTypes[key]; + if(!IsPlayerValid(playerData[key])) continue; + playerData[key].PlayerPawn.Value!.MoveType = preFastForwardMoveTypes[key]; } } @@ -1093,5 +1107,295 @@ public void ExecUnpracCommands() { Server.ExecuteCommand("mp_death_drop_breachcharge true; mp_death_drop_defuser true; mp_death_drop_taser true; mp_drop_knife_enable false; mp_death_drop_grenade 2; ammo_grenade_limit_total 4; mp_defuser_allocation 0; sv_infinite_ammo 0; mp_force_pick_time 15"); } + public bool IsValidPositionForLastGrenade(CCSPlayerController player, int position) + { + int userId = player.UserId!.Value; + if (!lastGrenadesData.ContainsKey(userId) || lastGrenadesData[userId].Count <= 0) + { + PrintToPlayerChat(player, $"You have not thrown any nade yet!"); + return false; + } + + if (lastGrenadesData[userId].Count < position) + { + PrintToPlayerChat(player, $"Your grenade history only goes from 1 to {lastGrenadesData[userId].Count}!"); + return false; + } + + return true; + } + + public void RethrowSpecificNade(CCSPlayerController player, string nadeType) + { + if (!isPractice || !player.UserId.HasValue) return; + int userId = player.UserId.Value; + if (!nadeSpecificLastGrenadeData.ContainsKey(userId) || !nadeSpecificLastGrenadeData[userId].ContainsKey(nadeType)) + { + PrintToPlayerChat(player, $"You have not thrown any {nadeType} yet!"); + return; + } + GrenadeThrownData grenadeThrown = nadeSpecificLastGrenadeData[userId][nadeType]; + AddTimer(grenadeThrown.Delay, () => grenadeThrown.Throw(player)); + } + + public void HandleBackCommand(CCSPlayerController player, string number) + { + if (!isPractice || player == null || !player.UserId.HasValue) return; + int userId = player.UserId.Value; + if (!string.IsNullOrWhiteSpace(number)) + { + if (int.TryParse(number, out int positionNumber) && positionNumber >= 1) + { + if (IsValidPositionForLastGrenade(player, positionNumber)) + { + positionNumber -= 1; + lastGrenadesData[userId][positionNumber].LoadPosition(player); + PrintToPlayerChat(player, $"Teleported to grenade of history position: {positionNumber+1}/{lastGrenadesData[userId].Count}"); + } + } + else + { + PrintToPlayerChat(player, $"Invalid value for !back command. Please specify a valid non-negative number. Usage: !back "); + return; + } + } + else + { + int thrownCount = lastGrenadesData.ContainsKey(userId) ? lastGrenadesData[userId].Count : 0; + ReplyToUserCommand(player, $"Usage: !back (You've thrown {thrownCount} grenades till now)"); + } + } + + public void HandleThrowIndexCommand(CCSPlayerController player, string argString) + { + if (!isPractice || !IsPlayerValid(player)) return; + int userId = player!.UserId!.Value; + + string[] argsList = argString.Split(); + + foreach (string arg in argsList) + { + if (int.TryParse(arg, out int positionNumber) && positionNumber >= 1) + { + if (IsValidPositionForLastGrenade(player, positionNumber)) + { + positionNumber -= 1; + GrenadeThrownData grenadeThrown = lastGrenadesData[userId][positionNumber]; + AddTimer(grenadeThrown.Delay, () => grenadeThrown.Throw(player)); + PrintToPlayerChat(player, $"Throwing grenade of history position: {positionNumber+1}/{lastGrenadesData[userId].Count}"); + } + } + else + { + PrintToPlayerChat(player, $"'{arg}' is not a valid non-negative number for !throwindex command."); + } + } + } + + public void HandleDelayCommand(CCSPlayerController player, string delay) + { + if (!isPractice || !IsPlayerValid(player)) return; + + if (!isPractice || player == null || !player.UserId.HasValue) return; + int userId = player.UserId.Value; + if (string.IsNullOrWhiteSpace(delay)) + { + ReplyToUserCommand(player, $"Usage: !delay "); + return; + } + + if (float.TryParse(delay, out float delayInSeconds) && delayInSeconds > 0) + { + if (IsValidPositionForLastGrenade(player, 0)) + { + lastGrenadesData[userId].Last().Delay = delayInSeconds; + PrintToPlayerChat(player, $"Delay of {delayInSeconds:0.00}s set for grenade of index: {lastGrenadesData[userId].Count}."); + } + } + else + { + PrintToPlayerChat(player, $"Delay should be valid float number and greater than 0 seconds."); + return; + } + } + + public void DisplayPracticeTimerCenter(int userId) + { + if (!playerData.ContainsKey(userId) || !playerTimers.ContainsKey(userId)) return; + if (!IsPlayerValid(playerData[userId])) return; + playerTimers[userId].DisplayTimerCenter(playerData[userId]); + } + + [ConsoleCommand("css_throw", "Throws the last thrown grenade")] + [ConsoleCommand("css_rethrow", "Throws the last thrown grenade")] + public void OnRethrowCommand(CCSPlayerController? player, CommandInfo? command) + { + + if (!isPractice || player == null || !player.UserId.HasValue) return; + int userId = player.UserId.Value; + if (!lastGrenadesData.ContainsKey(userId) || lastGrenadesData[userId].Count <= 0) + { + PrintToPlayerChat(player, $"You have not thrown any nade yet!"); + return; + } + GrenadeThrownData lastGrenade = lastGrenadesData[userId].Last(); + AddTimer(lastGrenade.Delay, () => lastGrenade.Throw(player)); + } + + [ConsoleCommand("css_throwsmoke", "Throws the last thrown smoke")] + [ConsoleCommand("css_rethrowsmoke", "Throws the last thrown smoke")] + public void OnRethrowSmokeCommand(CCSPlayerController? player, CommandInfo? command) + { + if (player == null) return; + RethrowSpecificNade(player, "smoke"); + } + + [ConsoleCommand("css_throwflash", "Throws the last thrown flash")] + [ConsoleCommand("css_rethrowflash", "Throws the last thrown flash")] + public void OnRethrowFlashCommand(CCSPlayerController? player, CommandInfo? command) + { + if (player == null) return; + RethrowSpecificNade(player, "flash"); + } + + [ConsoleCommand("css_throwgrenade", "Throws the last thrown he grenade")] + [ConsoleCommand("css_rethrowgrenade", "Throws the last thrown he grenade")] + [ConsoleCommand("css_thrownade", "Throws the last thrown he grenade")] + [ConsoleCommand("css_rethrownade", "Throws the last thrown he grenade")] + public void OnRethrowGrenadeCommand(CCSPlayerController? player, CommandInfo? command) + { + if (player == null) return; + RethrowSpecificNade(player, "hegrenade"); + } + + [ConsoleCommand("css_throwmolotov", "Throws the last thrown molotov")] + [ConsoleCommand("css_rethrowmolotov", "Throws the last thrown molotov")] + public void OnRethrowMolotovCommand(CCSPlayerController? player, CommandInfo? command) + { + if (player == null) return; + RethrowSpecificNade(player, "molotov"); + } + + [ConsoleCommand("css_throwdecoy", "Throws the last thrown decoy")] + [ConsoleCommand("css_rethrowdecoy", "Throws the last thrown decoy")] + public void OnRethrowDecoyCommand(CCSPlayerController? player, CommandInfo? command) + { + if (player == null) return; + RethrowSpecificNade(player, "decoy"); + } + + [ConsoleCommand("css_last", "Teleports to the last thrown grenade position")] + public void OnLastCommand(CCSPlayerController? player, CommandInfo? command) + { + if (!isPractice || player == null || !player.UserId.HasValue) return; + int userId = player.UserId.Value; + if (!lastGrenadesData.ContainsKey(userId) || lastGrenadesData[userId].Count <= 0) + { + PrintToPlayerChat(player, $"You have not thrown any nade yet!"); + return; + } + lastGrenadesData[userId].Last().LoadPosition(player); + } + + [ConsoleCommand("css_back", "Teleports to the provided position in grenade thrown history")] + public void OnBackCommand(CCSPlayerController? player, CommandInfo command) + { + if (!isPractice || player == null || !player.UserId.HasValue) return; + if (command.ArgCount >= 2) + { + string commandArg = command.ArgByIndex(1); + HandleBackCommand(player, commandArg); + } + else + { + int userId = player!.UserId!.Value; + int thrownCount = lastGrenadesData.ContainsKey(userId) ? lastGrenadesData[userId].Count : 0; + ReplyToUserCommand(player, $"Usage: !back (You've thrown {thrownCount} grenades till now)"); + } + } + + [ConsoleCommand("css_throwidx", "Throws grenade of provided position in grenade thrown history")] + [ConsoleCommand("css_throwindex", "Throws grenade of provided position in grenade thrown history")] + public void OnThrowIndexCommand(CCSPlayerController? player, CommandInfo command) + { + if (!isPractice || !IsPlayerValid(player)) return; + if (command.ArgCount >= 2) + { + HandleThrowIndexCommand(player!, command.ArgString); + } + else + { + int userId = player!.UserId!.Value; + int thrownCount = lastGrenadesData.ContainsKey(userId) ? lastGrenadesData[userId].Count : 0; + ReplyToUserCommand(player, $"Usage: !throwindex (You've thrown {thrownCount} grenades till now)"); + } + } + + [ConsoleCommand("css_lastindex", "Returns index of the last thrown grenade")] + public void OnLastIndexCommand(CCSPlayerController? player, CommandInfo command) + { + if (!isPractice || !IsPlayerValid(player)) return; + if (IsValidPositionForLastGrenade(player!, 1)) + { + PrintToPlayerChat(player!, $"Index of last thrown grenade: {lastGrenadesData[player!.UserId!.Value].Count}"); + } + } + + [ConsoleCommand("css_delay", "Adds a delay to the last thrown grenade. Usage: !delay ")] + public void OnDelayCommand(CCSPlayerController? player, CommandInfo command) + { + if (!isPractice || !IsPlayerValid(player)) return; + if (command.ArgCount >= 2) + { + HandleDelayCommand(player!, command.ArgByIndex(1)); + } + else + { + ReplyToUserCommand(player, $"Usage: !delay "); + } + } + + [ConsoleCommand("css_timer", "Starts a timer, use .timer again to stop it.")] + public void OnTimerCommand(CCSPlayerController? player, CommandInfo command) + { + if (!isPractice || !IsPlayerValid(player)) return; + int userId = player!.UserId!.Value; + if (playerTimers.ContainsKey(userId)) + { + playerTimers[userId].KillTimer(); + double timerResult = playerTimers[userId].GetTimerResult(); + player.PrintToCenter($"Timer: {timerResult}s"); + PrintToPlayerChat(player, $"Timer stopped! Result: {timerResult}s"); + playerTimers.Remove(userId); + } + else + { + playerTimers[userId] = new PlayerPracticeTimer(PracticeTimerType.OnMovement) + { + StartTime = DateTime.Now, + Timer = AddTimer(0.1f, () => DisplayPracticeTimerCenter(userId), TimerFlags.REPEAT) + }; + PrintToPlayerChat(player, $"Timer started! User !timer to stop it."); + } + } + + // Todo: Implement timer2 when we have OnPlayerRunCmd in CS#. Using OnTick would be its alternative, it would be very expensive and not worth it. + // [ConsoleCommand("css_timer2", "Starts a timer, use .timer2 again to stop it.")] + // public void OnTimer2Command(CCSPlayerController? player, CommandInfo command) + // { + // if (!isPractice || !IsPlayerValid(player)) return; + // int userId = player!.UserId!.Value; + // if (playerTimers.ContainsKey(userId)) + // { + // PrintToPlayerChat(player, $"Timer stopped! Result: {playerTimers[userId].GetTimerResult()}s"); + // playerTimers[userId].KillTimer(); + // playerTimers.Remove(userId); + // } + // else + // { + // playerTimers[userId] = new PlayerPracticeTimer(PracticeTimerType.OnMovement); + // PrintToPlayerChat(player, $"When you start moving a timer will run until you stop moving."); + // } + // } } } diff --git a/ReadySystem.cs b/ReadySystem.cs index 50ffc80..0e5b030 100644 --- a/ReadySystem.cs +++ b/ReadySystem.cs @@ -1,9 +1,19 @@ +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Core.Attributes.Registration; +using CounterStrikeSharp.API.Modules.Commands; using CounterStrikeSharp.API.Modules.Utils; namespace MatchZy; public partial class MatchZy { + public Dictionary teamReadyOverride = new() { + {CsTeam.Terrorist, false}, + {CsTeam.CounterTerrorist, false}, + {CsTeam.Spectator, false} + }; + + public bool allowForceReady = true; public bool IsTeamsReady() { @@ -41,12 +51,10 @@ public bool IsTeamReady(int team) return true; } - // Todo: Implement Force ready system - - // if (IsTeamForcedReady(team) && readyCount >= minReady) - // { - // return true; - // } + if (IsTeamForcedReady((CsTeam)team) && readyCount >= minReady) + { + return true; + } return false; } @@ -79,4 +87,36 @@ public int GetTeamMinReady(int team) } return (playerCount, readyCount); } + + public bool IsTeamForcedReady(CsTeam team) { + return teamReadyOverride[team]; + } + + [ConsoleCommand("css_forceready", "Force-readies the team")] + public void OnForceReadyCommandCommand(CCSPlayerController? player, CommandInfo? command) + { + Log($"{readyAvailable} {isMatchSetup} {allowForceReady} {IsPlayerValid(player)}"); + if (!readyAvailable || !isMatchSetup || !allowForceReady || !IsPlayerValid(player)) return; + + int minReady = GetTeamMinReady(player!.TeamNum); + (int playerCount, int readyCount) = GetTeamPlayerCount(player!.TeamNum, false); + + if (playerCount < minReady) + { + ReplyToUserCommand(player, $"You must have at least {minReady} player(s) on the server to ready up."); + return; + } + + foreach (var key in playerData.Keys) + { + if (!playerData[key].IsValid) continue; + if (playerData[key].TeamNum == player.TeamNum) { + playerReadyStatus[key] = true; + ReplyToUserCommand(playerData[key], $"Your team was force-readied by {player.PlayerName}"); + } + } + + teamReadyOverride[(CsTeam)player.TeamNum] = true; + CheckLiveRequired(); + } } diff --git a/SmokeGrenadeProjectile.cs b/SmokeGrenadeProjectile.cs new file mode 100644 index 0000000..e8f2d9f --- /dev/null +++ b/SmokeGrenadeProjectile.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; +using CounterStrikeSharp.API.Modules.Utils; + +namespace MatchZy; + +public class SmokeGrenadeProjectile +{ + public static string smokeGrenadeProjectileWindowsSig = @"\x48\x89\x5C\x24\x08\x48\x89\x6C\x24\x10\x48\x89\x74\x24\x18\x57\x41\x56\x41\x57\x48\x83\xEC\x50\x4C\x8B\xB4\x24\x90\x00\x00\x00\x49\x8B\xF8"; + public static string smokeGrenadeProjectileLinuxSig = @"\x55\x4c\x89\xc1\x48\x89\xe5\x41\x57\x41\x56\x49\x89\xd6\x48\x89\xf2\x48\x89\xfe\x41\x55\x45\x89\xcd\x41\x54\x4d\x89\xc4\x53\x48\x83\xec\x28\x48\x89\x7d\xb8\x48"; + + public static string smokeGrenadeProjectileSig = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? smokeGrenadeProjectileLinuxSig : smokeGrenadeProjectileWindowsSig; + public static MemoryFunctionWithReturn CSmokeGrenadeProjectile_CreateFunc = new(smokeGrenadeProjectileSig); + + public static nint Create(Vector position, QAngle angle, Vector velocity, CCSPlayerController player) + { + return CSmokeGrenadeProjectile_CreateFunc.Invoke( + position.Handle, + angle.Handle, + velocity.Handle, + velocity.Handle, + IntPtr.Zero, + 45, + player.TeamNum + ); + } + +} \ No newline at end of file diff --git a/Teams.cs b/Teams.cs index 25a9a7c..fbd1a4b 100644 --- a/Teams.cs +++ b/Teams.cs @@ -25,9 +25,8 @@ public class Team public partial class MatchZy { [ConsoleCommand("css_coach", "Sets coach for the requested team")] - public void OnCoachCommand(CCSPlayerController? player, CommandInfo? command) + public void OnCoachCommand(CCSPlayerController? player, CommandInfo command) { - Log($"[OnCoachCommand]"); HandleCoachCommand(player, command.ArgString); } @@ -162,9 +161,9 @@ public void HandleCoaches() coach.InGameMoneyServices!.Account = 0; AddTimer(0.5f, () => HandleCoachTeam(coach, true)); // AddTimer(1, () => { - // Server.ExecuteCommand("mp_suicide_penalty 0; mp_death_drop_gun 0"); - // coach.PlayerPawn.Value.CommitSuicide(false, true); - // Server.ExecuteCommand("mp_suicide_penalty 1; mp_death_drop_gun 1"); + // Server.ExecuteCommand("mp_suicide_penalty 0; mp_death_drop_gun 0"); + // coach.PlayerPawn.Value.CommitSuicide(false, true); + // Server.ExecuteCommand("mp_suicide_penalty 1; mp_death_drop_gun 1"); // }); coach.ActionTrackingServices!.MatchStats.Kills = 0; coach.ActionTrackingServices!.MatchStats.Deaths = 0; diff --git a/Utility.cs b/Utility.cs index 24290b3..a23784e 100644 --- a/Utility.cs +++ b/Utility.cs @@ -20,6 +20,16 @@ public partial class MatchZy public const string knifeCfgPath = "MatchZy/knife.cfg"; public const string liveCfgPath = "MatchZy/live.cfg"; + private void PrintToAllChat(string message) + { + Server.PrintToChatAll($"{chatPrefix} {message}"); + } + + private void PrintToPlayerChat(CCSPlayerController player, string message) + { + player.PrintToChat($"{chatPrefix} {message}"); + } + private void LoadAdmins() { string fileName = "MatchZy/admins.json"; string filePath = Path.Join(Server.GameDirectory + "/csgo/cfg", fileName); @@ -275,11 +285,12 @@ private void KillPhaseTimers() { foreach (var key in playerData.Keys) { if (team == 2 && reverseTeamSides["TERRORIST"].coach == playerData[key]) continue; if (team == 3 && reverseTeamSides["CT"].coach == playerData[key]) continue; + if (!IsPlayerValid(playerData[key])) continue; if (playerData[key].PlayerPawn == null) continue; - if (!playerData[key].PlayerPawn.IsValid) continue; + if (!playerData[key].PlayerPawn.IsValid || playerData[key].PlayerPawn.Value == null) continue; if (playerData[key].TeamNum == team) { - if (playerData[key].PlayerPawn.Value.Health > 0) count++; - totalHealth += playerData[key].PlayerPawn.Value.Health; + if (playerData[key].PlayerPawn.Value!.Health > 0) count++; + totalHealth += playerData[key].PlayerPawn.Value!.Health; } } return (count, totalHealth); @@ -316,6 +327,13 @@ private void ResetMatch(bool warmupCfgRequired = true) playerReadyStatus[key] = false; } + teamReadyOverride = new() + { + {CsTeam.Terrorist, false}, + {CsTeam.CounterTerrorist, false}, + {CsTeam.Spectator, false} + }; + HandleClanTags(); // Reset unpause data @@ -333,6 +351,8 @@ private void ResetMatch(bool warmupCfgRequired = true) // Reset owned bots data pracUsedBots = new Dictionary>(); noFlashList = new(); + lastGrenadesData = new(); + nadeSpecificLastGrenadeData = new(); UnpauseMatch(); matchzyTeam1.teamName = "COUNTER-TERRORISTS"; @@ -394,7 +414,7 @@ private void UpdatePlayersMap() { if (isMatchSetup || matchModeOnly) { CsTeam team = GetPlayerTeam(player); - if (team == CsTeam.None) { + if (team == CsTeam.None && player.UserId.HasValue) { Server.ExecuteCommand($"kickid {(ushort)player.UserId}"); continue; } @@ -432,7 +452,8 @@ private void UpdatePlayersMap() { } } - private void HandleKnifeWinner(EventCsWinPanelRound @event) { + public void DetermineKnifeWinner() + { // Knife Round code referred from Get5, thanks to the Get5 team for their amazing job! (int tAlive, int tHealth) = GetAlivePlayers(2); (int ctAlive, int ctHealth) = GetAlivePlayers(3); @@ -447,10 +468,14 @@ private void HandleKnifeWinner(EventCsWinPanelRound @event) { knifeWinner = 2; } else { // Choosing a winner randomly - Random random = new Random(); + Random random = new(); knifeWinner = random.Next(2, 4); } + } + private void HandleKnifeWinner(EventCsWinPanelRound @event) + { + DetermineKnifeWinner(); // Below code is working partially (Winner audio plays correctly for knife winner team, but may display round winner incorrectly) // Hence we restart the game with StartAfterKnifeWarmup and allow the winning team to choose side @@ -479,16 +504,18 @@ private void HandleMapChangeCommand(CCSPlayerController? player, string mapName) } if (matchStarted) { - player.PrintToChat($"{chatPrefix} Map cannot be changed once the match is started!"); + ReplyToUserCommand(player, $"Map cannot be changed once the match is started!"); return; } if (long.TryParse(mapName, out _)) { // Check if mapName is a long for workshop map ids + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"host_workshop_map \"{mapName}\""); } else if (Server.IsMapValid(mapName)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"changelevel \"{mapName}\""); } else { - player.PrintToChat($"{chatPrefix} Invalid map name!"); + ReplyToUserCommand(player, $"Invalid map name!"); } } @@ -733,23 +760,15 @@ private void ChangeMap(string mapName, float delay) Log($"[ChangeMap] Changing map to {mapName} with delay {delay}"); AddTimer(delay, () => { if (long.TryParse(mapName, out _)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"host_workshop_map \"{mapName}\""); } else if (Server.IsMapValid(mapName)) { + Server.ExecuteCommand($"bot_kick"); Server.ExecuteCommand($"changelevel \"{mapName}\""); } }); } - private void ChangeMapOnMatchEnd() { - ResetMatch(); - string mapName = Server.MapName; - if (long.TryParse(mapName, out _)) { - Server.ExecuteCommand($"host_workshop_map \"{mapName}\""); - } else if (Server.IsMapValid(mapName)) { - Server.ExecuteCommand($"changelevel \"{mapName}\""); - } - } - private string GetMatchWinnerName() { (int t1score, int t2score) = GetTeamsScore(); if (t1score > t2score) { @@ -791,7 +810,8 @@ public void HandlePostRoundStartEvent(EventRoundStart @event) { public void HandlePostRoundFreezeEndEvent(EventRoundFreezeEnd @event) { - List coaches = new List + if (!matchStarted) return; + List coaches = new() { matchzyTeam1.coach, matchzyTeam2.coach @@ -799,12 +819,26 @@ public void HandlePostRoundFreezeEndEvent(EventRoundFreezeEnd @event) foreach (var coach in coaches) { - if (coach == null) continue; - AddTimer(1.0f, () => HandleCoachTeam(coach)); + if (!IsPlayerValid(coach)) continue; + // foreach (var weapon in coach!.PlayerPawn.Value!.WeaponServices!.MyWeapons) + // { + // if (weapon is { IsValid: true, Value.IsValid: true }) + // { + // if (weapon.Value.DesignerName.Contains("bayonet") || weapon.Value.DesignerName.Contains("knife")) + // { + // continue; + // } + // weapon.Value.Remove(); + // } + // } + + coach!.ChangeTeam(CsTeam.Spectator); + AddTimer(1, () => HandleCoachTeam(coach, false)); + // HandleCoachTeam(coach, false, true); } } - private void HandleCoachTeam(CCSPlayerController playerController, bool isFreezeTime = false) + private void HandleCoachTeam(CCSPlayerController playerController, bool isFreezeTime = false, bool suicide = false) { CsTeam oldTeam = CsTeam.Spectator; if (matchzyTeam1.coach == playerController) { @@ -826,6 +860,14 @@ private void HandleCoachTeam(CCSPlayerController playerController, bool isFreeze playerController.ChangeTeam(oldTeam); } if (playerController.InGameMoneyServices != null) playerController.InGameMoneyServices.Account = 0; + if (suicide && playerController.PlayerPawn.IsValid && playerController.PlayerPawn.Value != null) + { + bool suicidePenalty = ConVar.Find("mp_suicide_penalty")!.GetPrimitiveValue(); + int deathDropGunEnabled = ConVar.Find("mp_death_drop_gun")!.GetPrimitiveValue(); + Server.ExecuteCommand("mp_suicide_penalty 0; mp_death_drop_gun 0"); + playerController.PlayerPawn.Value.CommitSuicide(explode: false, force: true); + Server.ExecuteCommand($"mp_suicide_penalty {suicidePenalty}; mp_death_drop_gun {deathDropGunEnabled}"); + } } private void HandlePostRoundEndEvent(EventRoundEnd @event) { @@ -1160,9 +1202,9 @@ public void WriteClientNamesInFile(StringBuilder sb, JToken? players) static bool IsValidUrl(string url) { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri result)) + if (Uri.TryCreate(url, UriKind.Absolute, out Uri? result)) { - return result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps; + return result != null && (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps); } return false; } @@ -1474,5 +1516,23 @@ public bool IsMapReloadRequiredForGameMode(bool wingman) } return false; } + + public void KickPlayer(CCSPlayerController player) + { + if (player.UserId.HasValue) + { + Server.ExecuteCommand($"kickid {(ushort)player.UserId}"); + } + } + + public bool IsPlayerValid(CCSPlayerController? player) + { + return ( + player != null && + player.IsValid && + player.PlayerPawn.IsValid && + player.PlayerPawn.Value != null + ); + } } } diff --git a/cfg/MatchZy/config.cfg b/cfg/MatchZy/config.cfg index 43347b6..d23499b 100644 --- a/cfg/MatchZy/config.cfg +++ b/cfg/MatchZy/config.cfg @@ -72,3 +72,9 @@ matchzy_autostart_mode 1 // Whether nades should be saved globally instead of being privated to players by default or not. Default value: false matchzy_save_nades_as_global_enabled false + +// Whether force ready using !forceready is enabled or not (Currently works in Match Setup only). Default value: True +matchzy_allow_force_ready true + +// Maximum number of grenade history that may be saved per-map, per-client. Set to 0 to disable. Default value: 512 +matchzy_max_saved_last_grenades 512 diff --git a/documentation/docs/match_setup.md b/documentation/docs/match_setup.md index 2ba4803..012ea3c 100644 --- a/documentation/docs/match_setup.md +++ b/documentation/docs/match_setup.md @@ -57,7 +57,7 @@ There are 2 commands available which can be used to load a match: } }, "clinch_series": true, - "min_players_to_ready": 2, + "players_per_team": 5, "cvars": { "hostname": "MatchZy: Astralis vs NaVi #27", "mp_friendlyfire": "0"