diff --git a/jRandomSkills - SRC Files/src/jRandomSkills.cs b/jRandomSkills - SRC Files/src/jRandomSkills.cs index 7e106f1..7c6e120 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -26,6 +26,7 @@ namespace src public IWasdMenuManager? MenuManager; // Skills that were enabled at least once this round; used to reset only those on round change (not all 124). public static readonly ConcurrentDictionary ActiveSkillsThisRound = new(); + public static readonly ConcurrentDictionary SkillsUsedThisMap = new(); public override string ModuleName => "[CS2] [ jRandomSkills ]"; public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)"; @@ -95,7 +96,10 @@ namespace src return null; if (methodName == "EnableSkill") + { ActiveSkillsThisRound.TryAdd(skill, 0); + SkillsUsedThisMap.TryAdd(skill, 0); + } var method = _skillMethodCache.GetOrAdd((skill, methodName), key => { diff --git a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs index eb692d0..cc2a7fc 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs @@ -138,24 +138,56 @@ namespace src.player 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; - try - { - Instance.SkillAction(p.Skill.ToString(), methodName, args); - } - catch (Exception ex) - { - // One skill's failure must not break the dispatch chain. - Server.PrintToConsole($"[jRandomSkills] {p.Skill}.{methodName} failed: {ex.InnerException?.Message ?? ex.Message}"); - } + 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) @@ -322,7 +354,7 @@ namespace src.player { lock (setLock) { - DispatchToActiveSkills("OnTakeDamage", h); + DispatchOnTakeDamage(h); if (Fortnite.skillInThisRound == true && !Instance.SkillPlayer.Any(p => !p.IsDrawing && p.Skill == Skills.Fortnite)) @@ -468,7 +500,17 @@ namespace src.player foreach (var skill in _activeSkillsList) { if (freeze && _freezeDisabledSkills.Contains(skill)) continue; - Instance.SkillAction(_skillNames[skill], "OnTick"); + 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); @@ -541,6 +583,10 @@ namespace src.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); EntityManager.DestroyPlayerEntities(player.Index); @@ -660,8 +706,12 @@ namespace src.player 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]); @@ -674,9 +724,13 @@ namespace src.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"); ActiveSkillsThisRound.Clear(); + tickFailuresLogged.Clear(); } } @@ -712,6 +766,7 @@ namespace src.player EntityManager.SuppressKills = false; ActiveSkillsThisRound.Clear(); + SkillsUsedThisMap.Clear(); nextRoundPicks.Clear(); playersSkills.Clear(); diff --git a/jRandomSkills - SRC Files/src/player/skills/Aimbot.cs b/jRandomSkills - SRC Files/src/player/skills/Aimbot.cs index 53dde0d..de633b9 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Aimbot.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Aimbot.cs @@ -58,6 +58,11 @@ namespace src.player.skills Marshal.WriteInt32(hitGroupOffset, 56, hitGroup); } + public static void NewRound() + { + hitGroups.Clear(); + } + public static void DisableSkill(CCSPlayerController _) { foreach (var hit in hitGroups) @@ -65,6 +70,8 @@ namespace src.player.skills if (hit.Key != nint.Zero) 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) diff --git a/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs b/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs index f6501f3..a7b7938 100644 --- a/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs +++ b/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs @@ -67,6 +67,10 @@ namespace src.player.skills { if (Server.TickCount % 2 != 0) return; + var bomb = invisiblePlayers.IsEmpty + ? null + : Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); + foreach (var player in Utilities.GetPlayers()) { 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. if (invisiblePlayers.ContainsKey(player.Index)) - ClearSpottedState(player); + ClearSpottedState(player, bomb); var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo?.Skill != skillName) continue; @@ -99,6 +103,11 @@ namespace src.player.skills public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) { + if (invisiblePlayers.IsEmpty) return; + + var bomb = Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); + if (bomb != null && !bomb.IsValid) bomb = null; + foreach (var (info, player) in infoList) { if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; @@ -127,14 +136,10 @@ namespace src.player.skills if (info.TransmitEntities.Contains(entity.Index)) info.TransmitEntities.Remove(entity.Index); - var bombIndex = GetBombIndex(); - if (bombIndex == null) continue; + if (bomb == null) continue; - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; - - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); + if (info.TransmitEntities.Contains(bomb.Index)) + info.TransmitEntities.Remove(bomb.Index); } } } @@ -212,19 +217,8 @@ namespace src.player.skills particle.AcceptInput("Start"); } - private static uint? GetBombIndex() - { - var bombEntities = Utilities.FindAllEntitiesByDesignerName("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. - private static void ClearSpottedState(CCSPlayerController player) + private static void ClearSpottedState(CCSPlayerController player, CC4? bomb) { var pawn = player.PlayerPawn?.Value; if (pawn != null && pawn.IsValid) @@ -234,7 +228,6 @@ namespace src.player.skills pawn.EntitySpottedState.SpottedByMask[1] = 0; } - var bomb = Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); if (bomb != null && bomb.IsValid && bomb.OwnerEntity?.Index == player.Index) { bomb.EntitySpottedState.Spotted = false; diff --git a/jRandomSkills - SRC Files/src/player/skills/Darkness.cs b/jRandomSkills - SRC Files/src/player/skills/Darkness.cs index dda03b6..af1cda6 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Darkness.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Darkness.cs @@ -21,6 +21,19 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { lock (setLock) diff --git a/jRandomSkills - SRC Files/src/player/skills/Deaf.cs b/jRandomSkills - SRC Files/src/player/skills/Deaf.cs index 290ee23..e9431f1 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Deaf.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Deaf.cs @@ -18,6 +18,16 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { foreach (var playerIndex in deafPlayers.Keys) diff --git a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs index 7799be7..a2b914b 100644 --- a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs +++ b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.Utils; using src.utils; @@ -9,10 +10,23 @@ namespace src.player.skills public class FriendlyFire : ISkill { private const Skills skillName = Skills.FriendlyFire; + private static bool defaultAutoKick = true; + private static bool autoKickOverridden; public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); + + try { defaultAutoKick = ConVar.Find("mp_autokick")?.GetPrimitiveValue() ?? 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) @@ -56,8 +70,12 @@ namespace src.player.skills float damage = param2.Damage; param2.Damage = 0; - Server.ExecuteCommand("mp_autokick 0"); - + if (!autoKickOverridden) + { + autoKickOverridden = true; + Server.ExecuteCommand("mp_autokick 0"); + } + SkillUtils.AddHealth( victimPawn, (int)(damage * SkillsInfo.GetValue(skillName, "healthDamageMultiplier")), diff --git a/jRandomSkills - SRC Files/src/player/skills/Ghost.cs b/jRandomSkills - SRC Files/src/player/skills/Ghost.cs index 741ba66..b8da8c3 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Ghost.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Ghost.cs @@ -56,6 +56,11 @@ namespace src.player.skills public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) { + if (invisiblePlayers.IsEmpty) return; + + var bomb = Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); + uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null; + foreach (var (info, player) in infoList) { if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; @@ -84,14 +89,11 @@ namespace src.player.skills if (info.TransmitEntities.Contains(entity.Index)) info.TransmitEntities.Remove(entity.Index); - var bombIndex = GetBombIndex(playerController); - if (bombIndex == null) continue; + // Hide the bomb as well, but only while this hidden player is the one holding it. + if (bomb == null || bombOwnerIndex != playerController.Index) continue; - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; - - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); + if (info.TransmitEntities.Contains(bomb.Index)) + info.TransmitEntities.Remove(bomb.Index); } } } @@ -232,18 +234,6 @@ namespace src.player.skills playerInfo.PrintHTML = $"{player.GetTranslation("disabled_weapon")}"; } - private static uint? GetBombIndex(CCSPlayerController player) - { - var bombEntities = Utilities.FindAllEntitiesByDesignerName("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) { } diff --git a/jRandomSkills - SRC Files/src/player/skills/Glitch.cs b/jRandomSkills - SRC Files/src/player/skills/Glitch.cs index 0eb45a2..dd90650 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Glitch.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Glitch.cs @@ -18,6 +18,19 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { lock (setLock) diff --git a/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs b/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs index 9c831ec..9cae5c3 100644 --- a/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs +++ b/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs @@ -75,7 +75,7 @@ namespace src.player.skills public static void OnTick() { - int tick = SkillsInfo.GetValue(skillName, "tickCooldown"); + int tick = Math.Max(1, SkillsInfo.GetValue(skillName, "tickCooldown")); if (Server.TickCount % tick != 0) return; float smokeRadius = SkillsInfo.GetValue(skillName, "smokeRadius"); diff --git a/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs b/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs index 5825382..263a456 100644 --- a/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs @@ -20,7 +20,7 @@ namespace src.player.skills public static void OnTick() { - int cooldown = (int)(SkillsInfo.GetValue(skillName, "cooldown") * 64); + int cooldown = Math.Max(1, (int)(SkillsInfo.GetValue(skillName, "cooldown") * 64)); if (Server.TickCount % cooldown != 0) return; if (players.IsEmpty || jRandomSkills.Instance.GameRules?.FreezePeriod == true) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/Iana.cs b/jRandomSkills - SRC Files/src/player/skills/Iana.cs index 71edf82..48e691d 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Iana.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Iana.cs @@ -340,6 +340,8 @@ namespace src.player.skills var victim = PlayerManager.GetPlayerEvent(@event.Userid); if (victim == null || !victim.IsValid) return; + if (PlayerManager.GetPlayerByIndex(victim.Index)?.Skill != skillName) return; + if (playersInfo.TryGetValue(victim.Index, out var playerSkill)) { if (playerSkill.CloneProp != null) diff --git a/jRandomSkills - SRC Files/src/player/skills/Illusionist.cs b/jRandomSkills - SRC Files/src/player/skills/Illusionist.cs index b3f1ded..c721b51 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Illusionist.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Illusionist.cs @@ -161,7 +161,7 @@ namespace src.player.skills replica.Teleport(nexPos, null, null); }, TimerFlags.REPEAT | TimerFlags.STOP_ON_MAPCHANGE); ActiveTimers.TryAdd(replicaIndex, moveTimer); - }); + }, TimerFlags.STOP_ON_MAPCHANGE); float duration = SkillsInfo.GetValue(skillName, ducking ? "durationCrouch" : "durationRun"); Instance.AddTimer(duration, () => diff --git a/jRandomSkills - SRC Files/src/player/skills/Jammer.cs b/jRandomSkills - SRC Files/src/player/skills/Jammer.cs index 272dcc1..3e3ced6 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Jammer.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Jammer.cs @@ -18,6 +18,19 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { lock (setLock) diff --git a/jRandomSkills - SRC Files/src/player/skills/Jester.cs b/jRandomSkills - SRC Files/src/player/skills/Jester.cs index 4b84ebf..9d11fb2 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Jester.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Jester.cs @@ -19,6 +19,16 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { Server.NextWorldUpdate(() => @@ -37,7 +47,7 @@ namespace src.player.skills SkillUtils.ResetPrintHTML(player); 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); pawn.Render = color; @@ -114,11 +124,10 @@ namespace src.player.skills var victim = PlayerManager.GetPlayerEvent(@event.Userid); if (!Instance.IsPlayerValid(victim)) return; - var jesterVictim = GetJesterInfo(victim!.Index); if (!Instance.IsPlayerValid(attacker)) { - if (jesterVictim != null && jesterVictim.Active) + if (IsActiveJester(victim!.Index)) { SkillUtils.RestoreHealth(victim); RestoreArmor(victim, @event.DmgArmor); @@ -126,14 +135,19 @@ namespace src.player.skills return; } - var jesterAttacker = GetJesterInfo(attacker!.Index); - if ((jesterVictim != null && jesterVictim.Active) || (jesterAttacker != null && jesterAttacker.Active)) + if (IsActiveJester(victim!.Index) || IsActiveJester(attacker!.Index)) { SkillUtils.RestoreHealth(victim); 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) { if (victim == null || !victim.IsValid || dmgArmor <= 0) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/JumpBan.cs b/jRandomSkills - SRC Files/src/player/skills/JumpBan.cs index 2026959..8896316 100644 --- a/jRandomSkills - SRC Files/src/player/skills/JumpBan.cs +++ b/jRandomSkills - SRC Files/src/player/skills/JumpBan.cs @@ -17,6 +17,16 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { bannedPlayers.Clear(); diff --git a/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs b/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs index 14ddd91..ab419eb 100644 --- a/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs @@ -31,6 +31,9 @@ namespace src.player.skills public static void NewRound() { playersInAction.Clear(); + + if (!hooked) return; + hooked = false; Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre); } diff --git a/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs b/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs index ac98739..b40af0a 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Magnifier.cs @@ -17,6 +17,16 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { foreach (var playerIndex in playersFOV.Keys) diff --git a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs index 81d7ff5..2789c86 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs @@ -58,6 +58,11 @@ namespace src.player.skills public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) { + if (invisiblePlayers.IsEmpty) return; + + var bomb = Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); + uint? bombOwnerIndex = bomb != null && bomb.IsValid ? bomb.OwnerEntity?.Index : null; + foreach (var (info, player) in infoList) { if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; @@ -86,13 +91,10 @@ namespace src.player.skills if (info.TransmitEntities.Contains(entity.Index)) info.TransmitEntities.Remove(entity.Index); - var bombIndex = GetBombIndex(playerController); - if (bombIndex == null) continue; - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; + if (bomb == null || bombOwnerIndex != playerController.Index) continue; - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); + if (info.TransmitEntities.Contains(bomb.Index)) + info.TransmitEntities.Remove(bomb.Index); } } } @@ -229,18 +231,6 @@ namespace src.player.skills particle.AcceptInput("Start"); } - private static uint? GetBombIndex(CCSPlayerController player) - { - var bombEntities = Utilities.FindAllEntitiesByDesignerName("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 float IdlePercentInvisibility { get; set; } = idlePercentInvisibility; diff --git a/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs b/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs index 9854821..79154c3 100644 --- a/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs +++ b/jRandomSkills - SRC Files/src/player/skills/NoRecoil.cs @@ -2,6 +2,7 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Utils; using src.utils; +using System.Collections.Concurrent; using static src.jRandomSkills; namespace src.player.skills @@ -10,6 +11,9 @@ namespace src.player.skills { private const Skills skillName = Skills.NoRecoil; + private static readonly ConcurrentDictionary holders = []; + private static bool noSpreadActive; + public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); @@ -17,21 +21,33 @@ namespace src.player.skills public static void NewRound() { - var players = Utilities.GetPlayers(); - foreach (var player in players) - DisableSkill(player); + holders.Clear(); + ApplyNoSpread(false); } public static void EnableSkill(CCSPlayerController player) { 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) { 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() diff --git a/jRandomSkills - SRC Files/src/player/skills/Planter.cs b/jRandomSkills - SRC Files/src/player/skills/Planter.cs index 9b6f0f1..bfa09d8 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Planter.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Planter.cs @@ -15,6 +15,7 @@ namespace src.player.skills private static readonly ConcurrentDictionary plantingPlayers = []; // 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 bool c4TimerOverridden; public static void LoadSkill() { @@ -25,6 +26,7 @@ namespace src.player.skills public static void EnableSkill(CCSPlayerController player) { // 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(skillName, "extraC4BlowTime")}"); } @@ -70,6 +72,9 @@ namespace src.player.skills DisableSkill(player); plantingPlayers.Clear(); + if (!c4TimerOverridden) return; + + c4TimerOverridden = false; Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); } diff --git a/jRandomSkills - SRC Files/src/player/skills/Poison.cs b/jRandomSkills - SRC Files/src/player/skills/Poison.cs index 7ce1b45..5351837 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Poison.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Poison.cs @@ -18,6 +18,19 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { lock (setLock) @@ -29,7 +42,7 @@ namespace src.player.skills public static void OnTick() { - int cooldown = (int)(64 * SkillsInfo.GetValue(skillName, "Cooldown")); + int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue(skillName, "Cooldown"))); if (Server.TickCount % cooldown == 0) { @@ -43,7 +56,7 @@ namespace src.player.skills var pawn = player.PlayerPawn.Value; if (pawn == null || !pawn.IsValid) continue; - if (Jester.GetJesterInfo(playerIndex)?.Active == true) continue; + if (Jester.IsActiveJester(playerIndex)) continue; if (pawn.Health <= SkillsInfo.GetValue(skillName, "MinHealth")) continue; SkillUtils.TakeHealth(pawn, SkillsInfo.GetValue(skillName, "Damage")); diff --git a/jRandomSkills - SRC Files/src/player/skills/PrimaryBan.cs b/jRandomSkills - SRC Files/src/player/skills/PrimaryBan.cs index ae2ceb4..fdbc57a 100644 --- a/jRandomSkills - SRC Files/src/player/skills/PrimaryBan.cs +++ b/jRandomSkills - SRC Files/src/player/skills/PrimaryBan.cs @@ -28,6 +28,19 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(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() { lock (setLock) diff --git a/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs b/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs index e0a5562..5ac1620 100644 --- a/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs +++ b/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs @@ -9,6 +9,7 @@ namespace src.player.skills public class RadarHack : ISkill { private const Skills skillName = Skills.RadarHack; + private static readonly Skills[] hidingSkills = [Skills.Ghost, Skills.Ninja, Skills.C4Camouflage]; public static void LoadSkill() { @@ -43,8 +44,9 @@ namespace src.player.skills var enemyPawn = enemyEvent.PlayerPawn.Value; if (enemyPawn == null || !enemyPawn.IsValid) continue; - // Invisibility (low render alpha) beats the radar hack. - if (enemyPawn.Render.A < 200) continue; + var enemyInfo = PlayerManager.GetPlayerByIndex(enemyEvent.Index); + 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. enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32)); diff --git a/jRandomSkills - SRC Files/src/player/skills/ReactiveArmor.cs b/jRandomSkills - SRC Files/src/player/skills/ReactiveArmor.cs index 7e5153b..55774a8 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ReactiveArmor.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ReactiveArmor.cs @@ -45,6 +45,8 @@ namespace src.player.skills if (attacker == null || !attacker.IsValid) 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 (!skillInfo.CanUse) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/Regeneration.cs b/jRandomSkills - SRC Files/src/player/skills/Regeneration.cs index 3d8e51d..abe6edb 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Regeneration.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Regeneration.cs @@ -16,7 +16,8 @@ namespace src.player.skills public static void OnTick() { - if (Server.TickCount % (int)(64 * SkillsInfo.GetValue(skillName, "cooldown")) != 0) return; + int cooldown = Math.Max(1, (int)(64 * SkillsInfo.GetValue(skillName, "cooldown"))); + if (Server.TickCount % cooldown != 0) return; foreach (var player in Utilities.GetPlayers()) { var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); diff --git a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs index 1f1b8d9..511d87a 100644 --- a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs @@ -11,7 +11,7 @@ namespace src.player.skills public class SecondLife : ISkill { private const Skills skillName = Skills.SecondLife; - private static readonly ConcurrentDictionary usedThisRound = []; + private static readonly ConcurrentDictionary usedThisRound = []; private static readonly object setLock = new(); public static void LoadSkill() @@ -45,7 +45,13 @@ namespace src.player.skills if (victimInfo == null || victimInfo.Skill != skillName) 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) { @@ -54,18 +60,18 @@ namespace src.player.skills var spawnpoint = SkillUtils.GetSpawnPointVector(victim); 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; - int startHealth = SkillsInfo.GetValue(skillName, "startHealth"); + victimPawn.Health = SkillsInfo.GetValue(skillName, "startHealth"); + Utilities.SetStateChanged(victimPawn, "CBaseEntity", "m_iHealth"); + Server.NextFrame(() => { if (victim == null || !victim.IsValid) return; var pawn = victim.PlayerPawn.Value; if (pawn == null || !pawn.IsValid) return; - pawn.Health = startHealth; - Utilities.SetStateChanged(pawn, "CBaseEntity", "m_iHealth"); pawn.Teleport(spawnpoint, null, new Vector(0, 0, 0)); }); } diff --git a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs index 845f819..88dd75d 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs @@ -12,6 +12,7 @@ namespace src.player.skills private const Skills skillName = Skills.ShortBomb; // 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 bool c4TimerOverridden; public static void LoadSkill() { @@ -22,11 +23,15 @@ namespace src.player.skills public static void EnableSkill(CCSPlayerController player) { // 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(skillName, "detonationTime")}"); } public static void NewRound() { + if (!c4TimerOverridden) return; + + c4TimerOverridden = false; Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); } diff --git a/jRandomSkills - SRC Files/src/player/skills/Spectator.cs b/jRandomSkills - SRC Files/src/player/skills/Spectator.cs index cc0dc35..dcabd47 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Spectator.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Spectator.cs @@ -59,20 +59,20 @@ namespace src.player.skills foreach (var player in Utilities.GetPlayers()) 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); if (enemy == null || !enemy.IsValid || enemy.PlayerPawn == null) { ChangeCamera(player, true); - return; + continue; } var enemyPawn = enemy.PlayerPawn.Value; if (enemyPawn == null || !enemyPawn.IsValid) { ChangeCamera(player, true); - return; + continue; } 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; 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); } diff --git a/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs b/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs index 385d43c..fcba7e8 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs @@ -75,7 +75,7 @@ namespace src.player.skills public static void OnTick() { - int tick = SkillsInfo.GetValue(skillName, "tickCooldown"); + int tick = Math.Max(1, SkillsInfo.GetValue(skillName, "tickCooldown")); if (Server.TickCount % tick != 0) return; float smokeRadius = SkillsInfo.GetValue(skillName, "smokeRadius"); diff --git a/jRandomSkills - SRC Files/src/player/skills/WildThrow.cs b/jRandomSkills - SRC Files/src/player/skills/WildThrow.cs index 8aaf599..56c0b0d 100644 --- a/jRandomSkills - SRC Files/src/player/skills/WildThrow.cs +++ b/jRandomSkills - SRC Files/src/player/skills/WildThrow.cs @@ -23,6 +23,8 @@ namespace src.player.skills { lock (setLock) { + if (infectedPlayers.IsEmpty && playersToTarget.IsEmpty) return; + foreach (var player in Utilities.GetPlayers()) DisableSkill(player); diff --git a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs index a1de499..26c1409 100644 --- a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs +++ b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs @@ -296,8 +296,8 @@ namespace src.utils var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo == null) return false; - var jester = Jester.GetJesterInfo(player.Index); - if (jester?.Active == true) return false; + if (playerInfo.Skill == Skills.Jester && Jester.GetJesterInfo(player.Index)?.Active == true) + return false; if (playerInfo.Skill == Skills.GodMode && GodMode.HaveHodMode(player.Index)) return false; diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll index 62467b2..d9f207a 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll differ diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll index a60933d..0e0d24a 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll differ