using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Events; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.UserMessages; using CounterStrikeSharp.API.Modules.Utils; using src.player.skills; using src.utils; using System.Collections.Concurrent; using static CounterStrikeSharp.API.Core.Listeners; using static src.jRandomSkills; using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; namespace src.player { public static partial class Event { private static bool IsVip(CCSPlayerController? player) { if (player == null || !player.IsValid || player.IsBot) return false; string flag = Config.LoadedConfig.VIPFlag; if (string.IsNullOrWhiteSpace(flag)) return false; return HasPermission(player, flag); } private static bool HasPermission(CCSPlayerController player, string permission) { try { return AdminManager.PlayerHasPermissions(player, permission); } catch { return false; } } private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List candidates, Dictionary assignmentCounts, Config.GameModes gameMode, bool vip) { if (candidates == null || candidates.Count == 0) return noneSkill; bool ignoreMax = gameMode == Config.GameModes.SameSkills || gameMode == Config.GameModes.TeamSkills; const int attempts = 6; var filtered = new List(candidates.Count); for (int attempt = 0; attempt < attempts; attempt++) { var (roll, rolled) = RarityManager.RollRarity(vip); string rolledName = rolled.ToString(); filtered.Clear(); foreach (var s in candidates) { if (s == null) continue; var def = SkillsInfo.GetSkillConfig(s.Skill); if (def == null) continue; if (!string.Equals(def.Rarity ?? string.Empty, rolledName, StringComparison.OrdinalIgnoreCase)) continue; if (!ignoreMax && def.MaxPerServer >= 0) { int current = assignmentCounts.TryGetValue(s.Skill, out var c) ? c : 0; if (current >= def.MaxPerServer) continue; } filtered.Add(s); } if (filtered.Count > 0) return filtered[Random.Shared.Next(filtered.Count)]; } var fallback = candidates.Where(s => { var def = SkillsInfo.GetSkillConfig(s.Skill); if (def == null) return true; if (ignoreMax) return true; if (def.MaxPerServer < 0) return true; int current = assignmentCounts.TryGetValue(s.Skill, out var c) ? c : 0; return current < def.MaxPerServer; }).ToList(); if (fallback.Count > 0) return fallback[Random.Shared.Next(fallback.Count)]; return candidates[Random.Shared.Next(candidates.Count)]; } private static HookResult RoundStart(EventRoundStart @event, GameEventInfo info) { lock (setLock) { bool isWarmup = IsWarmupPeriod(); isTransmitRegistered = false; setSkillRetries = 0; SkillUtils.ClearKillCredits(); SkillUtils.ClearCurses(); Instance.AddTimer(.1f, () => DisableAll(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); foreach (var player in Utilities.GetPlayers().Where(p => p != null && p.IsValid && !p.IsHLTV && InPlayingTeam(p))) { var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) continue; skillPlayer.IsDrawing = !isWarmup; skillPlayer.PrintHTML = null; } Instance.RemoveListener(CheckTransmit); int freezetime = SkillUtils.CvarValue("mp_freezetime", 0); int introDelay = IsTeamIntroPeriod() ? 7 : 0; freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + introDelay); setSkillTimer?.Kill(); if (isWarmup) { setSkillTimer = Instance?.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug("RoundStart: warmup, SetSkill scheduled in 1.00s.", DebugCategory.Round); return HookResult.Continue; } float timeToDraw = introDelay + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f; setSkillTimer = Instance?.AddTimer(timeToDraw, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug($"RoundStart: SetSkill scheduled in {timeToDraw:F2}s (freezetime={freezetime}, intro={introDelay}, timer={(setSkillTimer == null ? "null" : "ok")}).", DebugCategory.Round); return HookResult.Continue; } } private static void DisableAll() { long perfStart = PerfLog.Start(); DisableAllCore(); PerfLog.End("DisableAll total", perfStart, 2.0); } private static void DisableAllCore() { lock (setLock) { // Re-register CheckTransmit so the dying-entity filter covers the kills below. EnableTransmit(); Fortnite.skillInThisRound = false; EntityManager.DestroyAllTracked(); foreach (var player in Utilities.GetPlayers().Where(p => p != null && p.IsValid)) { if (player == null || !player.IsValid) continue; var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo == null) continue; ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0); SkillsUsedThisMap.TryAdd(playerInfo.Skill.ToString(), 0); if (playerInfo.SpecialSkill != noneSkill.Skill) { ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0); SkillsUsedThisMap.TryAdd(playerInfo.SpecialSkill.ToString(), 0); } Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]); playerInfo.Skill = noneSkill.Skill; playerInfo.SpecialSkill = noneSkill.Skill; playerInfo.PrintHTML = null; playerInfo.SkillChance = 1; playerInfo.SkillUsed = false; RestorePlayer(player); } // Reset every skill used so far on this map, not only the ones held this round: a skill // nobody drew now would otherwise never clear state left over from an earlier round. // Skills that never ran cannot hold state, so they stay out of the sweep. foreach (var skillName in SkillsUsedThisMap.Keys) SafeNewRound(skillName); ActiveSkillsThisRound.Clear(); tickFailuresLogged.Clear(); } } private static void SafeNewRound(string skillName) { try { Instance.SkillAction(skillName, "NewRound"); } catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] {skillName}.NewRound failed, cleanup continues: {(ex.InnerException ?? ex).Message}"); } } public static void OnMapChange() { lock (setLock) { isTransmitRegistered = false; Instance.RemoveListener(CheckTransmit); Fortnite.skillInThisRound = false; SkillUtils.ClearHealthBeforeHit(); EntityManager.SuppressKills = true; try { EntityManager.DestroyAllTracked(); foreach (var skill in SkillData.Skills) SafeNewRound(skill.Skill.ToString()); } finally { EntityManager.SuppressKills = false; } ActiveSkillsThisRound.Clear(); SkillsUsedThisMap.Clear(); nextRoundPicks.Clear(); SkillUtils.ClearAllHudSuppression(); playersCustomFOV.Clear(); playersSkills.Clear(); staticSkills.Clear(); ctSkill = noneSkill; tSkill = noneSkill; allSkill = noneSkill; PlayerManager.Clear(); SkillUtils.Cvar("sv_legacy_jump")?.SetValue("1"); } } private static HookResult RoundEnd(EventRoundEnd @event, GameEventInfo info) { Illiterate.Disable(); DispatchToActiveSkills("RoundEnd"); lock (setLock) { Instance.AddTimer(.5f, () => { if (!Config.LoadedConfig.SummaryAfterTheRound) return; var _players = Utilities.GetPlayers().Where(p => p.IsValid && p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist).OrderBy(p => p.Team).ToList(); foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid)) { string skillsText = ""; foreach (var _player in _players) { var _playerSkill = PlayerManager.GetPlayerByIndex(_player.Index); if (_playerSkill == null) continue; var skillInfo = SkillData.Skills.FirstOrDefault(s => s.Skill == _playerSkill.Skill); var specialSkillInfo = SkillData.Skills.FirstOrDefault(s => s.Skill == _playerSkill.SpecialSkill); if (skillInfo == null) continue; skillsText += $" {ChatColors.DarkRed}\u202A{_player.PlayerName}\u202C{ChatColors.Lime}: {(_playerSkill.SpecialSkill == Skills.None || specialSkillInfo == null ? player.GetSkillName(skillInfo.Skill, _playerSkill.SkillChance) : $"{player.GetSkillName(specialSkillInfo.Skill)} -> {player.GetSkillName(skillInfo.Skill, _playerSkill.SkillChance)}")}\n"; } if (string.IsNullOrEmpty(skillsText)) continue; SkillUtils.PrintToChat(player, string.Empty, title: player.GetTranslationWithoutIlliterate("summary"), border: "t"); foreach (string text in skillsText.Split("\n")) if (!string.IsNullOrEmpty(text)) SkillUtils.PrintToChat(player, text, title: player.GetTranslationWithoutIlliterate("teammate_skills"), border: ""); SkillUtils.PrintToChat(player, string.Empty, border: "b"); } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); // Before the optional disable below, so the "don't repeat the current skill" // exclusion still sees this round's skills. Instance.AddTimer(.6f, PrecomputeNextRoundSkills, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); if (Config.LoadedConfig.DisableSkillsOnRoundEnd) { isTransmitRegistered = false; Instance.AddTimer(1f, () => DisableAll(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Instance.RemoveListener(CheckTransmit); } return HookResult.Continue; } } private static void SetSkill() { long perfStart = PerfLog.Start(); try { SetSkillCore(); } catch (Exception ex) { Debug.WriteToDebug($"SetSkill threw: {ex}", DebugCategory.Skill); if (setSkillRetries < MaxSetSkillRetries) { setSkillRetries++; setSkillTimer = Instance?.AddTimer(.5f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } } PerfLog.End("SetSkill total", perfStart, 2.0); } private sealed class PickContext { public required List BaseList { get; init; } public required Dictionary RequiredPermissions { get; init; } public required HashSet NeedsTeammates { get; init; } public required HashSet CtOnly { get; init; } public required HashSet TOnly { get; init; } public required HashSet PistolRoundBanned { get; init; } public required HashSet MinPlayerBanned { get; init; } public required int TerroristCount { get; init; } public required int CounterTerroristCount { get; init; } } private static PickContext BuildPickContext(List validPlayers) { Dictionary perms = []; foreach (var s in SkillData.Skills) { if (s == null || s.Skill == Skills.None) continue; string perm = SkillsInfo.GetValue(s.Skill, "requiredPermission"); if (!string.IsNullOrEmpty(perm)) perms[s.Skill] = perm; } return new PickContext { BaseList = [.. SkillData.Skills.Where(s => s != null && s.Skill != Skills.None)], RequiredPermissions = perms, NeedsTeammates = ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates).Select(s => s.Name)), CtOnly = ToSkillSet(counterterroristSkills.Select(s => s.Name)), TOnly = ToSkillSet(terroristSkills.Select(s => s.Name)), PistolRoundBanned = SkillUtils.IsPistolRound() ? ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.DisableOnPistolRound).Select(s => s.Name)) : [], MinPlayerBanned = ToSkillSet(SkillsInfo.LoadedConfig.Where(s => s.MinPlayer > 0 && validPlayers.Count < s.MinPlayer).Select(s => s.Name)), TerroristCount = validPlayers.Count(p => p.Team == CsTeam.Terrorist), CounterTerroristCount = validPlayers.Count(p => p.Team == CsTeam.CounterTerrorist), }; } private static HashSet ToSkillSet(IEnumerable names) { HashSet set = []; foreach (var name in names) if (Enum.TryParse(name, out var skill)) set.Add(skill); return set; } private static jSkill_SkillInfo PickSkillForPlayer(CCSPlayerController player, jSkill_PlayerInfo skillPlayer, PickContext ctx, Dictionary assignmentCounts, Config.GameModes gameMode) { List skillList = [.. ctx.BaseList]; if (!player.IsBot && ctx.RequiredPermissions.Count != 0) skillList.RemoveAll(s => ctx.RequiredPermissions.TryGetValue(s.Skill, out var perm) && !HasPermission(player, perm)); if (gameMode != Config.GameModes.FullRandom) skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill); int teamCount = player.Team == CsTeam.Terrorist ? ctx.TerroristCount : ctx.CounterTerroristCount; if (teamCount == 1) skillList.RemoveAll(s => ctx.NeedsTeammates.Contains(s.Skill)); if (player.Team == CsTeam.Terrorist) skillList.RemoveAll(s => ctx.CtOnly.Contains(s.Skill)); else skillList.RemoveAll(s => ctx.TOnly.Contains(s.Skill)); if (ctx.PistolRoundBanned.Count != 0) skillList.RemoveAll(s => ctx.PistolRoundBanned.Contains(s.Skill)); if (ctx.MinPlayerBanned.Count != 0) skillList.RemoveAll(s => ctx.MinPlayerBanned.Contains(s.Skill)); if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet? skills)) { skillList.RemoveAll(s => skills.Contains(s.Skill)); if (skillList.Count == 0) skills.Clear(); } var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player)); if (gameMode == Config.GameModes.NoRepeat) { if (playersSkills.TryGetValue(player.Index, out HashSet? value)) value.Add(randomSkill.Skill); else playersSkills.TryAdd(player.Index, [randomSkill.Skill]); } return randomSkill; } private static bool IsPickStillValid(jSkill_SkillInfo pick, CCSPlayerController player, List validPlayers, Dictionary assignmentCounts) { if (pick.Skill == Skills.None) return true; if (!SkillData.Skills.Any(s => s.Skill == pick.Skill)) return false; string name = SkillNames.Get(pick.Skill); if (player.Team == CsTeam.Terrorist && counterterroristSkills.Any(s => s.Name == name)) return false; if (player.Team == CsTeam.CounterTerrorist && terroristSkills.Any(s => s.Name == name)) return false; var def = SkillsInfo.GetSkillConfig(pick.Skill); if (def == null) return false; if (def.DisableOnPistolRound && SkillUtils.IsPistolRound()) return false; if (def.NeedsTeammates && validPlayers.Count(p => p.Team == player.Team) == 1) return false; if (def.MinPlayer > 0 && validPlayers.Count < def.MinPlayer) return false; if (def.MaxPerServer >= 0 && assignmentCounts.TryGetValue(pick.Skill, out var c) && c >= def.MaxPerServer) return false; return true; } // Runs at round end so the expensive skill selection is off the round-start hot path; // SetSkillCore then only applies the picks. private static void PrecomputeNextRoundSkills() { long perfStart = PerfLog.Start(); lock (setLock) { nextRoundPicks.Clear(); var gameMode = (Config.GameModes)Config.LoadedConfig.GameMode; if (gameMode is not (Config.GameModes.Normal or Config.GameModes.FullRandom or Config.GameModes.NoRepeat)) return; if (Instance?.GameRules == null || Instance.GameRules.WarmupPeriod == true) return; var validPlayers = Utilities.GetPlayers() .Where(p => p != null && p.IsValid && !p.IsHLTV) .Where(p => { try { return p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; } catch { return false; } }).ToList(); var ctx = BuildPickContext(validPlayers); Dictionary assignmentCounts = []; foreach (var player in validPlayers) { var skillPlayer = PlayerManager.GetPlayerByIndex(player.Index); if (skillPlayer == null) continue; try { var pick = PickSkillForPlayer(player, skillPlayer, ctx, assignmentCounts, gameMode); nextRoundPicks[player.Index] = pick; if (pick.Skill != Skills.None) assignmentCounts[pick.Skill] = assignmentCounts.TryGetValue(pick.Skill, out var c) ? c + 1 : 1; } catch (Exception ex) { Debug.WriteToDebug($"PrecomputeSkills failed for {skillPlayer.PlayerName}: {ex.Message}", DebugCategory.Skill); } } } PerfLog.End("PrecomputeSkills total", perfStart, 2.0); } public static void UpdateSkillHudExpired(jSkill_PlayerInfo skillPlayer, Skills skill) { float globalHudExpired = Config.LoadedConfig.SkillHudDuration; float? skillHudExpired = SkillsInfo.GetValue(skill, "hudDuration"); skillPlayer.SkillHudExpired = !skillHudExpired.HasValue ? (globalHudExpired == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(globalHudExpired)) : skillHudExpired.Value == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(skillHudExpired.Value); float globalDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration; float? skillDescriptionHudExpired = SkillsInfo.GetValue(skill, "descriptionHudDuration"); skillPlayer.SkillDescriptionHudExpired = !skillDescriptionHudExpired.HasValue ? (globalDescriptionHudExpired == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(globalDescriptionHudExpired)) : skillDescriptionHudExpired.Value == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(skillDescriptionHudExpired.Value); } private static int CountConnectedPlayers() { try { return Utilities.GetPlayers().Count(p => { if (p == null || !p.IsValid || p.IsHLTV) return false; var pawn = p.PlayerPawn?.Value; return pawn != null && pawn.IsValid; }); } catch { return 0; } } private static bool InPlayingTeam(CCSPlayerController player) { try { return player.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; } catch { return false; } } private static bool IsWarmupPeriod() { try { var gameRules = Instance?.GameRules; if (gameRules == null || gameRules.Handle == IntPtr.Zero) return true; return gameRules.WarmupPeriod; } catch { return true; } } private static bool IsTeamIntroPeriod() { try { var gameRules = Instance?.GameRules; if (gameRules == null || gameRules.Handle == IntPtr.Zero) return false; return gameRules.TeamIntroPeriod; } catch { return false; } } private static void SetSkillCore() { setSkillTimer = null; lock (setLock) { if (Instance == null) return; // GameRules null = not ready; keep polling so skills land right after warmup ends. if (IsWarmupPeriod()) { if (++gameRulesPolls % 10 == 0) Debug.WriteToDebug($"SetSkill waiting: gameRules={(Instance.GameRules == null ? "null" : "warmup")}, poll {gameRulesPolls}.", DebugCategory.Skill); setSkillTimer?.Kill(); setSkillTimer = Instance.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); return; } gameRulesPolls = 0; var validPlayers = Utilities.GetPlayers() .Where(p => p != null && p.IsValid && !p.IsHLTV) .Where(p => { try { return p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; } catch { return false; } }).ToList(); if (Config.LoadedConfig.GameMode == (int)Config.GameModes.TeamSkills) { List tSkills = [.. SkillData.Skills]; tSkills.RemoveAll(s => s.Skill == tSkill.Skill || s.Skill == Skills.None || counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); tSkill = tSkills.Count == 0 ? noneSkill : tSkills[Instance.Random.Next(tSkills.Count)]; List ctSkills = [.. SkillData.Skills]; ctSkills.RemoveAll(s => s.Skill == ctSkill.Skill || s.Skill == Skills.None || terroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); ctSkill = ctSkills.Count == 0 ? noneSkill : ctSkills[Instance.Random.Next(ctSkills.Count)]; } else if (Config.LoadedConfig.GameMode == (int)Config.GameModes.SameSkills) { List allSkills = [.. SkillData.Skills]; allSkills.RemoveAll(s => s.Skill == allSkill.Skill || s.Skill == Skills.None || !allTeamsSkills.Any(s2 => s2.Name == s.Skill.ToString())); allSkill = allSkills.Count == 0 ? noneSkill : allSkills[Instance.Random.Next(allSkills.Count)]; } else if (Config.LoadedConfig.GameMode == (int)Config.GameModes.Debug && debugSkills.Count == 0) debugSkills = [.. SkillData.Skills]; Dictionary assignmentCounts = new(); foreach (var sp in Instance.SkillPlayer) { if (sp == null) continue; if (assignmentCounts.TryGetValue(sp.Skill, out var cnt)) assignmentCounts[sp.Skill] = cnt + 1; else assignmentCounts[sp.Skill] = 1; } PickContext? pickContext = null; int assigned = 0; foreach (var player in validPlayers) { if (player == null) continue; var teammates = validPlayers.Where(p => p != null && p.IsValid && p.Team == player.Team && p != player).ToList(); string teammateSkills = ""; var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) continue; skillPlayer.IsDrawing = false; skillPlayer.HudOnDeathBlocked = null; if (player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) { skillPlayer.Skill = Skills.None; continue; } jSkill_SkillInfo randomSkill = noneSkill; Config.GameModes gameMode = (Config.GameModes)Config.LoadedConfig.GameMode; if (gameMode == Config.GameModes.Normal || gameMode == Config.GameModes.FullRandom || gameMode == Config.GameModes.NoRepeat) { // Prefer the pick made at the end of the previous round; re-pick only when // it no longer fits (team change, missing player, max reached). if (nextRoundPicks.TryGetValue(player.Index, out var pre) && IsPickStillValid(pre, player, validPlayers, assignmentCounts)) randomSkill = pre; else { pickContext ??= BuildPickContext(validPlayers); randomSkill = PickSkillForPlayer(player, skillPlayer, pickContext, assignmentCounts, gameMode); } } else if (gameMode == Config.GameModes.TeamSkills) randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill; else if (gameMode == Config.GameModes.SameSkills) randomSkill = allSkill; else if (gameMode == Config.GameModes.Debug) { if (debugSkills.Count == 0) debugSkills = [.. SkillData.Skills]; randomSkill = debugSkills[0]; debugSkills.RemoveAt(0); player.PrintToChat($"{SkillData.Skills.Count - debugSkills.Count}/{SkillData.Skills.Count}"); } Instance?.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]); skillPlayer.Skill = randomSkill.Skill; skillPlayer.SpecialSkill = Skills.None; if (randomSkill.Skill != Skills.None) { if (assignmentCounts.TryGetValue(randomSkill.Skill, out var cnt)) assignmentCounts[randomSkill.Skill] = cnt + 1; else assignmentCounts[randomSkill.Skill] = 1; } if (randomSkill.Skill == Skills.Illiterate) Illiterate.Enable(); var playerIndex = player.Index; Instance?.AddTimer(.2f, () => { var playerTarget = Utilities.GetPlayerFromIndex((int)playerIndex); if (playerTarget == null || !playerTarget.IsValid) return; if (SkillsInfo.GetValue(randomSkill.Skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime()) Instance?.AddTimer(Config.LoadedConfig.SkillTimeBeforeStart, () => { var playerTarget = Utilities.GetPlayerFromIndex((int)playerIndex); if (playerTarget == null || !playerTarget.IsValid) return; if (PlayerManager.GetPlayerByIndex(playerTarget!.Index)?.Skill != randomSkill.Skill) return; Debug.WriteToDebug("Enabling skill after freeze time: " + randomSkill.Skill, DebugCategory.Skill); Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [playerTarget]); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); else { if (PlayerManager.GetPlayerByIndex(playerTarget!.Index)?.Skill != randomSkill.Skill) return; Debug.WriteToDebug("Enabling skill: " + randomSkill.Skill, DebugCategory.Skill); Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [playerTarget]); } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug($"Player {skillPlayer.PlayerName} has got the skill \"{SkillNames.Get(randomSkill.Skill)}\".", DebugCategory.Skill); UpdateSkillHudExpired(skillPlayer, randomSkill.Skill); assigned++; if (randomSkill.Display && Config.LoadedConfig.YourSkillChatInfo) Instance?.AddTimer(.6f, () => { var descTarget = Utilities.GetPlayerFromIndex((int)playerIndex); if (descTarget == null || !descTarget.IsValid) return; SkillUtils.PrintToChat(descTarget, $"{ChatColors.DarkRed}{descTarget.GetSkillName(randomSkill.Skill)}{ChatColors.Lime}: {descTarget.GetSkillDescription(randomSkill.Skill)}", border: "tb"); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); if (Config.LoadedConfig.TeamMateSkillChatInfo) { Instance?.AddTimer(.2f, () => { if (player == null || !player.IsValid) return; foreach (var teammate in teammates) { var teammateInfo = PlayerManager.GetPlayerByIndex(teammate.Index); if (teammateInfo != null && teammateInfo?.Skill != null) { var skillInfo = SkillData.Skills.FirstOrDefault(p => p.Skill == teammateInfo.Skill); teammateSkills += $" {ChatColors.DarkRed}\u202A{teammate.PlayerName}\u202C{ChatColors.Lime}: {(skillInfo == null ? player.GetSkillName(Skills.None) : player.GetSkillName(skillInfo.Skill, teammateInfo.SkillChance))}\n"; } } if (!string.IsNullOrEmpty(teammateSkills)) { SkillUtils.PrintToChat(player, string.Empty, title: player.GetTranslationWithoutIlliterate("teammate_skills"), border: "t"); foreach (string text in teammateSkills.Split("\n")) if (!string.IsNullOrEmpty(text)) SkillUtils.PrintToChat(player, text, title: player.GetTranslationWithoutIlliterate("teammate_skills"), border: ""); if (!randomSkill.Display) SkillUtils.PrintToChat(player, string.Empty, title: player.GetTranslationWithoutIlliterate("teammate_skills"), border: "b"); } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } } nextRoundPicks.Clear(); if (assigned == 0 && CountConnectedPlayers() > 0) { if (setSkillRetries < MaxSetSkillRetries) { setSkillRetries++; Debug.WriteToDebug($"SetSkill assigned nothing (valid={validPlayers.Count}, connected={CountConnectedPlayers()}); retry {setSkillRetries}/{MaxSetSkillRetries} in .5s.", DebugCategory.Skill); setSkillTimer = Instance?.AddTimer(.5f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } else Debug.WriteToDebug($"SetSkill assigned nothing (valid={validPlayers.Count}, connected={CountConnectedPlayers()}) and gave up after {MaxSetSkillRetries} retries.", DebugCategory.Skill); } else setSkillRetries = 0; } } public static void SetRandomSkill(CCSPlayerController player) { lock (setLock) { var validPlayers = Utilities.GetPlayers().Where(p => p != null && p.IsValid && !p.IsHLTV && p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist).ToList(); if (Config.LoadedConfig.GameMode == (int)Config.GameModes.TeamSkills) { List tSkills = [.. SkillData.Skills]; tSkills.RemoveAll(s => s.Skill == tSkill.Skill || s.Skill == Skills.None || counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); tSkill = tSkills.Count == 0 ? noneSkill : tSkills[0]; List ctSkills = [.. SkillData.Skills]; ctSkills.RemoveAll(s => s.Skill == ctSkill.Skill || s.Skill == Skills.None || terroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); ctSkill = ctSkills.Count == 0 ? noneSkill : ctSkills[0]; } if (player == null) return; var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) return; skillPlayer.IsDrawing = false; if (player.PlayerPawn.Value == null || !player.PlayerPawn.IsValid) { skillPlayer.Skill = Skills.None; return; } jSkill_SkillInfo randomSkill = noneSkill; if (Instance?.GameRules != null && Instance?.GameRules.WarmupPeriod == false) { Config.GameModes gameMode = (Config.GameModes)Config.LoadedConfig.GameMode; if (staticSkills.TryGetValue(player.Index, out var staticSkill)) randomSkill = staticSkill; else if (gameMode == Config.GameModes.Normal || gameMode == Config.GameModes.FullRandom || gameMode == Config.GameModes.NoRepeat) { List skillList = [.. SkillData.Skills]; skillList.RemoveAll(s => s?.Skill == Skills.None); if (!player.IsBot) skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue(s.Skill, "requiredPermission")) && !HasPermission(player, SkillsInfo.GetValue(s.Skill, "requiredPermission"))); if (gameMode != Config.GameModes.FullRandom) skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill); if (validPlayers.Count(p => p.Team == player.Team) == 1) { SkillsInfo.DefaultSkillInfo[] skillsNeedsTeammates = [.. SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates)]; skillList.RemoveAll(s => skillsNeedsTeammates.Any(s2 => s2.Name == s.Skill.ToString())); } if (player.Team == CsTeam.Terrorist) skillList.RemoveAll(s => counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); else skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); if (SkillUtils.IsPistolRound()) skillList.RemoveAll(s => SkillsInfo.GetValue(s.Skill, "disableOnPistolRound")); SkillsInfo.DefaultSkillInfo[] skillsMinPlayer = [.. SkillsInfo.LoadedConfig.Where(s => s.MinPlayer > 0 && validPlayers.Count < s.MinPlayer)]; if (skillsMinPlayer.Length != 0) skillList.RemoveAll(s => skillsMinPlayer.Any(s2 => s2.Name == s.Skill.ToString())); if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out HashSet? skills)) { skillList.RemoveAll(s => skills.Contains(s.Skill)); if (skillList.Count == 0) skills.Clear(); } var assignmentCounts = new Dictionary(); foreach (var sp in Instance.SkillPlayer) { if (sp == null) continue; if (assignmentCounts.TryGetValue(sp.Skill, out var cnt)) assignmentCounts[sp.Skill] = cnt + 1; else assignmentCounts[sp.Skill] = 1; } randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode, IsVip(player)); } else if (gameMode == Config.GameModes.TeamSkills) randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill; else if (gameMode == Config.GameModes.SameSkills) randomSkill = allSkill; else if (gameMode == Config.GameModes.Debug) { if (debugSkills.Count == 0) debugSkills = [.. SkillData.Skills]; randomSkill = debugSkills[0]; debugSkills.RemoveAt(0); player.PrintToChat($"{SkillData.Skills.Count - debugSkills.Count}/{SkillData.Skills.Count}"); } } Instance?.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]); skillPlayer.Skill = randomSkill.Skill; skillPlayer.SpecialSkill = Skills.None; if (randomSkill.Display && Config.LoadedConfig.YourSkillChatInfo) SkillUtils.PrintToChat(player, $"{ChatColors.DarkRed}{player.GetSkillName(randomSkill.Skill)}{ChatColors.Lime}: {player.GetSkillDescription(randomSkill.Skill)}", border: "tb"); if (randomSkill.Skill == Skills.Illiterate) Illiterate.Enable(); Instance?.AddTimer(.2f, () => { if (SkillsInfo.GetValue(randomSkill.Skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime()) Instance?.AddTimer(Config.LoadedConfig.SkillTimeBeforeStart, () => { if (PlayerManager.GetPlayerByIndex(player!.Index)?.Skill != randomSkill.Skill) return; Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [player]); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); else Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [player]); }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug($"Player {skillPlayer.PlayerName} has got the skill \"{SkillNames.Get(randomSkill.Skill)}\".", DebugCategory.Skill); UpdateSkillHudExpired(skillPlayer, randomSkill.Skill); } } public static DateTime GetFreezeTimeEnd() => freezeTimeEnd; } }