Merge pull request #41 from ByDexterTR/fix/skill-state-leaks

1.2.2.b9
This commit is contained in:
Juzlus 2026-07-22 23:18:42 +02:00 • committed by GitHub
commit 5fc3e8db66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 316 additions and 102 deletions

View file

@ -26,6 +26,7 @@ namespace src
public IWasdMenuManager? MenuManager; public IWasdMenuManager? MenuManager;
// Skills that were enabled at least once this round; used to reset only those on round change (not all 124). // Skills that were enabled at least once this round; used to reset only those on round change (not all 124).
public static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new(); public static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new();
public static readonly ConcurrentDictionary<string, byte> SkillsUsedThisMap = new();
public override string ModuleName => "[CS2] [ jRandomSkills ]"; public override string ModuleName => "[CS2] [ jRandomSkills ]";
public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)"; public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)";
@ -95,7 +96,10 @@ namespace src
return null; return null;
if (methodName == "EnableSkill") if (methodName == "EnableSkill")
{
ActiveSkillsThisRound.TryAdd(skill, 0); ActiveSkillsThisRound.TryAdd(skill, 0);
SkillsUsedThisMap.TryAdd(skill, 0);
}
var method = _skillMethodCache.GetOrAdd((skill, methodName), key => var method = _skillMethodCache.GetOrAdd((skill, methodName), key =>
{ {

View file

@ -138,22 +138,54 @@ namespace src.player
return candidates[Random.Shared.Next(candidates.Count)]; return candidates[Random.Shared.Next(candidates.Count)];
} }
private static readonly Skills[] lateDamageSkills = [Skills.SecondLife];
private static readonly HashSet<Skills> 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) private static void DispatchToActiveSkills(string methodName, params object[] args)
{ {
var seen = new HashSet<Skills>(); var seen = new HashSet<Skills>();
foreach (var p in Instance.SkillPlayer) foreach (var p in Instance.SkillPlayer)
{ {
if (p.IsDrawing || !seen.Add(p.Skill)) continue; if (p.IsDrawing || !seen.Add(p.Skill)) continue;
try InvokeSkill(p.Skill, methodName, args);
}
}
private static void DispatchOnTakeDamage(DynamicHook h)
{ {
Instance.SkillAction(p.Skill.ToString(), methodName, args); object[] args = [h];
} var seen = new HashSet<Skills>();
catch (Exception ex) List<Skills>? deferred = null;
foreach (var p in Instance.SkillPlayer)
{ {
// One skill's failure must not break the dispatch chain. if (p.IsDrawing || !seen.Add(p.Skill)) continue;
Server.PrintToConsole($"[jRandomSkills] {p.Skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}");
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) private static HookResult PlayerMakeSound(UserMessage um)
@ -322,7 +354,7 @@ namespace src.player
{ {
lock (setLock) lock (setLock)
{ {
DispatchToActiveSkills("OnTakeDamage", h); DispatchOnTakeDamage(h);
if (Fortnite.skillInThisRound == true && if (Fortnite.skillInThisRound == true &&
!Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite)) !Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite))
@ -468,8 +500,18 @@ namespace src.player
foreach (var skill in _activeSkillsList) foreach (var skill in _activeSkillsList)
{ {
if (freeze && _freezeDisabledSkills.Contains(skill)) continue; if (freeze && _freezeDisabledSkills.Contains(skill)) continue;
try
{
Instance.SkillAction(_skillNames[skill], "OnTick"); 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); PerfLog.Sample("OnTick(skills)", perfStart);
} }
@ -541,6 +583,10 @@ namespace src.player
Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]); 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); PlayerManager.UnregisterPlayer(player.Index);
EntityManager.DestroyPlayerEntities(player.Index); EntityManager.DestroyPlayerEntities(player.Index);
@ -660,8 +706,12 @@ namespace src.player
if (playerInfo == null) continue; if (playerInfo == null) continue;
ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0); ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0);
SkillsUsedThisMap.TryAdd(playerInfo.Skill.ToString(), 0);
if (playerInfo.SpecialSkill != noneSkill.Skill) if (playerInfo.SpecialSkill != noneSkill.Skill)
{
ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0); ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0);
SkillsUsedThisMap.TryAdd(playerInfo.SpecialSkill.ToString(), 0);
}
Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]); Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]);
@ -674,9 +724,13 @@ namespace src.player
RestorePlayer(player); RestorePlayer(player);
} }
foreach (var skillName in ActiveSkillsThisRound.Keys) // 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"); Instance.SkillAction(skillName, "NewRound");
ActiveSkillsThisRound.Clear(); ActiveSkillsThisRound.Clear();
tickFailuresLogged.Clear();
} }
} }
@ -712,6 +766,7 @@ namespace src.player
EntityManager.SuppressKills = false; EntityManager.SuppressKills = false;
ActiveSkillsThisRound.Clear(); ActiveSkillsThisRound.Clear();
SkillsUsedThisMap.Clear();
nextRoundPicks.Clear(); nextRoundPicks.Clear();
playersSkills.Clear(); playersSkills.Clear();

