diff --git a/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs b/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs index 3dca3d3..3f8bd4f 100644 --- a/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs +++ b/jRandomSkills - SRC Files/src/player/skills/C4Camouflage.cs @@ -75,6 +75,10 @@ namespace src.player.skills SkillUtils.SetPlayerInvisibility(player, 0); } + // CheckTransmit hides the model but the radar blip comes from spotted state, so clear it every tick. + if (invisiblePlayers.ContainsKey(player.Index)) + ClearSpottedState(player); + var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo?.Skill != skillName) continue; @@ -100,14 +104,6 @@ namespace src.player.skills if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero; - bool isObservingC4Camouflage = false; - - if (targetHandle != nint.Zero) - { - var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle); - var targetInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(target)?.Index); - if (targetInfo?.Skill == skillName) isObservingC4Camouflage = true; - } foreach (var playerIndex in invisiblePlayers.Keys) { @@ -118,26 +114,27 @@ namespace src.player.skills if (player.Team == playerController.Team) continue; - if (!isObservingC4Camouflage) - { - var playerPawn = playerController.PlayerPawn.Value; - if (playerPawn == null || !playerPawn.IsValid) continue; + var playerPawn = playerController.PlayerPawn.Value; + if (playerPawn == null || !playerPawn.IsValid) continue; - var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); - if (entity == null || !entity.IsValid) continue; + // Only the actively spectated pawn stays transmitted; hiding it breaks the camera. + if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle) + continue; - if (info.TransmitEntities.Contains(entity.Index)) - info.TransmitEntities.Remove(entity.Index); + var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); + if (entity == null || !entity.IsValid) continue; - var bombIndex = GetBombIndex(); - if (bombIndex == null) continue; + if (info.TransmitEntities.Contains(entity.Index)) + info.TransmitEntities.Remove(entity.Index); - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; + var bombIndex = GetBombIndex(); + if (bombIndex == null) continue; - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); - } + var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); + if (bombEntity == null || !bombEntity.IsValid) continue; + + if (info.TransmitEntities.Contains(bombEntity.Index)) + info.TransmitEntities.Remove(bombEntity.Index); } } } @@ -162,8 +159,6 @@ namespace src.player.skills invisiblePlayers.TryAdd(player.Index, 0); SkillUtils.SetPlayerInvisibility(player, .5f); - - SkillUtils.ForceFullUpdateToAll(); } public static void DisableSkill(CCSPlayerController player) @@ -171,8 +166,6 @@ namespace src.player.skills invisiblePlayers.TryRemove(player.Index, out _); SkillUtils.SetPlayerInvisibility(player, 0); EntityManager.DestroyPlayerEntities(player.Index); - - SkillUtils.ForceFullUpdateToAll(); } private static void CreatePlayerPosProp(CCSPlayerController player) @@ -228,6 +221,26 @@ namespace src.player.skills return bomb.Index; } + // Wipe spotted state (pawn + carried bomb) so a disguised carrier produces no radar blip. + private static void ClearSpottedState(CCSPlayerController player) + { + var pawn = player.PlayerPawn?.Value; + if (pawn != null && pawn.IsValid) + { + pawn.EntitySpottedState.Spotted = false; + pawn.EntitySpottedState.SpottedByMask[0] = 0; + 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; + bomb.EntitySpottedState.SpottedByMask[0] = 0; + bomb.EntitySpottedState.SpottedByMask[1] = 0; + } + } + public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#00911f", CsTeam onlyTeam = CsTeam.Terrorist, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = -1, Rarity rarity = Rarity.Uncommon) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity) { } diff --git a/jRandomSkills - SRC Files/src/player/skills/Ghost.cs b/jRandomSkills - SRC Files/src/player/skills/Ghost.cs index 7aad102..5535eb3 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Ghost.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Ghost.cs @@ -14,7 +14,7 @@ namespace src.player.skills { private const Skills skillName = Skills.Ghost; private static readonly string[] allowedWeapons = [ - "weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet" + "weapon_molotov", "weapon_incgrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_decoy", "weapon_hegrenade", "weapon_knife", "weapon_bayonet", "weapon_c4" ]; private static readonly ConcurrentDictionary invisiblePlayers = []; private const string bloodParticle = "particles/blood_impact/blood_impact_high.vpcf"; @@ -61,14 +61,6 @@ namespace src.player.skills if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero; - bool isObservingGhost = false; - - if (targetHandle != nint.Zero) - { - var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle); - var targetInfo = PlayerManager.GetPlayerByIndex(target?.Index); - if (targetInfo?.Skill == skillName) isObservingGhost = true; - } foreach (var playerIndex in invisiblePlayers.Keys) { @@ -79,26 +71,27 @@ namespace src.player.skills if (player.Team == playerController.Team) continue; - if (!isObservingGhost) - { - var playerPawn = playerController.PlayerPawn.Value; - if (playerPawn == null || !playerPawn.IsValid) continue; + var playerPawn = playerController.PlayerPawn.Value; + if (playerPawn == null || !playerPawn.IsValid) continue; - var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); - if (entity == null || !entity.IsValid) continue; + // Only the actively spectated pawn stays transmitted; hiding it breaks the camera. + if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle) + continue; - if (info.TransmitEntities.Contains(entity.Index)) - info.TransmitEntities.Remove(entity.Index); + var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); + if (entity == null || !entity.IsValid) continue; - var bombIndex = GetBombIndex(playerController); - if (bombIndex == null) continue; + if (info.TransmitEntities.Contains(entity.Index)) + info.TransmitEntities.Remove(entity.Index); - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; + var bombIndex = GetBombIndex(playerController); + if (bombIndex == null) continue; - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); - } + var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); + if (bombEntity == null || !bombEntity.IsValid) continue; + + if (info.TransmitEntities.Contains(bombEntity.Index)) + info.TransmitEntities.Remove(bombEntity.Index); } } } @@ -113,8 +106,6 @@ namespace src.player.skills if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0) CreatePlayerPosProp(player); - - SkillUtils.ForceFullUpdateToAll(); } private static void CreatePlayerPosProp(CCSPlayerController player) @@ -150,8 +141,6 @@ namespace src.player.skills invisiblePlayers.TryRemove(player.Index, out _); EntityManager.DestroyPlayerEntities(player.Index); - - SkillUtils.ForceFullUpdateToAll(); } public static void OnTick() diff --git a/jRandomSkills - SRC Files/src/player/skills/Glaz.cs b/jRandomSkills - SRC Files/src/player/skills/Glaz.cs index 4fc33b7..82901ca 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Glaz.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Glaz.cs @@ -111,8 +111,6 @@ namespace src.player.skills SkillUtils.TryGiveWeapon(player, CsItem.SmokeGrenade); SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, grenadeLimit); - - SkillUtils.ForceFullUpdateToAll(); } public static void DisableSkill(CCSPlayerController player) @@ -121,8 +119,6 @@ namespace src.player.skills playersWithSkill.TryRemove(player.Index, out _); SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, 1); - - SkillUtils.ForceFullUpdateToAll(); } public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#5d00ff", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = -1, Rarity rarity = Rarity.Common, int grenadeLimit = 2) : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity) diff --git a/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs b/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs index f6cdfc7..455eb73 100644 --- a/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/HotBomb.cs @@ -44,12 +44,18 @@ namespace src.player.skills public static void WeaponPickup(EventItemPickup @event) { - var player = PlayerManager.GetPlayerEvent(@event.Userid); - if (player == null || !player.IsValid) return; - var weapon = @event.Item; if (string.IsNullOrEmpty(weapon) || weapon != "c4") return; + // Gate on the bomb being hot (red), not on `players`: the fresh round's C4 is + // handed out before NewRound clears `players`, but a new C4 spawns white, so colour is race-free. + var bomb = Utilities.FindAllEntitiesByDesignerName("weapon_c4").FirstOrDefault(); + if (bomb == null || !bomb.IsValid) return; + if (bomb.Render.R != 255 || bomb.Render.G != 0 || bomb.Render.B != 0) return; + + var player = PlayerManager.GetPlayerEvent(@event.Userid); + if (player == null || !player.IsValid) return; + player.PrintToCenterAlert(player.GetTranslation("hotbomb_hint")); } @@ -100,7 +106,7 @@ namespace src.player.skills { ChangeC4Color(Color.White); foreach (var enemy in Utilities.GetPlayers().Where(p => p.IsValid && p.Team == CsTeam.Terrorist && p.PlayerPawn?.Value?.Health > 0)) - enemy.PrintToCenterAlert(enemy.GetTranslation("hotbomb_disable")); + enemy.PrintToCenterAlert(enemy.GetTranslation("hotbomb_disable_info")); } } diff --git a/jRandomSkills - SRC Files/src/player/skills/Jackal.cs b/jRandomSkills - SRC Files/src/player/skills/Jackal.cs index ebcbf26..716d1c6 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Jackal.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Jackal.cs @@ -98,6 +98,9 @@ namespace src.player.skills var relay = EntityManager.CreateTrackedPhysicsProp(player.Index); if (relay == null || !relay.IsValid) return; + // Register before parenting so the trail is filtered from its first snapshot. + activeTrails[player.Index] = relay.Index; + CBaseEntity target = playerPawn; var entities = EntityManager.GetPlayerEntities(player.Index, "empty_prop"); @@ -123,8 +126,6 @@ namespace src.player.skills particle.AcceptInput("SetParent", relay, particle, "!activator"); particle.AcceptInput("Start"); } - - activeTrails[player.Index] = relay.Index; } public static void EnableSkill(CCSPlayerController player) @@ -144,8 +145,6 @@ namespace src.player.skills mainSkillTimer ??= Instance.AddTimer(2.5f, () => UpdateAllTrails(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT | CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); - - SkillUtils.ForceFullUpdateToAll(); } private static void UpdateAllTrails() @@ -168,8 +167,6 @@ namespace src.player.skills mainSkillTimer.Kill(); mainSkillTimer = null; } - - SkillUtils.ForceFullUpdateToAll(); } public class SkillConfig(Skills skill = skillName, bool active = true, string color = "#f542ef", CsTeam onlyTeam = CsTeam.None, bool disableOnFreezeTime = false, bool needsTeammates = false, string requiredPermission = "", int maxPerServer = 1, Rarity rarity = Rarity.Common, string particleName = "particles/ui/hud/ui_map_def_utility_trail.vpcf") : SkillsInfo.DefaultSkillInfo(skill, active, color, onlyTeam, disableOnFreezeTime, needsTeammates, requiredPermission, maxPerServer, rarity) diff --git a/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs b/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs index 9847792..a7530ef 100644 --- a/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/LongKnife.cs @@ -15,7 +15,13 @@ namespace src.player.skills private static bool hooked = false; private const int actionCode = 503; private static readonly ConcurrentDictionary playersInAction = []; - private static readonly MemoryFunctionVoid Shoot_Secondary = new(GameData.GetSignature("Shoot_Secondary")); + private static readonly MemoryFunctionVoid? Shoot_Secondary = ResolveShootSecondary(); + + private static MemoryFunctionVoid? ResolveShootSecondary() + { + try { return new(GameData.GetSignature("Shoot_Secondary")); } + catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] Shoot_Secondary signature unresolved: {ex.Message}"); return null; } + } public static void LoadSkill() { @@ -26,12 +32,12 @@ namespace src.player.skills { playersInAction.Clear(); hooked = false; - Shoot_Secondary.Unhook(ShootSecondary, HookMode.Pre); + Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre); } public static void EnableSkill(CCSPlayerController player) { - if (hooked) return; + if (hooked || Shoot_Secondary == null) return; hooked = true; Shoot_Secondary.Hook(ShootSecondary, HookMode.Pre); playersInAction.TryAdd(player.Index, 0); @@ -42,7 +48,7 @@ namespace src.player.skills playersInAction.TryRemove(player.Index, out _); if (playersInAction.IsEmpty) { - Shoot_Secondary.Unhook(ShootSecondary, HookMode.Pre); + Shoot_Secondary?.Unhook(ShootSecondary, HookMode.Pre); hooked = false; } } diff --git a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs index 02d3d66..8c133d4 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs @@ -63,14 +63,6 @@ namespace src.player.skills if (player == null || !player.IsValid || player.Team == CsTeam.Spectator) continue; var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget.Value?.Handle ?? nint.Zero; - bool isObservingNinja = false; - - if (targetHandle != nint.Zero) - { - var target = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == targetHandle); - var targetInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(target)?.Index); - if (targetInfo?.Skill == skillName) isObservingNinja = true; - } foreach (var playerIndex in invisiblePlayers.Keys) { @@ -81,25 +73,26 @@ namespace src.player.skills if (player.Team == playerController.Team) continue; - if (!isObservingNinja) - { - var playerPawn = playerController.PlayerPawn.Value; - if (playerPawn == null || !playerPawn.IsValid) continue; + var playerPawn = playerController.PlayerPawn.Value; + if (playerPawn == null || !playerPawn.IsValid) continue; - var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); - if (entity == null || !entity.IsValid) continue; + // Only the actively spectated pawn stays transmitted; hiding it breaks the camera. + if (targetHandle != nint.Zero && playerPawn.Handle == targetHandle) + continue; - if (info.TransmitEntities.Contains(entity.Index)) - info.TransmitEntities.Remove(entity.Index); + var entity = Utilities.GetEntityFromIndex((int)playerPawn.Index); + if (entity == null || !entity.IsValid) continue; - var bombIndex = GetBombIndex(playerController); - if (bombIndex == null) continue; - var bombEntity = Utilities.GetEntityFromIndex((int)bombIndex); - if (bombEntity == null || !bombEntity.IsValid) continue; + if (info.TransmitEntities.Contains(entity.Index)) + info.TransmitEntities.Remove(entity.Index); - if (info.TransmitEntities.Contains(bombEntity.Index)) - info.TransmitEntities.Remove(bombEntity.Index); - } + var bombIndex = GetBombIndex(playerController); + if (bombIndex == 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); } } } @@ -139,8 +132,6 @@ namespace src.player.skills if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0) CreatePlayerPosProp(player); - - SkillUtils.ForceFullUpdateToAll(); } public static void DisableSkill(CCSPlayerController player) @@ -148,8 +139,6 @@ namespace src.player.skills SkillUtils.SetPlayerInvisibility(player, 0); invisiblePlayers.TryRemove(player.Index, out _); EntityManager.DestroyPlayerEntities(player.Index); - - SkillUtils.ForceFullUpdateToAll(); } private static void CreatePlayerPosProp(CCSPlayerController player) diff --git a/jRandomSkills - SRC Files/src/player/skills/Planter.cs b/jRandomSkills - SRC Files/src/player/skills/Planter.cs index e7f5a78..75c91cd 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Planter.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Planter.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Memory; using CounterStrikeSharp.API.Modules.Utils; using src.utils; @@ -12,10 +13,19 @@ namespace src.player.skills { private const Skills skillName = Skills.Planter; 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; public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); + defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue() ?? 40; + } + + public static void EnableSkill(CCSPlayerController player) + { + // At round start (not at plant) so the client HUD/alert countdown is right before the plant completes. + Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue(skillName, "extraC4BlowTime")}"); } public static void BombBeginplant(EventBombBeginplant @event) @@ -59,6 +69,8 @@ namespace src.player.skills foreach (var player in Utilities.GetPlayers()) DisableSkill(player); plantingPlayers.Clear(); + + Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); } public static void DisableSkill(CCSPlayerController player) diff --git a/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs b/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs index ae6ecc9..c30f308 100644 --- a/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs +++ b/jRandomSkills - SRC Files/src/player/skills/RadarHack.cs @@ -31,7 +31,9 @@ namespace src.player.skills private static void SetEnemiesVisibleOnRadar(CCSPlayerController player) { if (player == null || !player.IsValid || player.PlayerPawn?.Value == null) return; - int playerIndex = (int)player.Index - 1; + + // SpottedByMask is indexed by player slot (0-63), not entity index. + int slot = player.Slot; foreach (var enemy in Utilities.GetPlayers().FindAll(p => p.Team != player.Team)) { @@ -39,17 +41,21 @@ namespace src.player.skills if (enemyEvent == null || !enemyEvent.IsValid) continue; var enemyPawn = enemyEvent.PlayerPawn.Value; - if (enemyPawn == null) continue; + if (enemyPawn == null || !enemyPawn.IsValid) continue; - enemyPawn.EntitySpottedState.SpottedByMask[0] |= (1u << (int)(playerIndex % 32)); + // Invisibility (low render alpha) beats the radar hack. + if (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)); } - + var bombEntities = Utilities.FindAllEntitiesByDesignerName("weapon_c4").ToList(); if (bombEntities.Count != 0) { var bomb = bombEntities.FirstOrDefault(); if (bomb != null && bomb.IsValid) - bomb.EntitySpottedState.SpottedByMask[0] |= (1u << (int)(playerIndex % 32)); + bomb.EntitySpottedState.SpottedByMask[0] |= (1u << (slot % 32)); } } diff --git a/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs b/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs index 3a046b8..7894bca 100644 --- a/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs +++ b/jRandomSkills - SRC Files/src/player/skills/RichBoy.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using static src.jRandomSkills; @@ -15,6 +16,8 @@ namespace src.player.skills SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); } + private static int GetMaxMoney() => ConVar.Find("mp_maxmoney")?.GetPrimitiveValue() ?? 16000; + public static void EnableSkill(CCSPlayerController player) { var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); @@ -24,7 +27,7 @@ namespace src.player.skills var moneyServices = player.InGameMoneyServices; if (moneyServices == null) return; - moneyBonus = Math.Min(moneyBonus, 16000 - moneyServices.Account); + moneyBonus = Math.Min(moneyBonus, GetMaxMoney() - moneyServices.Account); playerInfo.SkillChance = moneyBonus; AddMoney(player, moneyBonus); @@ -48,7 +51,7 @@ namespace src.player.skills var moneyServices = player.InGameMoneyServices; if (moneyServices == null) return; - moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, 16000); + moneyServices.Account = Math.Clamp(moneyServices.Account + money, minimum, GetMaxMoney()); Utilities.SetStateChanged(player, "CCSPlayerController", "m_pInGameMoneyServices"); } diff --git a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs index c791b62..e586642 100644 --- a/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/SecondLife.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; using CounterStrikeSharp.API.Modules.Utils; using static src.jRandomSkills; using System.Collections.Concurrent; @@ -10,7 +11,7 @@ namespace src.player.skills public class SecondLife : ISkill { private const Skills skillName = Skills.SecondLife; - private static readonly ConcurrentDictionary secondLifePlayers = []; + private static readonly ConcurrentDictionary usedThisRound = []; private static readonly object setLock = new(); public static void LoadSkill() @@ -20,30 +21,53 @@ namespace src.player.skills public static void NewRound() { - secondLifePlayers.Clear(); + usedThisRound.Clear(); } - public static void PlayerHurt(EventPlayerHurt @event) + // Must intercept pre-damage: player_hurt fires after the death is committed, so a + // lethal hit (esp. fall damage) can't be undone there. Cancelling the blow here keeps the respawn clean. + public static void OnTakeDamage(DynamicHook h) { - var victim = PlayerManager.GetPlayerEvent(@event.Userid); + var victimEntity = h.GetParam(0); + var info = h.GetParam(1); + if (victimEntity == null || !victimEntity.IsValid || info == null) return; - if (!Instance.IsPlayerValid(victim)) return; - var victimInfo = PlayerManager.GetPlayerByIndex(victim!.Index); + var victimPawn = victimEntity.As(); + if (victimPawn == null || !victimPawn.IsValid || victimPawn.DesignerName != "player") return; + + var victimController = victimPawn.Controller.Value; + if (victimController == null || !victimController.IsValid) return; + + var victim = victimController.As(); + if (victim == null || !victim.IsValid || !victim.PawnIsAlive) return; + + var victimInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index)); if (victimInfo == null || victimInfo.Skill != skillName) return; - var victimPawn = victim!.PlayerPawn.Value; - if (victimPawn!.Health > 0 || (secondLifePlayers.TryGetValue(victim.Handle, out int tick) && tick + 4 < Server.TickCount)) - return; + if (info.Damage < victimPawn.Health) return; + if (usedThisRound.ContainsKey(victim.Handle)) return; lock (setLock) { - SetHealth(victim, SkillsInfo.GetValue(skillName, "startHealth")); + if (usedThisRound.ContainsKey(victim.Handle)) return; var spawnpoint = SkillUtils.GetSpawnPointVector(victim); - if (spawnpoint == null) return; + if (spawnpoint == null) return; // no clean respawn point -> let the normal death happen - victimPawn.Teleport(spawnpoint, null, null); - secondLifePlayers.TryAdd(victim.Handle, Server.TickCount); + usedThisRound.TryAdd(victim.Handle, 0); + info.Damage = 0; + + int startHealth = SkillsInfo.GetValue(skillName, "startHealth"); + 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)); + }); } } @@ -54,7 +78,7 @@ namespace src.player.skills public static void DisableSkill(CCSPlayerController player) { - secondLifePlayers.TryRemove(player.Handle, out _); + usedThisRound.TryRemove(player.Handle, out _); if (player.PlayerPawn.Value == null) return; SetHealth(player, Math.Min(player.PlayerPawn.Value.Health + SkillsInfo.GetValue(skillName, "startHealth"), 100)); } @@ -73,4 +97,4 @@ namespace src.player.skills public int StartHealth { get; set; } = startHealth; } } -} \ No newline at end of file +} diff --git a/jRandomSkills - SRC Files/src/player/skills/Shade.cs b/jRandomSkills - SRC Files/src/player/skills/Shade.cs index e4c8b9a..cf41ecb 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Shade.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Shade.cs @@ -83,7 +83,7 @@ namespace src.player.skills var attackerPawn = attacker.PlayerPawn.Value; if (attackerPawn == null || !attackerPawn.IsValid) return false; - var victimPawn = attacker.PlayerPawn.Value; + var victimPawn = victim.PlayerPawn.Value; if (victimPawn == null || !victimPawn.IsValid) return false; var result = RayTrace.TraceHullShape( @@ -123,7 +123,11 @@ namespace src.player.skills Vector direction = SkillUtils.GetForwardVector(targetAngle); Vector targetPos = victimPos - (direction * distance); - if (CheckTeleport(attacker, victim, victimPos, targetPos, targetAngle)) + // Trace can only ignore one entity (victim); start a step out so the attacker's + // own body in melee range doesn't clip the hull and report a false "no space". + Vector traceStart = victimPos - (direction * 20f); + + if (CheckTeleport(attacker, victim, traceStart, targetPos, targetAngle)) { attackerPawn.Teleport(targetPos, targetAngle, Vector.Zero); teleported = true; diff --git a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs index 9191387..edcd603 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ShortBomb.cs @@ -1,5 +1,6 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Cvars; using CounterStrikeSharp.API.Modules.Utils; using src.utils; using static src.jRandomSkills; @@ -9,10 +10,24 @@ namespace src.player.skills public class ShortBomb : ISkill { 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; public static void LoadSkill() { SkillUtils.RegisterSkill(skillName, SkillsInfo.GetValue(skillName, "color")); + defaultC4Timer = ConVar.Find("mp_c4timer")?.GetPrimitiveValue() ?? 40; + } + + public static void EnableSkill(CCSPlayerController player) + { + // At round start (not at plant) so the client HUD/alert countdown is right before the plant completes. + Server.ExecuteCommand($"mp_c4timer {SkillsInfo.GetValue(skillName, "detonationTime")}"); + } + + public static void NewRound() + { + Server.ExecuteCommand($"mp_c4timer {defaultC4Timer}"); } public static void BombPlanted(EventBombPlanted @event) diff --git a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs index 947e857..4b0fd36 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs @@ -95,6 +95,10 @@ namespace src.player.skills public static bool OnWeaponCanAcquire(DynamicHook hook, CCSPlayerController player, CEconItemView econItem, CCSWeaponBaseVData vdata) { + // vdata can be non-null yet point at freed/invalid schema memory; reading + // .Name then throws NullReferenceException inside get_Name(). Guard the handle. + if (vdata == null || vdata.Handle == IntPtr.Zero) return false; + string weaponName = vdata.Name; if (string.IsNullOrEmpty(weaponName)) return false; @@ -123,15 +127,12 @@ namespace src.player.skills public static void EnableSkill(CCSPlayerController _) { Event.EnableTransmit(); - SkillUtils.ForceFullUpdateToAll(); } public static void DisableSkill(CCSPlayerController player) { if (knivesInfo.TryRemove(player.Index, out KnifeInfo? knifeInfo) && knifeInfo != null) DisableKnifeSkill(knifeInfo); - - SkillUtils.ForceFullUpdateToAll(); } public static void DisableKnifeSkill(KnifeInfo knifeInfo) diff --git a/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs b/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs index fe99fb0..361498f 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs @@ -28,19 +28,30 @@ namespace src.player.skills temporaryBlockList.TryRemove(kvp.Key, out _); } + // One pawn->info map per frame instead of a GetPlayers scan per client. + var infoByPawnHandle = new Dictionary(); + foreach (var p in Utilities.GetPlayers()) + { + var h = p?.Pawn?.Value?.Handle; + if (p != null && p.IsValid && h != null) + infoByPawnHandle[h.Value] = PlayerManager.GetPlayerByIndex(p.Index); + } + foreach (var (info, player) in infoList) { if (player == null || !player.IsValid) continue; var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); - var observedPlayer = Utilities.GetPlayers().FirstOrDefault(p => p?.Pawn?.Value?.Handle == player?.Pawn?.Value?.ObserverServices?.ObserverTarget?.Value?.Handle); - var observerInfo = observedPlayer != null ? PlayerManager.GetPlayerByIndex(observedPlayer.Index) : null; + var targetHandle = player.Pawn.Value?.ObserverServices?.ObserverTarget?.Value?.Handle ?? nint.Zero; + jSkill_PlayerInfo? observerInfo = null; + if (targetHandle != nint.Zero) + infoByPawnHandle.TryGetValue(targetHandle, out observerInfo); foreach (var kvp in glows) { var enemyIndex = kvp.Key; var glowInfo = kvp.Value; - var enemy = Utilities.GetPlayers().FirstOrDefault(e => e != null && e.IsValid && e.Index == enemyIndex); + var enemy = Utilities.GetPlayerFromIndex((int)enemyIndex); bool hasSkill = playerInfo?.Skill == skillName; bool observerHasSkill = observerInfo != null && observerInfo?.Skill == skillName; @@ -85,8 +96,9 @@ namespace src.player.skills var relayIndex = kvp.Value.RelayIndex; var glowIndex = kvp.Value.GlowIndex; - SkillUtils.SafeKillEntity(relayIndex); + // The glow follows the relay, so it dies first. SkillUtils.SafeKillEntity(glowIndex); + SkillUtils.SafeKillEntity(relayIndex); temporaryBlockList.TryAdd(relayIndex, DateTime.Now.AddSeconds(2)); temporaryBlockList.TryAdd(glowIndex, DateTime.Now.AddSeconds(2)); @@ -103,8 +115,6 @@ namespace src.player.skills if (glows.IsEmpty) SetGlowEffectForAll(); - - SkillUtils.ForceFullUpdateToAll(); } public static void DisableSkill(CCSPlayerController player) @@ -116,8 +126,6 @@ namespace src.player.skills NewRound(); return; } - - SkillUtils.ForceFullUpdateToAll(); } private static void SetGlowEffectForAll() @@ -149,6 +157,9 @@ namespace src.player.skills if (modelGlow == null || !modelGlow.IsValid || modelRelay == null || !modelRelay.IsValid) continue; + // Register before spawn so the glow is filtered from its first snapshot. + glows.TryAdd(enemy.Index, (modelRelay.Index, modelGlow.Index, enemy.Team)); + var relayEntity = modelRelay.CBodyComponent?.SceneNode?.Owner?.Entity; if (relayEntity != null) relayEntity.Flags = (uint)(relayEntity.Flags & ~(1 << 2)); @@ -180,8 +191,6 @@ namespace src.player.skills modelRelay.AcceptInput("FollowEntity", enemyPawn, modelRelay, "!activator"); modelGlow.AcceptInput("FollowEntity", modelRelay, modelGlow, "!activator"); - - glows.TryAdd(enemy.Index, (modelRelay.Index, modelGlow.Index, enemy.Team)); } } diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll index c5c63ec..a22e1b0 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 27bb56a..cfc8ef6 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll differ