using CounterStrikeSharp.API.Core; using System.Collections; using System.Collections.Concurrent; using System.Reflection; namespace src.utils { // Per-player cooldown bypass (css_setoverride). Skills keep their cooldown in their own static // per-player state, so instead of touching every skill this clears that state just before the // skill's UseSkill runs. Resetting only at use time (not every tick) matters: AimLock and Noclip // reuse their Cooldown timestamp as the start of the active effect. Skills that trigger from // OnTick rather than UseSkill (BlastShot, Dash) check Has themselves. public static class CooldownOverride { private static readonly HashSet _players = []; private static readonly ConcurrentDictionary _stateFields = new(); // Toggles the override and returns the new state. public static bool Toggle(CCSPlayerController player) { if (_players.Remove(Key(player))) return false; _players.Add(Key(player)); return true; } // A bot a human has taken over counts as that human, since skills run on the bot's controller. public static bool Has(CCSPlayerController? player) { player = PlayerManager.GetPlayerFromEvent(player); return player != null && _players.Contains(Key(player)); } // Bots all have SteamID 0, so key them by slot instead (a SteamID64 is never that small). private static ulong Key(CCSPlayerController player) => player.IsBot ? player.Index : player.SteamID; public static void ResetBeforeUse(Type skillType, CCSPlayerController player) { foreach (var field in _stateFields.GetOrAdd(skillType, FindStateFields)) { if (field.GetValue(null) is not IDictionary state) continue; // Spectator keeps a plain index -> last-use time map. if (field.FieldType.GetGenericArguments()[1] == typeof(DateTime)) { state.Remove(player.Index); continue; } var entry = state[player.Index]; if (entry == null) continue; foreach (var prop in entry.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!prop.CanWrite) continue; if (prop.Name == "Cooldown" && prop.PropertyType == typeof(DateTime)) prop.SetValue(entry, DateTime.MinValue); else if (prop.Name == "CanUse" && prop.PropertyType == typeof(bool)) prop.SetValue(entry, true); else if (prop.Name is "NextUse" or "NextCamera" && prop.PropertyType == typeof(float)) prop.SetValue(entry, 0f); } } } // Static ConcurrentDictionary fields keyed by player index - the per-player skill // state every cooldown skill uses. DateTime-valued ones only when they are last-use maps. private static FieldInfo[] FindStateFields(Type skillType) => [.. skillType.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) .Where(f => f.FieldType.IsGenericType && f.FieldType.GetGenericTypeDefinition() == typeof(ConcurrentDictionary<,>) && f.FieldType.GetGenericArguments()[0] == typeof(uint) && (f.FieldType.GetGenericArguments()[1] != typeof(DateTime) || f.Name.Contains("use", StringComparison.OrdinalIgnoreCase)))]; } }