using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes; using CounterStrikeSharp.API.Modules.Admin; using CounterStrikeSharp.API.Modules.Cvars; 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 RayTraceAPI; 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 Timer? setSkillTimer = null; private static DateTime freezeTimeEnd = DateTime.MinValue; private static bool isTransmitRegistered = false; public static readonly jSkill_SkillInfo noneSkill = new(Skills.None, SkillsInfo.GetValue(Skills.None, "color"), false); private static jSkill_SkillInfo ctSkill = noneSkill; private static jSkill_SkillInfo tSkill = noneSkill; private static jSkill_SkillInfo allSkill = noneSkill; private static List debugSkills = [.. SkillData.Skills]; public static readonly SkillsInfo.DefaultSkillInfo[] terroristSkills = [.. SkillsInfo.LoadedConfig.Where(s => s.OnlyTeam == (int)CsTeam.Terrorist)]; public static readonly SkillsInfo.DefaultSkillInfo[] counterterroristSkills = [.. SkillsInfo.LoadedConfig.Where(s => s.OnlyTeam == (int)CsTeam.CounterTerrorist)]; private static readonly SkillsInfo.DefaultSkillInfo[] allTeamsSkills = [.. SkillsInfo.LoadedConfig.Where(s => s.OnlyTeam == 0)]; private static readonly ConcurrentDictionary> playersSkills = []; public static readonly ConcurrentDictionary staticSkills = []; private static readonly object setLock = new(); public static void Load() { Instance.RegisterEventHandler(PlayerConnectFull); Instance.RegisterEventHandler(PlayerDisconnect); // Instance.RegisterEventHandler(PlayerChat); Instance.RegisterEventHandler(PlayerSpawned); Instance.RegisterEventHandler(RoundStart); Instance.RegisterEventHandler(RoundEnd); Instance.RegisterEventHandler(PlayerDeath); Instance.RegisterEventHandler(PlayerBlind); Instance.RegisterEventHandler(PlayerHurt); Instance.RegisterEventHandler(PlayerJump); Instance.RegisterEventHandler(BotTakeover); Instance.RegisterEventHandler(WeaponFire); Instance.RegisterEventHandler(WeaponEquip); Instance.RegisterEventHandler(WeaponPickup); Instance.RegisterEventHandler(WeaponReload); Instance.RegisterEventHandler(GrenadeThrown); Instance.RegisterEventHandler(BombBeginplant); Instance.RegisterEventHandler(BombAbortplant); Instance.RegisterEventHandler(BombPlanted); Instance.RegisterEventHandler(BombBegindefuse); Instance.RegisterEventHandler(DecoyStarted); Instance.RegisterEventHandler(DecoyDetonate); Instance.RegisterEventHandler(SmokegrenadeDetonate); Instance.RegisterEventHandler(SmokegrenadeExpired); Instance.RegisterListener(CheckUseSkill); Instance.RegisterListener(EntitySpawned); Instance.RegisterListener(OnTick); Instance.RegisterListener(OnPlayerConnectedBot); Instance.HookUserMessage(208, PlayerMakeSound); VirtualFunctions.CBaseEntity_TakeDamageOldFunc.Hook(OnTakeDamage, HookMode.Pre); Instance.RegisterEventHandler(BulletImpact); VirtualFunctions.CBaseTrigger_StartTouchFunc.Hook(OnTriggerEnter, HookMode.Post); VirtualFunctions.CBaseTrigger_EndTouchFunc.Hook(OnTriggerExit, HookMode.Pre); VirtualFunctions.CCSPlayer_ItemServices_CanAcquireFunc.Hook(OnWeaponCanAcquire, HookMode.Pre); // Disabled after CS2 updates started crashing Linux servers on player join. // The hooked native signature is only used to block weapon drops for Iana clones. // Keeping the plugin alive is safer than installing a stale global hook at load time. } private static jSkill_SkillInfo ChooseSkillByRarityAndMax(List candidates, Dictionary assignmentCounts, Config.GameModes gameMode) { if (candidates == null || candidates.Count == 0) return noneSkill; bool ignoreMax = gameMode == Config.GameModes.SameSkills || gameMode == Config.GameModes.TeamSkills; const int attempts = 6; for (int attempt = 0; attempt < attempts; attempt++) { var (roll, rolled) = RarityManager.RollRarity(); var filtered = candidates.Where(s => { if (s == null) return false; var def = SkillsInfo.LoadedConfig.FirstOrDefault(d => d.Name == s.Skill.ToString()); if (def == null) return false; if (!string.Equals(def.Rarity ?? string.Empty, rolled.ToString(), StringComparison.OrdinalIgnoreCase)) return false; if (!ignoreMax && def.MaxPerServer >= 0) { int current = assignmentCounts.TryGetValue(s.Skill, out var c) ? c : 0; if (current >= def.MaxPerServer) return false; } return true; }).ToList(); if (filtered.Count > 0) return filtered[Random.Shared.Next(filtered.Count)]; } var fallback = candidates.Where(s => { var def = SkillsInfo.LoadedConfig.FirstOrDefault(d => d.Name == s.Skill.ToString()); 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 readonly Skills[] lateDamageSkills = [Skills.SecondLife]; private static readonly HashSet tickFailuresLogged = []; private static void InvokeSkill(Skills skill, string methodName, object[] args) { try { Instance.SkillAction(skill.ToString(), methodName, args); } catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] {skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}"); } } private static void DispatchToActiveSkills(string methodName, params object[] args) { var seen = new HashSet(); foreach (var p in Instance.SkillPlayer) { if (p.IsDrawing || !seen.Add(p.Skill)) continue; InvokeSkill(p.Skill, methodName, args); } } private static void DispatchOnTakeDamage(DynamicHook h) { object[] args = [h]; var seen = new HashSet(); List? deferred = null; foreach (var p in Instance.SkillPlayer) { if (p.IsDrawing || !seen.Add(p.Skill)) continue; if (Array.IndexOf(lateDamageSkills, p.Skill) >= 0) { (deferred ??= []).Add(p.Skill); continue; } InvokeSkill(p.Skill, "OnTakeDamage", args); } if (deferred == null) return; foreach (var skill in deferred) InvokeSkill(skill, "OnTakeDamage", args); } private static HookResult PlayerMakeSound(UserMessage um) { lock (setLock) { DispatchToActiveSkills("PlayerMakeSound", um); return HookResult.Continue; } } private static HookResult WeaponFire(EventWeaponFire @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("WeaponFire", @event); return HookResult.Continue; } } private static HookResult WeaponEquip(EventItemEquip @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("WeaponEquip", @event); return HookResult.Continue; } } private static HookResult WeaponPickup(EventItemPickup @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("WeaponPickup", @event); return HookResult.Continue; } } private static HookResult WeaponReload(EventWeaponReload @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("WeaponReload", @event); return HookResult.Continue; } } private static HookResult GrenadeThrown(EventGrenadeThrown @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("GrenadeThrown", @event); return HookResult.Continue; } } private static HookResult BombBeginplant(EventBombBeginplant @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BombBeginplant", @event); return HookResult.Continue; } } private static HookResult BombAbortplant(EventBombAbortplant @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BombAbortplant", @event); return HookResult.Continue; } } private static HookResult BombPlanted(EventBombPlanted @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BombPlanted", @event); return HookResult.Continue; } } private static HookResult BombBegindefuse(EventBombBegindefuse @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BombBegindefuse", @event); return HookResult.Continue; } } private static HookResult DecoyStarted(EventDecoyStarted @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("DecoyStarted", @event); return HookResult.Continue; } } private static HookResult DecoyDetonate(EventDecoyDetonate @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("DecoyDetonate", @event); return HookResult.Continue; } } private static HookResult SmokegrenadeDetonate(EventSmokegrenadeDetonate @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("SmokegrenadeDetonate", @event); return HookResult.Continue; } } private static HookResult SmokegrenadeExpired(EventSmokegrenadeExpired @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("SmokegrenadeExpired", @event); return HookResult.Continue; } } private static HookResult PlayerHurt(EventPlayerHurt @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("PlayerHurt", @event); return HookResult.Continue; } } private static HookResult PlayerJump(EventPlayerJump @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("PlayerJump", @event); return HookResult.Continue; } } private static HookResult BotTakeover(EventBotTakeover @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BotTakeover", @event); return HookResult.Continue; } } private static HookResult PlayerBlind(EventPlayerBlind @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("PlayerBlind", @event); return HookResult.Continue; } } private static HookResult OnTakeDamage(DynamicHook h) { lock (setLock) { DispatchOnTakeDamage(h); if (Fortnite.skillInThisRound == true && !Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite)) Instance.SkillAction("Fortnite", "OnTakeDamage", [h]); return HookResult.Continue; } } private static HookResult OnTriggerEnter(DynamicHook hook) { lock (setLock) { CBaseTrigger trigger = hook.GetParam(0); CBaseEntity entity = hook.GetParam(1); DispatchToActiveSkills("OnTriggerEnter", trigger, entity); return HookResult.Continue; } } private static HookResult OnTriggerExit(DynamicHook hook) { lock (setLock) { CBaseTrigger trigger = hook.GetParam(0); CBaseEntity entity = hook.GetParam(1); DispatchToActiveSkills("OnTriggerExit", trigger, entity); return HookResult.Continue; } } private static HookResult OnWeaponCanAcquire(DynamicHook hook) { lock (setLock) { CCSPlayer_ItemServices itemServices = hook.GetParam(0); if (itemServices == null || itemServices.Pawn.Value == null || !itemServices.Pawn.Value.IsValid) return HookResult.Continue; CEconItemView econItem = hook.GetParam(1); if (econItem == null) return HookResult.Continue; CBasePlayerPawn pawn = itemServices.Pawn.Value; if (pawn == null || !pawn.IsValid || pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return HookResult.Continue; CCSPlayerController player = pawn.Controller.Value.As(); if (player == null || !player.IsValid) return HookResult.Continue; var playerInfo = PlayerManager.GetPlayerByIndex(player.Index); if (playerInfo == null) return HookResult.Continue; CCSWeaponBaseVData vdata = VirtualFunctions.GetCSWeaponDataFromKeyFunc.Invoke(-1, econItem.ItemDefinitionIndex.ToString()); if (vdata == null || vdata.Handle == IntPtr.Zero) return HookResult.Continue; var activeSkills = Instance.SkillPlayer .Where(p => !p.IsDrawing) .Select(p => p.Skill.ToString()) .Distinct(); bool block = false; foreach (string skillName in activeSkills) { bool? result = (bool?)Instance.SkillAction(skillName, "OnWeaponCanAcquire", [hook, player, econItem, vdata]); if (result == true) { block = true; break; } } return block ? HookResult.Handled : HookResult.Continue; } } private static HookResult WeaponDrop(DynamicHook hook) { lock (setLock) { CCSPlayerController player = hook.GetParam(0); if (player == null || !player.IsValid) return HookResult.Continue; var activeSkills = Instance.SkillPlayer .Where(p => !p.IsDrawing) .Select(p => p.Skill.ToString()) .Distinct(); bool block = false; foreach (string skillName in activeSkills) { bool? result = (bool?)Instance.SkillAction(skillName, "WeaponDrop", [hook, player]); if (result == true) { block = true; break; } } return block ? HookResult.Handled : HookResult.Continue; } } private static readonly Dictionary _skillNames = Enum.GetValues().ToDictionary(s => s, s => s.ToString()); private static readonly HashSet _activeSkillsSet = []; private static readonly List _activeSkillsList = []; private static readonly Comparison _tickOrderCmp = (a, b) => TickOrder(a).CompareTo(TickOrder(b)); private static HashSet? _freezeDisabledSkills; // AreaReaper and ChillOut depend on other skills' tick results, so they must tick last. private static int TickOrder(Skills s) => s == Skills.AreaReaper ? 2 : s == Skills.ChillOut ? 1 : 0; private static HashSet BuildFreezeDisabledSkills() { var set = new HashSet(); foreach (var s in SkillData.Skills) if (SkillsInfo.GetValue(s.Skill, "disableOnFreezeTime")) set.Add(s.Skill); return set; } public static void InvalidateFreezeDisabledCache() => _freezeDisabledSkills = null; private static void OnTick() { long perfStart = PerfLog.Start(); lock (setLock) { _activeSkillsSet.Clear(); _activeSkillsList.Clear(); foreach (var p in Instance.SkillPlayer) { if (p.IsDrawing) continue; if (_activeSkillsSet.Add(p.Skill)) _activeSkillsList.Add(p.Skill); } _activeSkillsList.Sort(_tickOrderCmp); bool freeze = SkillUtils.IsFreezeTime(); _freezeDisabledSkills ??= BuildFreezeDisabledSkills(); foreach (var skill in _activeSkillsList) { if (freeze && _freezeDisabledSkills.Contains(skill)) continue; try { Instance.SkillAction(_skillNames[skill], "OnTick"); } catch (Exception ex) { // Without this one throwing skill cancels every later skill's tick, every frame. // Logged once per skill per round; at 64 ticks a repeat would flood the console. if (tickFailuresLogged.Add(skill)) Server.PrintToConsole($"[jRandomSkills] {skill}.OnTick failed: {ex.InnerException?.Message ?? ex.Message}"); } } } PerfLog.Sample("OnTick(skills)", perfStart); } private static void OnPlayerConnectedBot(int playerSlot) { lock (setLock) { var player = Utilities.GetPlayerFromSlot(playerSlot); if (player == null || !player.IsValid) return; if (player.IsBot && !Config.LoadedConfig.EnableBotSkills) return; var existing = Instance.SkillPlayer.FirstOrDefault(p => p.PlayerIndex == player.Index); if (existing != null) { PlayerManager.Register(existing); return; } var playerInfo = new jSkill_PlayerInfo { IsBot = player.IsBot, PlayerName = player.PlayerName, PlayerIndex = player.Index, Skill = Skills.None, SpecialSkill = Skills.None, IsDrawing = false, SkillChance = 1, PrintHTML = null, DisplayHUD = true, SkillUsed = false, }; PlayerManager.Register(playerInfo); } } private static HookResult PlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info) { lock (setLock) { var player = PlayerManager.GetPlayerEvent(@event.Userid); if (player == null || !player.IsValid) return HookResult.Continue; Localization.PreResolveLanguage(player); string welcomeMsg = player.GetTranslation("welcome_message", "welcome"); foreach (string line in welcomeMsg.Split("\n")) player.PrintToChat($" {ChatColors.Green}" + line.Replace("{PLAYER}", $" {ChatColors.Red}\u202A{player.PlayerName}\u202C{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{SERVER_NAME}", $" {ChatColors.Red}{ConVar.Find("hostname")?.StringValue ?? "Default Server"}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{VERSION}", $" {ChatColors.Red}v{Instance.ModuleVersion}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{SKILLS_COUNT}", $" {ChatColors.Red}{SkillData.Skills.Count - 1}{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) .Replace("{AUTHOR1}", $" {ChatColors.Red}Jakub Bartosik (D3X){ChatColors.Green} ({ChatColors.Red}https://github.com/jakubbartosik/dRandomSkills{ChatColors.Green})", StringComparison.OrdinalIgnoreCase) .Replace("{AUTHOR2}", $" {ChatColors.Red}Juzlus{ChatColors.Green} ({ChatColors.Red}https://github.com/Juzlus/jRandomSkills{ChatColors.Green})", StringComparison.OrdinalIgnoreCase)); return HookResult.Continue; } } private static HookResult PlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info) { lock (setLock) { var player = PlayerManager.GetPlayerEvent(@event.Userid); if (player == null || !player.IsValid) return HookResult.Continue; var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) return HookResult.Continue; Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]); uint leavingIndex = player.Index; foreach (var skill in SkillData.Skills) Instance.SkillAction(skill.Skill.ToString(), "PlayerDisconnect", [leavingIndex]); PlayerManager.UnregisterPlayer(player.Index); EntityManager.DestroyPlayerEntities(player.Index); return HookResult.Continue; } } private static HookResult PlayerChat(EventPlayerChat @event, GameEventInfo info) { var userID = @event.Userid; if (userID == 0 || string.IsNullOrEmpty(@event.Text)) return HookResult.Continue; string text = @event.Text.Split(' ')[0].Trim(); string commandName = text.StartsWith('!') || text.StartsWith('/') ? text[1..] : text; var player = Utilities.GetPlayerFromUserid(userID); if (player == null || !player.IsValid) return HookResult.Continue; var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) return HookResult.Continue; var temp = skillPlayer.SkillHudExpired; skillPlayer.SkillHudExpired = DateTime.MinValue; Instance.AddTimer(5f, () => { if (skillPlayer.SkillHudExpired == DateTime.MinValue) skillPlayer.SkillHudExpired = temp; }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); return HookResult.Continue; } private static HookResult PlayerSpawned(EventPlayerSpawned @event, GameEventInfo info) { lock (setLock) { var player = PlayerManager.GetPlayerEvent(@event.Userid); if (player == null || !player.IsValid) return HookResult.Continue; var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) return HookResult.Continue; if (setSkillTimer != null) { skillPlayer.IsDrawing = true; return HookResult.Continue; } skillPlayer.IsDrawing = false; if (Instance?.GameRules != null && Instance?.GameRules.WarmupPeriod == false && skillPlayer.Skill == Skills.None && skillPlayer.SpecialSkill == Skills.None) SetRandomSkill(player); return HookResult.Continue; } } private static HookResult RoundStart(EventRoundStart @event, GameEventInfo info) { lock (setLock) { bool isWarmup = Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true; isTransmitRegistered = false; 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 && p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist)) { var skillPlayer = PlayerManager.GetPlayerByIndex(player!.Index); if (skillPlayer == null) continue; skillPlayer.IsDrawing = !isWarmup; skillPlayer.PrintHTML = null; } Instance.RemoveListener(CheckTransmit); int freezetime = ConVar.Find("mp_freezetime")?.GetPrimitiveValue() ?? 0; freezeTimeEnd = DateTime.Now.AddSeconds(freezetime + (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0)); setSkillTimer?.Kill(); if (isWarmup) { setSkillTimer = Instance?.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); return HookResult.Continue; } float timeToDraw = (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0) + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f; setSkillTimer = Instance?.AddTimer(timeToDraw, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); 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) Instance.SkillAction(skillName, "NewRound"); ActiveSkillsThisRound.Clear(); tickFailuresLogged.Clear(); } } public static void RestorePlayer(CCSPlayerController? player) { if (player == null || !player.IsValid) return; var pawn = player.PlayerPawn?.Value; if (pawn == null || !pawn.IsValid) return; pawn.HideHUD = (uint)(pawn.HideHUD & ~(1 << 8)); Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_iHideHUD"); player.ReplicateConVar("sv_disable_radar", "0"); player.DesiredFOV = 0; Utilities.SetStateChanged(player, "CBasePlayerController", "m_iDesiredFOV"); } public static void OnMapChange() { lock (setLock) { isTransmitRegistered = false; Instance.RemoveListener(CheckTransmit); Fortnite.skillInThisRound = false; EntityManager.SuppressKills = true; EntityManager.DestroyAllTracked(); foreach (var skill in SkillData.Skills) Instance.SkillAction(skill.Skill.ToString(), "NewRound"); EntityManager.SuppressKills = false; ActiveSkillsThisRound.Clear(); SkillsUsedThisMap.Clear(); nextRoundPicks.Clear(); playersSkills.Clear(); staticSkills.Clear(); ctSkill = noneSkill; tSkill = noneSkill; allSkill = noneSkill; PlayerManager.Clear(); ConVar.Find("sv_legacy_jump")?.SetValue("1"); } } private static HookResult RoundEnd(EventRoundEnd @event, GameEventInfo info) { Illiterate.Disable(); 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.GetTranslation("summary"), border: "t"); foreach (string text in skillsText.Split("\n")) if (!string.IsNullOrEmpty(text)) SkillUtils.PrintToChat(player, text, title: player.GetTranslation("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 HookResult PlayerDeath(EventPlayerDeath @event, GameEventInfo info) { long perfStart = PerfLog.Start(); var result = PlayerDeathCore(@event, info); PerfLog.End("PlayerDeath total", perfStart, 2.0); return result; } private static HookResult PlayerDeathCore(EventPlayerDeath @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("PlayerDeath", @event); var victim = PlayerManager.GetPlayerEvent(@event.Userid); if (victim == null) return HookResult.Continue; var playerInfo = PlayerManager.GetPlayerByIndex(victim.Index); if (playerInfo == null || playerInfo.IsDrawing) return HookResult.Continue; Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [victim]); var attacker = PlayerManager.GetPlayerEvent(@event.Attacker); if (attacker == null || victim == attacker) return HookResult.Continue; if (victim == attacker) return HookResult.Continue; if (Config.LoadedConfig.KillerSkillChatInfo) { var attackerInfo = PlayerManager.GetPlayerByIndex(attacker!.Index); if (attackerInfo != null) { var skillData = SkillData.Skills.FirstOrDefault(s => s.Skill == attackerInfo.Skill); var specialSkillData = SkillData.Skills.FirstOrDefault(s => s.Skill == attackerInfo.SpecialSkill); if (skillData == null || specialSkillData == null) return HookResult.Continue; string skillDesc = victim.GetSkillDescription(skillData.Skill); SkillUtils.PrintToChat(victim, $"{ChatColors.DarkRed}{(attackerInfo.SpecialSkill == Skills.None ? victim.GetSkillName(skillData.Skill) : $"{victim.GetSkillName(specialSkillData.Skill)} -> {victim.GetSkillName(skillData.Skill)}")}{ChatColors.Lime} - {skillDesc}", title: $"{victim.GetTranslation("enemy_skill")} {ChatColors.DarkRed}\u202A{attacker.PlayerName}\u202C{ChatColors.Lime}"); } } return HookResult.Continue; } } private static void CheckUseSkill(CCSPlayerController player, PlayerButtons pressed, PlayerButtons released) { if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return; lock (setLock) { string? button = Config.LoadedConfig.AlternativeSkillButton; if (string.IsNullOrEmpty(button) || button.Length < 2) return; string buttonName = $"{char.ToUpperInvariant(button[0])}{button[1..].ToLowerInvariant()}"; if (!Enum.TryParse(buttonName, out var skillButton)) return; if ((pressed & skillButton) == 0) return; if (SkillUtils.HasMenu(player)) return; var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo == null || playerInfo.IsDrawing) return; if (SkillsInfo.GetValue(playerInfo.Skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime()) return; if (skillButton == PlayerButtons.Use) { var pawn = player.PlayerPawn.Value; if (pawn == null || !pawn.IsValid) return; if (pawn.AbsOrigin == null || pawn.AbsRotation == null) return; if (pawn.IsDefusing) return; Vector eyePos = new(pawn.AbsOrigin.X, pawn.AbsOrigin.Y, pawn.AbsOrigin.Z + pawn.ViewOffset.Z); Vector endPos = eyePos + SkillUtils.GetForwardVector(pawn.EyeAngles) * 80; ulong mask = (ulong)(InteractionLayers.MASK_WORLD_ONLY | InteractionLayers.Player | InteractionLayers.NPC); ulong contents = 0; var result = RayTrace.TraceShape(player, eyePos, endPos, mask, contents); if (result.HasValue && result.Value.DidHit) { var entity = Activator.CreateInstance(typeof(CBaseEntity), result.Value.HitEntity) as CBaseEntity; if (entity == null || !entity.IsValid) return; string designer = entity.DesignerName; if (designer.Contains("door") || designer.Contains("button") || designer.Contains("weapon") || designer.Contains("blocker")) return; } } Debug.WriteToDebug($"Player {player.PlayerName} used the skill: {playerInfo.Skill} by PlayerButtons: {pressed}"); Instance.SkillAction(playerInfo.Skill.ToString(), "UseSkill", [player]); } } private static void EntitySpawned(CEntityInstance entity) { lock (setLock) { DispatchToActiveSkills("OnEntitySpawned", entity); } } private static HookResult BulletImpact(EventBulletImpact @event, GameEventInfo info) { lock (setLock) { DispatchToActiveSkills("BulletImpact", @event); return HookResult.Continue; } } private static void SetSkill() { long perfStart = PerfLog.Start(); SetSkillCore(); PerfLog.End("SetSkill total", perfStart, 2.0); } private static readonly Dictionary nextRoundPicks = []; private static jSkill_SkillInfo PickSkillForPlayer(CCSPlayerController player, jSkill_PlayerInfo skillPlayer, List validPlayers, Dictionary assignmentCounts, Config.GameModes gameMode) { List skillList = [.. SkillData.Skills]; skillList.RemoveAll(s => s?.Skill == Skills.None); if (!player.IsBot) skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(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 (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag? skills)) { skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill)); if (skillList.Count == 0) skills.Clear(); } var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode); if (gameMode == Config.GameModes.NoRepeat) { if (playersSkills.TryGetValue(player.Index, out ConcurrentBag? value)) value.Add(randomSkill); else playersSkills.TryAdd(player.Index, [randomSkill]); } 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 = pick.Skill.ToString(); 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.LoadedConfig.FirstOrDefault(d => d.Name == name); if (def == null) return false; if (def.NeedsTeammates && validPlayers.Count(p => p.Team == player.Team) == 1) 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(); Dictionary assignmentCounts = []; foreach (var player in validPlayers) { var skillPlayer = PlayerManager.GetPlayerByIndex(player.Index); if (skillPlayer == null) continue; var pick = PickSkillForPlayer(player, skillPlayer, validPlayers, assignmentCounts, gameMode); nextRoundPicks[player.Index] = pick; if (pick.Skill != Skills.None) assignmentCounts[pick.Skill] = assignmentCounts.TryGetValue(pick.Skill, out var c) ? c + 1 : 1; } } 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 void SetSkillCore() { setSkillTimer = null; lock (setLock) { if (Instance == null) return; // GameRules null = not ready; keep polling so skills land right after warmup ends. if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true) { setSkillTimer?.Kill(); setSkillTimer = Instance.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); 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(); 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; } 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; 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 randomSkill = PickSkillForPlayer(player, skillPlayer, validPlayers, 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 (randomSkill.Display) SkillUtils.PrintToChat(playerTarget, $"{ChatColors.DarkRed}{playerTarget.GetSkillName(randomSkill.Skill)}{ChatColors.Lime}: {playerTarget.GetSkillDescription(randomSkill.Skill)}", border: !Utilities.GetPlayers().Any(p => p != null && p.IsValid && p.Team == playerTarget.Team && p != playerTarget) ? "tb" : "t"); 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); 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); Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [playerTarget]); } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug($"Player {skillPlayer.PlayerName} has got the skill \"{player.GetSkillName(randomSkill.Skill)}\"."); UpdateSkillHudExpired(skillPlayer, randomSkill.Skill); if (Config.LoadedConfig.TeamMateSkillChatInfo) { Instance?.AddTimer(.6f, () => { 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.GetTranslation("teammate_skills"), border: "t"); foreach (string text in teammateSkills.Split("\n")) if (!string.IsNullOrEmpty(text)) SkillUtils.PrintToChat(player, text, title: player.GetTranslation("teammate_skills"), border: ""); SkillUtils.PrintToChat(player, string.Empty, title: player.GetTranslation("teammate_skills"), border: "b"); } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } } nextRoundPicks.Clear(); } } 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")) && !AdminManager.PlayerHasPermissions(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 (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag? skills)) { skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == 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); } 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: !Utilities.GetPlayers().Any(p => p != null && p.IsValid && p.Team == player.Team && p != player) ? "tb" : "t"); 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 \"{player.GetSkillName(randomSkill.Skill)}\"."); UpdateSkillHudExpired(skillPlayer, randomSkill.Skill); } } public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) { long perfStart = PerfLog.Start(); lock (setLock) { // Keep dying entities out of snapshots until the engine processes the kill. var dying = EntityManager.GetRecentlyDestroyedSnapshot(); if (dying.Count > 0) { foreach (var (info, player) in infoList) { if (player == null || !player.IsValid) continue; foreach (var entityIndex in dying) if (info.TransmitEntities.Contains(entityIndex)) info.TransmitEntities.Remove(entityIndex); } } DispatchToActiveSkills("CheckTransmit", infoList); } PerfLog.Sample("CheckTransmit", perfStart); } public static void UpdateSkillHUD(CCSPlayerController? player, string? headerLine, string? centerLine, string? extraLine, bool isDescription) { lock (setLock) { if (player == null || !player.IsValid) return; if (Illiterate.CheckIlliterateSkill(player)) { headerLine = Illiterate.GetRandomText(headerLine); centerLine = Illiterate.GetRandomText(centerLine); extraLine = Illiterate.GetRandomText(extraLine); } var config = Config.LoadedConfig.HtmlHudCustomisation; var emptySymbol = $" "; var emptySymbol2 = $" "; string infoLine = string.IsNullOrEmpty(headerLine) || string.IsNullOrEmpty(config.HeaderLineSize) ? "" : $"{headerLine}:
"; string skillLine = $"{emptySymbol2}{centerLine}{emptySymbol2}"; string remainingLine = string.IsNullOrWhiteSpace(extraLine) ? "" : $"
{emptySymbol}{extraLine}{emptySymbol}"; var hudContent = infoLine + skillLine + remainingLine; player.PrintToCenterHtml(hudContent); } } public static void EnableTransmit() { if (!isTransmitRegistered) { Instance?.RegisterListener(CheckTransmit); isTransmitRegistered = true; } } public static DateTime GetFreezeTimeEnd() => freezeTimeEnd; } }