View file

@ -58,6 +58,11 @@ namespace src.player.skills
Marshal.WriteInt32(hitGroupOffset, 56, hitGroup); Marshal.WriteInt32(hitGroupOffset, 56, hitGroup);
} }
public static void NewRound()
{
hitGroups.Clear();
}
public static void DisableSkill(CCSPlayerController _) public static void DisableSkill(CCSPlayerController _)
{ {
foreach (var hit in hitGroups) foreach (var hit in hitGroups)
@ -65,6 +70,8 @@ namespace src.player.skills
if (hit.Key != nint.Zero) if (hit.Key != nint.Zero)
Marshal.WriteInt32(hit.Key, 56, hit.Value); Marshal.WriteInt32(hit.Key, 56, hit.Value);
} }
hitGroups.Clear();
} }
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff0000", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity) public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#ff0000", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)

View file

@ -67,6 +67,10 @@ namespace src.player.skills
{ {
if (Server.TickCount % 2 != 0) return; if (Server.TickCount % 2 != 0) return;
var bomb = invisiblePlayers.IsEmpty
? null
: Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
foreach (var player in Utilities.GetPlayers()) foreach (var player in Utilities.GetPlayers())
{ {
if (player.PlayerPawn?.Value?.Health <= 0 && invisiblePlayers.ContainsKey(player.Index)) if (player.PlayerPawn?.Value?.Health <= 0 && invisiblePlayers.ContainsKey(player.Index))
@ -77,7 +81,7 @@ namespace src.player.skills
// CheckTransmit hides the model but the radar blip comes from spotted state, so clear it every tick. // CheckTransmit hides the model but the radar blip comes from spotted state, so clear it every tick.
if (invisiblePlayers.ContainsKey(player.Index)) if (invisiblePlayers.ContainsKey(player.Index))
ClearSpottedState(player); ClearSpottedState(player, bomb);
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo?.Skill != skillName) continue; if (playerInfo?.Skill != skillName) continue;
@ -99,6 +103,11 @@ namespace src.player.skills
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{ {
if (invisiblePlayers.IsEmpty) return;
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
if (bomb != null && !bomb.IsValid) bomb = null;
foreach (var (info, player) in infoList) foreach (var (info, player) in infoList)
{ {
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
@ -127,14 +136,10 @@ namespace src.player.skills
if (info.TransmitEntities.Contains(entity.Index)) if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index); info.TransmitEntities.Remove(entity.Index);
var bombIndex = GetBombIndex(); if (bomb == null) continue;
if (bombIndex == null) continue;
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex); if (info.TransmitEntities.Contains(bomb.Index))
if (bombEntity == null || !bombEntity.IsValid) continue; info.TransmitEntities.Remove(bomb.Index);
if (info.TransmitEntities.Contains(bombEntity.Index))
info.TransmitEntities.Remove(bombEntity.Index);
} }
} }
} }
@ -212,19 +217,8 @@ namespace src.player.skills
particle.AcceptInput("Start"); particle.AcceptInput("Start");
} }
private static uint? GetBombIndex()
{
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
if (bombEntities.Count == 0) return null;
var bomb = bombEntities.FirstOrDefault();
if (bomb == null) return null;
return bomb.Index;
}
// Wipe spotted state (pawn + carried bomb) so a disguised carrier produces no radar blip. // Wipe spotted state (pawn + carried bomb) so a disguised carrier produces no radar blip.
private static void ClearSpottedState(CCSPlayerController player) private static void ClearSpottedState(CCSPlayerController player, CC4? bomb)
{ {
var pawn = player.PlayerPawn?.Value; var pawn = player.PlayerPawn?.Value;
if (pawn != null && pawn.IsValid) if (pawn != null && pawn.IsValid)
@ -234,7 +228,6 @@ namespace src.player.skills
pawn.EntitySpottedState.SpottedByMask[1] = 0; pawn.EntitySpottedState.SpottedByMask[1] = 0;
} }
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
if (bomb != null && bomb.IsValid && bomb.OwnerEntity?.Index == player.Index) if (bomb != null && bomb.IsValid && bomb.OwnerEntity?.Index == player.Index)
{ {
bomb.EntitySpottedState.Spotted = false; bomb.EntitySpottedState.Spotted = false;

View file

@ -21,6 +21,19 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
lock (setLock)
{
playersInDark.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
}
public static void NewRound() public static void NewRound()
{ {
lock (setLock) lock (setLock)

View file

@ -18,6 +18,16 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
deafPlayers.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
public static void NewRound() public static void NewRound()
{ {
foreach (var playerIndex in deafPlayers.Keys) foreach (var playerIndex in deafPlayers.Keys)

View file

@ -1,5 +1,6 @@
using CounterStrikeSharp.API; using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions;
using CounterStrikeSharp.API.Modules.Utils; using CounterStrikeSharp.API.Modules.Utils;
using src.utils; using src.utils;
@ -9,10 +10,23 @@ namespace src.player.skills
public class FriendlyFire : ISkill public class FriendlyFire : ISkill
{ {
private const Skills skillName = Skills.FriendlyFire; private const Skills skillName = Skills.FriendlyFire;
private static bool defaultAutoKick = true;
private static bool autoKickOverridden;
public static void LoadSkill() public static void LoadSkill()
{ {
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
try { defaultAutoKick = ConVar.Find("mp_autokick")?.GetPrimitiveValue<bool>() ?? true; }
catch { defaultAutoKick = true; }
}
public static void NewRound()
{
if (!autoKickOverridden) return;
autoKickOverridden = false;
Server.ExecuteCommand($"mp_autokick {(defaultAutoKick ? 1 : 0)}");
} }
public static void OnTakeDamage(DynamicHook h) public static void OnTakeDamage(DynamicHook h)
@ -56,7 +70,11 @@ namespace src.player.skills
float damage = param2.Damage; float damage = param2.Damage;
param2.Damage = 0; param2.Damage = 0;
if (!autoKickOverridden)
{
autoKickOverridden = true;
Server.ExecuteCommand("mp_autokick 0"); Server.ExecuteCommand("mp_autokick 0");
}
SkillUtils.AddHealth( SkillUtils.AddHealth(
victimPawn, victimPawn,

View file

@ -56,6 +56,11 @@ namespace src.player.skills
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{ {
if (invisiblePlayers.IsEmpty) return;
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
foreach (var (info, player) in infoList) foreach (var (info, player) in infoList)
{ {
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
@ -84,14 +89,11 @@ namespace src.player.skills
if (info.TransmitEntities.Contains(entity.Index)) if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index); info.TransmitEntities.Remove(entity.Index);
var bombIndex = GetBombIndex(playerController); // Hide the bomb as well, but only while this hidden player is the one holding it.
if (bombIndex == null) continue; if (bomb == null || bombOwnerIndex != playerController.Index) continue;
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex); if (info.TransmitEntities.Contains(bomb.Index))
if (bombEntity == null || !bombEntity.IsValid) continue; info.TransmitEntities.Remove(bomb.Index);
if (info.TransmitEntities.Contains(bombEntity.Index))
info.TransmitEntities.Remove(bombEntity.Index);
} }
} }
} }
@ -232,18 +234,6 @@ namespace src.player.skills
playerInfo.PrintHTML = $"<font color='#FF0000'>{player.GetTranslation("disabled_weapon")}</font>"; playerInfo.PrintHTML = $"<font color='#FF0000'>{player.GetTranslation("disabled_weapon")}</font>";
} }
private static uint? GetBombIndex(CCSPlayerController player)
{
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
if (bombEntities.Count == 0) return null;
var bomb = bombEntities.FirstOrDefault();
if (bomb == null) return null;
if (bomb.OwnerEntity.Index != player.Index) return null;
return bomb.Index;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#FFFFFF", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Epic) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity) public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#FFFFFF", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Epic) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{ {
} }

View file

@ -18,6 +18,19 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
lock (setLock)
{
glitchedPlayers.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
}
public static void NewRound() public static void NewRound()
{ {
lock (setLock) lock (setLock)

View file

@ -75,7 +75,7 @@ namespace src.player.skills
public static void OnTick() public static void OnTick()
{ {
int tick = SkillsInfo.GetValue<int>(skillName, "tickCooldown"); int tick = Math.Max(1, SkillsInfo.GetValue<int>(skillName, "tickCooldown"));
if (Server.TickCount % tick != 0) return; if (Server.TickCount % tick != 0) return;
float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius"); float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius");

View file

@ -20,7 +20,7 @@ namespace src.player.skills
public static void OnTick() public static void OnTick()
{ {
int cooldown = (int)(SkillsInfo.GetValue<float>(skillName, "cooldown") * 64); int cooldown = Math.Max(1, (int)(SkillsInfo.GetValue<float>(skillName, "cooldown") * 64));
if (Server.TickCount % cooldown != 0) return; if (Server.TickCount % cooldown != 0) return;
if (players.IsEmpty || jRandomSkills.Instance.GameRules?.FreezePeriod == true) return; if (players.IsEmpty || jRandomSkills.Instance.GameRules?.FreezePeriod == true) return;

View file

@ -340,6 +340,8 @@ namespace src.player.skills
var victim = PlayerManager.GetPlayerEvent(@event.Userid); var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (victim == null || !victim.IsValid) return; if (victim == null || !victim.IsValid) return;
if (PlayerManager.GetPlayerByIndex(victim.Index)?.Skill != skillName) return;
if (playersInfo.TryGetValue(victim.Index, out var playerSkill)) if (playersInfo.TryGetValue(victim.Index, out var playerSkill))
{ {
if (playerSkill.CloneProp != null) if (playerSkill.CloneProp != null)

View file

@ -161,7 +161,7 @@ namespace src.player.skills
replica.Teleport(nexPos, null, null); replica.Teleport(nexPos, null, null);
}, TimerFlags.REPEAT | TimerFlags.STOP_ON_MAPCHANGE); }, TimerFlags.REPEAT | TimerFlags.STOP_ON_MAPCHANGE);
ActiveTimers.TryAdd(replicaIndex, moveTimer); ActiveTimers.TryAdd(replicaIndex, moveTimer);
}); }, TimerFlags.STOP_ON_MAPCHANGE);
float duration = SkillsInfo.GetValue<float>(skillName, ducking ? "durationCrouch" : "durationRun"); float duration = SkillsInfo.GetValue<float>(skillName, ducking ? "durationCrouch" : "durationRun");
Instance.AddTimer(duration, () => Instance.AddTimer(duration, () =>

View file

@ -18,6 +18,19 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false);
} }
public static void PlayerDisconnect(uint playerIndex)
{
lock (setLock)
{
jammedPlayers.TryRemove(playerIndex, out _);
jammerToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in jammerToTarget)
if (kvp.Value == playerIndex)
jammerToTarget.TryRemove(kvp.Key, out _);
}
}
public static void NewRound() public static void NewRound()
{ {
lock (setLock) lock (setLock)

View file

@ -19,6 +19,16 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
if (!jesters.TryRemove(playerIndex, out var jester)) return;
jester.Generation++;
jester.Active = false;
jester.Timer?.Kill();
jester.Timer = null;
}
public static void NewRound() public static void NewRound()
{ {
Server.NextWorldUpdate(() => Server.NextWorldUpdate(() =>
@ -37,7 +47,7 @@ namespace src.player.skills
SkillUtils.ResetPrintHTML(player); SkillUtils.ResetPrintHTML(player);
var pawn = player.PlayerPawn.Value; var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) return; if (pawn == null || !pawn.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE) continue;
var color = Color.FromArgb(255, 255, 255, 255); var color = Color.FromArgb(255, 255, 255, 255);
pawn.Render = color; pawn.Render = color;
@ -114,11 +124,10 @@ namespace src.player.skills
var victim = PlayerManager.GetPlayerEvent(@event.Userid); var victim = PlayerManager.GetPlayerEvent(@event.Userid);
if (!Instance.IsPlayerValid(victim)) return; if (!Instance.IsPlayerValid(victim)) return;
var jesterVictim = GetJesterInfo(victim!.Index);
if (!Instance.IsPlayerValid(attacker)) if (!Instance.IsPlayerValid(attacker))
{ {
if (jesterVictim != null && jesterVictim.Active) if (IsActiveJester(victim!.Index))
{ {
SkillUtils.RestoreHealth(victim); SkillUtils.RestoreHealth(victim);
RestoreArmor(victim, @event.DmgArmor); RestoreArmor(victim, @event.DmgArmor);
@ -126,14 +135,19 @@ namespace src.player.skills
return; return;
} }
var jesterAttacker = GetJesterInfo(attacker!.Index); if (IsActiveJester(victim!.Index) || IsActiveJester(attacker!.Index))
if ((jesterVictim != null && jesterVictim.Active) || (jesterAttacker != null && jesterAttacker.Active))
{ {
SkillUtils.RestoreHealth(victim); SkillUtils.RestoreHealth(victim);
RestoreArmor(victim, @event.DmgArmor); RestoreArmor(victim, @event.DmgArmor);
} }
} }
public static bool IsActiveJester(uint playerIndex)
{
if (GetJesterInfo(playerIndex)?.Active != true) return false;
return PlayerManager.GetPlayerByIndex(playerIndex)?.Skill == skillName;
}
private static void RestoreArmor(CCSPlayerController? victim, int dmgArmor) private static void RestoreArmor(CCSPlayerController? victim, int dmgArmor)
{ {
if (victim == null || !victim.IsValid || dmgArmor <= 0) return; if (victim == null || !victim.IsValid || dmgArmor <= 0) return;

View file

@ -17,6 +17,16 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
bannedPlayers.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
public static void NewRound() public static void NewRound()
{ {
bannedPlayers.Clear(); bannedPlayers.Clear();

View file

@ -31,6 +31,9 @@ namespace src.player.skills
public static void NewRound() public static void NewRound()
{ {
playersInAction.Clear(); playersInAction.Clear();
if (!hooked) return;
hooked = false; hooked = false;
Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre); Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre);
} }

View file

@ -17,6 +17,16 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
playersFOV.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
public static void NewRound() public static void NewRound()
{ {
foreach (var playerIndex in playersFOV.Keys) foreach (var playerIndex in playersFOV.Keys)

View file

@ -58,6 +58,11 @@ namespace src.player.skills
public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList)
{ {
if (invisiblePlayers.IsEmpty) return;
var bomb = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").FirstOrDefault();
uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null;
foreach (var (info, player) in infoList) foreach (var (info, player) in infoList)
{ {
if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue;
@ -86,13 +91,10 @@ namespace src.player.skills
if (info.TransmitEntities.Contains(entity.Index)) if (info.TransmitEntities.Contains(entity.Index))
info.TransmitEntities.Remove(entity.Index); info.TransmitEntities.Remove(entity.Index);
var bombIndex = GetBombIndex(playerController); if (bomb == null || bombOwnerIndex != playerController.Index) continue;
if (bombIndex == null) continue;
var bombEntity = Utilities.GetEntityFromIndex<CBaseEntity>((int)bombIndex);
if (bombEntity == null || !bombEntity.IsValid) continue;
if (info.TransmitEntities.Contains(bombEntity.Index)) if (info.TransmitEntities.Contains(bomb.Index))
info.TransmitEntities.Remove(bombEntity.Index); info.TransmitEntities.Remove(bomb.Index);
} }
} }
} }
@ -229,18 +231,6 @@ namespace src.player.skills
particle.AcceptInput("Start"); particle.AcceptInput("Start");
} }
private static uint? GetBombIndex(CCSPlayerController player)
{
var bombEntities = Utilities.FindAllEntitiesByDesignerName<CC4>("weapon_c4").ToList();
if (bombEntities.Count == 0) return null;
var bomb = bombEntities.FirstOrDefault();
if (bomb == null) return null;
if (bomb.OwnerEntity.Index != player.Index) return null;
return bomb.Index;
}
public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#dedede", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float idlePercentInvisibility = .3f, float duckPercentInvisibility = .3f, float knifePercentInvisibility = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity) public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#dedede", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", float? hudDuration = null, float? descriptionHudDuration = null, int maxPerServer = -1, Rarity rarity = Rarity.Common, float idlePercentInvisibility = .3f, float duckPercentInvisibility = .3f, float knifePercentInvisibility = .3f) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, hudDuration, descriptionHudDuration, maxPerServer, rarity)
{ {
public float IdlePercentInvisibility { get; set; } = idlePercentInvisibility; public float IdlePercentInvisibility { get; set; } = idlePercentInvisibility;

View file

@ -2,6 +2,7 @@ using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils; using CounterStrikeSharp.API.Modules.Utils;
using src.utils; using src.utils;
using System.Collections.Concurrent;
using static src.jRandomSkills; using static src.jRandomSkills;
namespace src.player.skills namespace src.player.skills
@ -10,6 +11,9 @@ namespace src.player.skills
{ {
private const Skills skillName = Skills.NoRecoil; private const Skills skillName = Skills.NoRecoil;
private static readonly ConcurrentDictionary<uint, byte> holders = [];
private static bool noSpreadActive;
public static void LoadSkill() public static void LoadSkill()
{ {
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
@ -17,21 +21,33 @@ namespace src.player.skills
public static void NewRound() public static void NewRound()
{ {
var players = Utilities.GetPlayers(); holders.Clear();
foreach (var player in players) ApplyNoSpread(false);
DisableSkill(player);
} }
public static void EnableSkill(CCSPlayerController player) public static void EnableSkill(CCSPlayerController player)
{ {
if (player == null || !player.IsValid) return; if (player == null || !player.IsValid) return;
Server.ExecuteCommand("weapon_accuracy_nospread 1");
holders.TryAdd(player.Index, 0);
ApplyNoSpread(true);
} }
public static void DisableSkill(CCSPlayerController player) public static void DisableSkill(CCSPlayerController player)
{ {
if (player == null || !player.IsValid) return; if (player == null || !player.IsValid) return;
Server.ExecuteCommand("weapon_accuracy_nospread 0");
holders.TryRemove(player.Index, out _);
if (holders.IsEmpty)
ApplyNoSpread(false);
}
private static void ApplyNoSpread(bool enabled)
{
if (noSpreadActive == enabled) return;
noSpreadActive = enabled;
Server.ExecuteCommand($"weapon_accuracy_nospread {(enabled ? 1 : 0)}");
} }
public static void OnTick() public static void OnTick()

View file

@ -15,6 +15,7 @@ namespace src.player.skills
private static readonly ConcurrentDictionary<uint, float> plantingPlayers = []; private static readonly ConcurrentDictionary<uint, float> plantingPlayers = [];
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override. // mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
private static int defaultC4Timer = 40; private static int defaultC4Timer = 40;
private static bool c4TimerOverridden;
public static void LoadSkill() public static void LoadSkill()
{ {
@ -25,6 +26,7 @@ namespace src.player.skills
public static void EnableSkill(CCSPlayerController player) public static void EnableSkill(CCSPlayerController player)
{ {
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes. // At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
c4TimerOverridden = true;
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "extraC4BlowTime")}"); Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "extraC4BlowTime")}");
} }
@ -70,6 +72,9 @@ namespace src.player.skills
DisableSkill(player); DisableSkill(player);
plantingPlayers.Clear(); plantingPlayers.Clear();
if (!c4TimerOverridden) return;
c4TimerOverridden = false;
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
} }

View file

@ -18,6 +18,19 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"), false);
} }
public static void PlayerDisconnect(uint playerIndex)
{
lock (setLock)
{
poisonedPlayers.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
}
public static void NewRound() public static void NewRound()
{ {
lock (setLock) lock (setLock)
@ -29,7 +42,7 @@ namespace src.player.skills
public static void OnTick() public static void OnTick()
{ {
int cooldown = (int)(64 * SkillsInfo.GetValue<float>(skillName, "Cooldown")); int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue<float>(skillName, "Cooldown")));
if (Server.TickCount % cooldown == 0) if (Server.TickCount % cooldown == 0)
{ {
@ -43,7 +56,7 @@ namespace src.player.skills
var pawn = player.PlayerPawn.Value; var pawn = player.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) continue; if (pawn == null || !pawn.IsValid) continue;
if (Jester.GetJesterInfo(playerIndex)?.Active == true) continue; if (Jester.IsActiveJester(playerIndex)) continue;
if (pawn.Health <= SkillsInfo.GetValue<int>(skillName, "MinHealth")) continue; if (pawn.Health <= SkillsInfo.GetValue<int>(skillName, "MinHealth")) continue;
SkillUtils.TakeHealth(pawn, SkillsInfo.GetValue<int>(skillName, "Damage")); SkillUtils.TakeHealth(pawn, SkillsInfo.GetValue<int>(skillName, "Damage"));

View file

@ -28,6 +28,19 @@ namespace src.player.skills
SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color")); SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue<string>(skillName, "color"));
} }
public static void PlayerDisconnect(uint playerIndex)
{
lock (setLock)
{
bannedPlayers.TryRemove(playerIndex, out _);
playersToTarget.TryRemove(playerIndex, out _);
foreach (var kvp in playersToTarget)
if (kvp.Value == playerIndex)
playersToTarget.TryRemove(kvp.Key, out _);
}
}
public static void NewRound() public static void NewRound()
{ {
lock (setLock) lock (setLock)

View file

@ -9,6 +9,7 @@ namespace src.player.skills
public class RadarHack : ISkill public class RadarHack : ISkill
{ {
private const Skills skillName = Skills.RadarHack; private const Skills skillName = Skills.RadarHack;
private static readonly Skills[] hidingSkills = [Skills.Ghost, Skills.Ninja, Skills.C4Camouflage];
public static void LoadSkill() public static void LoadSkill()
{ {
@ -43,8 +44,9 @@ namespace src.player.skills
var enemyPawn = enemyEvent.PlayerPawn.Value; var enemyPawn = enemyEvent.PlayerPawn.Value;
if (enemyPawn == null || !enemyPawn.IsValid) continue; if (enemyPawn == null || !enemyPawn.IsValid) continue;
// Invisibility (low render alpha) beats the radar hack. var enemyInfo = PlayerManager.GetPlayerByIndex(enemyEvent.Index);
if (enemyPawn.Render.A < 200) continue; if (enemyInfo != null && Array.IndexOf(hidingSkills, enemyInfo.Skill) >= 0 && enemyPawn.Render.A < 200)
continue;
// Only the observer's slot bit — the Spotted bool would reveal to the whole team. // Only the observer's slot bit — the Spotted bool would reveal to the whole team.
enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32)); enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32));

View file

@ -45,6 +45,8 @@ namespace src.player.skills
if (attacker == null || !attacker.IsValid) return; if (attacker == null || !attacker.IsValid) return;
if (victimEvent == null || !victimEvent.IsValid || !victim.PawnIsAlive) return; if (victimEvent == null || !victimEvent.IsValid || !victim.PawnIsAlive) return;
if (PlayerManager.GetPlayerByIndex(victimEvent!.Index)?.Skill != skillName) return;
if (SkillPlayerInfo.TryGetValue(victimEvent!.Index, out var skillInfo)) if (SkillPlayerInfo.TryGetValue(victimEvent!.Index, out var skillInfo))
{ {
if (!skillInfo.CanUse) return; if (!skillInfo.CanUse) return;

View file

@ -16,7 +16,8 @@ namespace src.player.skills
public static void OnTick() public static void OnTick()
{ {
if (Server.TickCount % (int)(64 * SkillsInfo.GetValue<float>(skillName, "cooldown")) != 0) return; int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue<float>(skillName, "cooldown")));
if (Server.TickCount % cooldown != 0) return;
foreach (var player in Utilities.GetPlayers()) foreach (var player in Utilities.GetPlayers())
{ {
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);

View file

@ -11,7 +11,7 @@ namespace src.player.skills
public class SecondLife : ISkill public class SecondLife : ISkill
{ {
private const Skills skillName = Skills.SecondLife; private const Skills skillName = Skills.SecondLife;
private static readonly ConcurrentDictionary<nint, byte> usedThisRound = []; private static readonly ConcurrentDictionary<nint, int> usedThisRound = [];
private static readonly object setLock = new(); private static readonly object setLock = new();
public static void LoadSkill() public static void LoadSkill()
@ -45,7 +45,13 @@ namespace src.player.skills
if (victimInfo == null || victimInfo.Skill != skillName) return; if (victimInfo == null || victimInfo.Skill != skillName) return;
if (info.Damage < victimPawn.Health) return; if (info.Damage < victimPawn.Health) return;
if (usedThisRound.ContainsKey(victim.Handle)) return;
if (usedThisRound.TryGetValue(victim.Handle, out int savedTick))
{
if (savedTick == Server.TickCount)
info.Damage = 0;
return;
}
lock (setLock) lock (setLock)
{ {
@ -54,18 +60,18 @@ namespace src.player.skills
var spawnpoint = SkillUtils.GetSpawnPointVector(victim); var spawnpoint = SkillUtils.GetSpawnPointVector(victim);
if (spawnpoint == null) return; // no clean respawn point -> let the normal death happen if (spawnpoint == null) return; // no clean respawn point -> let the normal death happen
usedThisRound.TryAdd(victim.Handle, 0); usedThisRound.TryAdd(victim.Handle, Server.TickCount);
info.Damage = 0; info.Damage = 0;
int startHealth = SkillsInfo.GetValue<int>(skillName, "startHealth"); victimPawn.Health = SkillsInfo.GetValue<int>(skillName, "startHealth");
Utilities.SetStateChanged(victimPawn, "CBaseEntity", "m_iHealth");
Server.NextFrame(() => Server.NextFrame(() =>
{ {
if (victim == null || !victim.IsValid) return; if (victim == null || !victim.IsValid) return;
var pawn = victim.PlayerPawn.Value; var pawn = victim.PlayerPawn.Value;
if (pawn == null || !pawn.IsValid) return; if (pawn == null || !pawn.IsValid) return;
pawn.Health = startHealth;
Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth");
pawn.Teleport(spawnpoint, null, new Vector(0, 0, 0)); pawn.Teleport(spawnpoint, null, new Vector(0, 0, 0));
}); });
} }

View file

@ -12,6 +12,7 @@ namespace src.player.skills
private const Skills skillName = Skills.ShortBomb; private const Skills skillName = Skills.ShortBomb;
// mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override. // mp_c4timer is an Int32 cvar; captured at load so restore never picks up another skill's override.
private static int defaultC4Timer = 40; private static int defaultC4Timer = 40;
private static bool c4TimerOverridden;
public static void LoadSkill() public static void LoadSkill()
{ {
@ -22,11 +23,15 @@ namespace src.player.skills
public static void EnableSkill(CCSPlayerController player) public static void EnableSkill(CCSPlayerController player)
{ {
// At round start (not at plant) so the client HUD/alert countdown is right before the plant completes. // At round start (not at plant) so the client HUD/alert countdown is right before the plant completes.
c4TimerOverridden = true;
Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "detonationTime")}"); Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue<int>(skillName, "detonationTime")}");
} }
public static void NewRound() public static void NewRound()
{ {
if (!c4TimerOverridden) return;
c4TimerOverridden = false;
Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}");
} }

View file

@ -59,20 +59,20 @@ namespace src.player.skills
foreach (var player in Utilities.GetPlayers()) foreach (var player in Utilities.GetPlayers())
if (cameras.TryGetValue(player.Index, out var cameraInfo) && cameraInfo.Item2 != 0) if (cameras.TryGetValue(player.Index, out var cameraInfo) && cameraInfo.Item2 != 0)
{ {
if (player == null || !player.IsValid) return; if (player == null || !player.IsValid) continue;
var enemy = Utilities.GetPlayerFromIndex((int)cameraInfo.Item3); var enemy = Utilities.GetPlayerFromIndex((int)cameraInfo.Item3);
if (enemy == null || !enemy.IsValid || enemy.PlayerPawn == null) if (enemy == null || !enemy.IsValid || enemy.PlayerPawn == null)
{ {
ChangeCamera(player, true); ChangeCamera(player, true);
return; continue;
} }
var enemyPawn = enemy.PlayerPawn.Value; var enemyPawn = enemy.PlayerPawn.Value;
if (enemyPawn == null || !enemyPawn.IsValid) if (enemyPawn == null || !enemyPawn.IsValid)
{ {
ChangeCamera(player, true); ChangeCamera(player, true);
return; continue;
} }
if (enemyPawn.Health <= 0 || (player.PlayerPawn?.Value != null && player.PlayerPawn.Value.Health <= 0)) if (enemyPawn.Health <= 0 || (player.PlayerPawn?.Value != null && player.PlayerPawn.Value.Health <= 0))
@ -117,6 +117,10 @@ namespace src.player.skills
pawn.CameraServices.ViewEntity.Raw = orginalCameraRaw; pawn.CameraServices.ViewEntity.Raw = orginalCameraRaw;
Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_pCameraServices"); Utilities.SetStateChanged(pawn, "CBasePlayerPawn", "m_pCameraServices");
if (forceToDefault && cameras.TryGetValue(player.Index, out var current) && current.Item2 != 0)
cameras[player.Index] = (current.Item1, 0, current.Item3);
BlockWeapon(player, !defaultCam); BlockWeapon(player, !defaultCam);
} }

View file

@ -75,7 +75,7 @@ namespace src.player.skills
public static void OnTick() public static void OnTick()
{ {
int tick = SkillsInfo.GetValue<int>(skillName, "tickCooldown"); int tick = Math.Max(1, SkillsInfo.GetValue<int>(skillName, "tickCooldown"));
if (Server.TickCount % tick != 0) return; if (Server.TickCount % tick != 0) return;
float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius"); float smokeRadius = SkillsInfo.GetValue<float>(skillName, "smokeRadius");

View file

@ -23,6 +23,8 @@ namespace src.player.skills
{ {
lock (setLock) lock (setLock)
{ {
if (infectedPlayers.IsEmpty && playersToTarget.IsEmpty) return;
foreach (var player in Utilities.GetPlayers()) foreach (var player in Utilities.GetPlayers())
DisableSkill(player); DisableSkill(player);

View file

@ -296,8 +296,8 @@ namespace src.utils
var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index);
if (playerInfo == null) return false; if (playerInfo == null) return false;
var jester = Jester.GetJesterInfo(player.Index); if (playerInfo.Skill == Skills.Jester && Jester.GetJesterInfo(player.Index)?.Active == true)
if (jester?.Active == true) return false; return false;
if (playerInfo.Skill == Skills.GodMode && GodMode.HaveHodMode(player.Index)) if (playerInfo.Skill == Skills.GodMode && GodMode.HaveHodMode(player.Index))
return false; return false;