css_setoverride only reset cooldowns before UseSkill, checked against the controller the skill ran on. After a bot takeover that's the bot (SteamID 0), so the human's override was ignored; it now resolves to the controlling human. Bots are keyed by slot, so overriding one bot no longer toggles all of them. BlastShot and Dash trigger from OnTick, not UseSkill, so they never saw the override. They check it now; BlastShot fires once per right-click press while overridden instead of every tick.
76 lines
3.6 KiB
C#
76 lines
3.6 KiB
C#
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<ulong> _players = [];
|
|
private static readonly ConcurrentDictionary<Type, FieldInfo[]> _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<uint, T> 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)))];
|
|
}
|
|
}
|