diff --git a/jRandomSkills - SRC Files/jRandomSkills.csproj b/jRandomSkills - SRC Files/jRandomSkills.csproj index c9bf873..0c589f9 100644 --- a/jRandomSkills - SRC Files/jRandomSkills.csproj +++ b/jRandomSkills - SRC Files/jRandomSkills.csproj @@ -15,7 +15,7 @@ - + diff --git a/jRandomSkills - SRC Files/src/gamedata/jRandomSkills.gamedata.json b/jRandomSkills - SRC Files/src/gamedata/jRandomSkills.gamedata.json index 2f7bc51..b4a5330 100644 --- a/jRandomSkills - SRC Files/src/gamedata/jRandomSkills.gamedata.json +++ b/jRandomSkills - SRC Files/src/gamedata/jRandomSkills.gamedata.json @@ -3,14 +3,14 @@ "signatures": { "library": "server", "windows": "48 8B C4 48 89 58 ? 48 89 68 ? 48 89 70 ? 57 41 56 41 57 48 81 EC ? ? ? ? 48 8B B4 24 ? ? ? ? 4D 8B F8", - "linux": "55 4C 89 C1 48 89 E5 41 57 45 89 CF 41 56 49 89 FE" + "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 45 89 CE 41 55 4D 89 C5 41 54 53 48 83 EC ? 48 89 55 ? 48 89 F2 48 89 FE" } }, "HEGrenadeProjectile_CreateFunc": { "signatures": { "library": "server", - "windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B AC 24 ? ? ? ? 49 8B F8", - "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7" + "windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B 6C 24 ? 49 8B F8 4C 8B C2 0F 29 74 24 ? 48 8B D1 48 8B D9 48 8D 0D ? ? ? ? 4C 8B CD E8 ? ? ? ? F3 0F 10 0D ? ? ? ? 48 8B C8 48 8B F0 E8 ? ? ? ? 48 8B D7 48 8B CE", + "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 49 89 D6 48 89 F2 48 89 FE 41 55 48 8D 3D ? ? ? ? 4D 89 C5 41 54 45 89 CC 53" } }, "Shoot_Secondary": { @@ -24,25 +24,7 @@ "signatures": { "library": "server", "windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B DA 48 8B F1 48 8D 55", - "linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC" - } - }, - "CServerSideClient_m_nDeltaTick": { - "offsets": { - "windows": 348, - "linux": 348 - } - }, - "INetworkGameServer_Slots": { - "offsets": { - "windows": 592, - "linux": 592 - } - }, - "INetworkServerService_GetIGameServer": { - "offsets": { - "windows": 23, - "linux": 24 + "linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8" } } -} \ No newline at end of file +} diff --git a/jRandomSkills - SRC Files/src/jRandomSkills.cs b/jRandomSkills - SRC Files/src/jRandomSkills.cs index ae555f3..5dd9c62 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -30,7 +30,7 @@ namespace src public override string ModuleName => "[CS2] [ jRandomSkills ]"; public override string ModuleAuthor => "D3X, Juzlus"; public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus."; - public override string ModuleVersion => "1.2.2.b6"; + public override string ModuleVersion => "1.2.2.b7"; public override void Load(bool hotReload) { @@ -56,6 +56,13 @@ namespace src }); } + public override void Unload(bool hotReload) + { + src.player.PerfLog.Info("===== PLUGIN UNLOAD (clean shutdown/reload) ====="); + Debug.WriteToDebug("===== PLUGIN UNLOAD (clean shutdown/reload) ====="); + base.Unload(hotReload); + } + internal void AddToManifest(string prop) { if (!ManifestResources.Contains(prop)) diff --git a/jRandomSkills - SRC Files/src/player/PerfLog.cs b/jRandomSkills - SRC Files/src/player/PerfLog.cs index 734900c..a63c9ac 100644 --- a/jRandomSkills - SRC Files/src/player/PerfLog.cs +++ b/jRandomSkills - SRC Files/src/player/PerfLog.cs @@ -20,8 +20,7 @@ namespace src.player { if (!Enabled) return 0; - // Write a header on the first measurement so the perf file appears immediately - // when PerfMode is active - makes "is it working?" instantly visible. + // First write creates the file so an active PerfMode is immediately visible. if (!_headerWritten) { _headerWritten = true; @@ -31,6 +30,12 @@ namespace src.player return Stopwatch.GetTimestamp(); } + public static void Info(string message) + { + if (!Enabled) return; + Write(message); + } + // One-shot measurement: logs "label took X.XXms" when the elapsed time reaches the threshold. public static void End(string label, long startTimestamp, double thresholdMs = 1.0) { diff --git a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs index 7bbd0f4..377474d 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs @@ -144,7 +144,15 @@ namespace src.player foreach (var p in Instance.SkillPlayer) { if (p.IsDrawing || !seen.Add(p.Skill)) continue; - Instance.SkillAction(p.Skill.ToString(), methodName, args); + 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}"); + } } } @@ -368,7 +376,7 @@ namespace src.player if (playerInfo == null) return HookResult.Continue; CCSWeaponBaseVData vdata = VirtualFunctions.GetCSWeaponDataFromKeyFunc.Invoke(-1, econItem.ItemDefinitionIndex.ToString()); - if (vdata == null) return HookResult.Continue; + if (vdata == null || vdata.Handle == IntPtr.Zero) return HookResult.Continue; var activeSkills = Instance.SkillPlayer .Where(p => !p.IsDrawing) @@ -507,6 +515,8 @@ namespace src.player var player = PlayerManager.GetPlayerEvent(@event.Userid); if (player == null || !player.IsValid) return HookResult.Continue; + Localization.PreResolveLanguage(player); + string welcomeMsg = player.GetTranslation("welcome_message", "welcome"); foreach (string line in welcomeMsg.Split("\n")) player.PrintToChat($" {ChatColors.Green}" + line.Replace("{PLAYER}", $" {ChatColors.Red}\u202A{player.PlayerName}\u202C{ChatColors.Green}", StringComparison.OrdinalIgnoreCase) @@ -613,6 +623,12 @@ namespace src.player setSkillTimer?.Kill(); + if (isWarmup) + { + setSkillTimer = Instance?.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + return HookResult.Continue; + } + float timeToDraw = (Instance?.GameRules?.TeamIntroPeriod == true ? 7 : 0) + Math.Max(freezetime - Config.LoadedConfig.SkillTimeBeforeStart, 0) + .3f; setSkillTimer = Instance?.AddTimer(timeToDraw, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); return HookResult.Continue; @@ -630,6 +646,9 @@ namespace src.player { lock (setLock) { + // Re-register CheckTransmit so the dying-entity filter covers the kills below. + EnableTransmit(); + Fortnite.skillInThisRound = false; EntityManager.DestroyAllTracked(); @@ -685,10 +704,15 @@ namespace src.player Instance.RemoveListener(CheckTransmit); Fortnite.skillInThisRound = false; + + EntityManager.SuppressKills = true; EntityManager.DestroyAllTracked(); foreach (var skill in SkillData.Skills) Instance.SkillAction(skill.Skill.ToString(), "NewRound"); + EntityManager.SuppressKills = false; + ActiveSkillsThisRound.Clear(); + nextRoundPicks.Clear(); playersSkills.Clear(); staticSkills.Clear(); @@ -740,6 +764,10 @@ namespace src.player } }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + // Before the optional disable below, so the "don't repeat the current skill" + // exclusion still sees this round's skills. + Instance.AddTimer(.6f, PrecomputeNextRoundSkills, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); + if (Config.LoadedConfig.DisableSkillsOnRoundEnd) { isTransmitRegistered = false; @@ -870,6 +898,98 @@ namespace src.player PerfLog.End("SetSkill total", perfStart, 2.0); } + private static readonly Dictionary nextRoundPicks = []; + + private static jSkill_SkillInfo PickSkillForPlayer(CCSPlayerController player, jSkill_PlayerInfo skillPlayer, List validPlayers, Dictionary assignmentCounts, Config.GameModes gameMode) + { + List skillList = [.. SkillData.Skills]; + skillList.RemoveAll(s => s?.Skill == Skills.None); + if (!player.IsBot) + skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue(s.Skill, "requiredPermission"))); + + if (gameMode != Config.GameModes.FullRandom) + skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill); + + if (validPlayers.Count(p => p.Team == player.Team) == 1) + { + SkillsInfo.DefaultSkillInfo[] skillsNeedsTeammates = [.. SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates)]; + skillList.RemoveAll(s => skillsNeedsTeammates.Any(s2 => s2.Name == s.Skill.ToString())); + } + + if (player.Team == CsTeam.Terrorist) + skillList.RemoveAll(s => counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); + else + skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); + + if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag? skills)) + { + skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill)); + if (skillList.Count == 0) skills.Clear(); + } + + var randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode); + + if (gameMode == Config.GameModes.NoRepeat) + { + if (playersSkills.TryGetValue(player.Index, out ConcurrentBag? value)) + value.Add(randomSkill); + else + playersSkills.TryAdd(player.Index, [randomSkill]); + } + + return randomSkill; + } + + private static bool IsPickStillValid(jSkill_SkillInfo pick, CCSPlayerController player, List validPlayers, Dictionary assignmentCounts) + { + if (pick.Skill == Skills.None) return true; + if (!SkillData.Skills.Any(s => s.Skill == pick.Skill)) return false; + + string name = pick.Skill.ToString(); + if (player.Team == CsTeam.Terrorist && counterterroristSkills.Any(s => s.Name == name)) return false; + if (player.Team == CsTeam.CounterTerrorist && terroristSkills.Any(s => s.Name == name)) return false; + + var def = SkillsInfo.LoadedConfig.FirstOrDefault(d => d.Name == name); + if (def == null) return false; + if (def.NeedsTeammates && validPlayers.Count(p => p.Team == player.Team) == 1) return false; + if (def.MaxPerServer >= 0 && assignmentCounts.TryGetValue(pick.Skill, out var c) && c >= def.MaxPerServer) return false; + + return true; + } + + // Runs at round end so the expensive skill selection is off the round-start hot path; + // SetSkillCore then only applies the picks. + private static void PrecomputeNextRoundSkills() + { + long perfStart = PerfLog.Start(); + lock (setLock) + { + nextRoundPicks.Clear(); + + var gameMode = (Config.GameModes)Config.LoadedConfig.GameMode; + if (gameMode is not (Config.GameModes.Normal or Config.GameModes.FullRandom or Config.GameModes.NoRepeat)) return; + if (Instance?.GameRules == null || Instance.GameRules.WarmupPeriod == true) return; + + var validPlayers = Utilities.GetPlayers() + .Where(p => p != null && p.IsValid && !p.IsHLTV) + .Where(p => { try { return p.Team is CsTeam.CounterTerrorist or CsTeam.Terrorist; } catch { return false; } }).ToList(); + + Dictionary assignmentCounts = []; + foreach (var player in validPlayers) + { + var skillPlayer = PlayerManager.GetPlayerByIndex(player.Index); + if (skillPlayer == null) continue; + + var pick = PickSkillForPlayer(player, skillPlayer, validPlayers, assignmentCounts, gameMode); + nextRoundPicks[player.Index] = pick; + + if (pick.Skill != Skills.None) + assignmentCounts[pick.Skill] = assignmentCounts.TryGetValue(pick.Skill, out var c) ? c + 1 : 1; + } + } + PerfLog.End("PrecomputeSkills total", perfStart, 2.0); + } + private static void SetSkillCore() { setSkillTimer = null; @@ -877,11 +997,11 @@ namespace src.player { if (Instance == null) return; - // GameRules can be null for a short window right after plugin load/hot-reload; - // treat that as "not ready" so skills are never assigned during warmup by accident. + // GameRules null = not ready; keep polling so skills land right after warmup ends. if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true) { setSkillTimer?.Kill(); + setSkillTimer = Instance.AddTimer(1f, SetSkill, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); return; } @@ -940,40 +1060,12 @@ namespace src.player Config.GameModes gameMode = (Config.GameModes)Config.LoadedConfig.GameMode; if (gameMode == Config.GameModes.Normal || gameMode == Config.GameModes.FullRandom || gameMode == Config.GameModes.NoRepeat) { - List skillList = [.. SkillData.Skills]; - skillList.RemoveAll(s => s?.Skill == Skills.None); - if (!player.IsBot) - skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue(s.Skill, "requiredPermission"))); - - if (gameMode != Config.GameModes.FullRandom) - skillList.RemoveAll(s => s?.Skill == skillPlayer?.Skill || s?.Skill == skillPlayer?.SpecialSkill); - - if (validPlayers.Count(p => p.Team == player.Team) == 1) - { - SkillsInfo.DefaultSkillInfo[] skillsNeedsTeammates = [.. SkillsInfo.LoadedConfig.Where(s => s.NeedsTeammates)]; - skillList.RemoveAll(s => skillsNeedsTeammates.Any(s2 => s2.Name == s.Skill.ToString())); - } - - if (player.Team == CsTeam.Terrorist) - skillList.RemoveAll(s => counterterroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); + // Prefer the pick made at the end of the previous round; re-pick only when + // it no longer fits (team change, missing player, max reached). + if (nextRoundPicks.TryGetValue(player.Index, out var pre) && IsPickStillValid(pre, player, validPlayers, assignmentCounts)) + randomSkill = pre; else - skillList.RemoveAll(s => terroristSkills.Any(s2 => s2.Name == s.Skill.ToString())); - - if (gameMode == Config.GameModes.NoRepeat && playersSkills.TryGetValue(player.Index, out ConcurrentBag? skills)) - { - skillList.RemoveAll(s => skills.Any(s2 => s2.Skill == s.Skill)); - if (skillList.Count == 0) skills.Clear(); - } - - randomSkill = skillList.Count == 0 ? noneSkill : ChooseSkillByRarityAndMax(skillList, assignmentCounts, gameMode); - - if (gameMode == Config.GameModes.NoRepeat) - { - if (playersSkills.TryGetValue(player.Index, out ConcurrentBag? value)) - value.Add(randomSkill); - else - playersSkills.TryAdd(player.Index, [randomSkill]); - } + randomSkill = PickSkillForPlayer(player, skillPlayer, validPlayers, assignmentCounts, gameMode); } else if (gameMode == Config.GameModes.TeamSkills) randomSkill = player.Team == CsTeam.Terrorist ? tSkill : ctSkill; @@ -1057,6 +1149,8 @@ namespace src.player }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); } } + + nextRoundPicks.Clear(); } } @@ -1178,6 +1272,19 @@ namespace src.player long perfStart = PerfLog.Start(); lock (setLock) { + // Keep dying entities out of snapshots until the engine processes the kill. + var dying = EntityManager.GetRecentlyDestroyedSnapshot(); + if (dying.Count > 0) + { + foreach (var (info, player) in infoList) + { + if (player == null || !player.IsValid) continue; + foreach (var entityIndex in dying) + if (info.TransmitEntities.Contains(entityIndex)) + info.TransmitEntities.Remove(entityIndex); + } + } + DispatchToActiveSkills("CheckTransmit", infoList); } PerfLog.Sample("CheckTransmit", perfStart); diff --git a/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs b/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs index a4ead86..043dba4 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs @@ -17,6 +17,13 @@ namespace src.player UpdateGameRules(); if (Server.TickCount % 2 != 0) return; + if (PerfLog.Enabled && Server.TickCount % 1920 == 0) + { + int server = Utilities.GetAllEntities().Count(e => e != null && e.IsValid); + var (tracked, owners) = EntityManager.GetStatistics(); + PerfLog.Info($"ENTITIES server={server} tracked={tracked} owners={owners}"); + } + long perfStart = PerfLog.Start(); foreach (var player in Utilities.GetPlayers()) { @@ -39,6 +46,8 @@ namespace src.player private static void OnMapEnd() { + PerfLog.Info("===== MAP END (clean map change) ====="); + Debug.WriteToDebug("===== MAP END (clean map change) ====="); BotManager.Stop(); } @@ -63,6 +72,10 @@ namespace src.player { if (player == null || !player.IsValid || player.IsBot) return; + // No skill HUD during warmup or after the match ended. + var gameRules = Instance?.GameRules; + if (gameRules == null || gameRules.WarmupPeriod == true || gameRules.GamePhase >= 5) return; + var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index); if (skillPlayer == null || !skillPlayer.DisplayHUD) return; 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 - SRC Files/src/utils/CServerSideClient.cs b/jRandomSkills - SRC Files/src/utils/CServerSideClient.cs deleted file mode 100644 index 7b50de7..0000000 --- a/jRandomSkills - SRC Files/src/utils/CServerSideClient.cs +++ /dev/null @@ -1,101 +0,0 @@ -// https://discord.com/channels/1160907911501991946/1508172390863994910/1508180670659166348 - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using CounterStrikeSharp.API; -using CounterStrikeSharp.API.Core; -using CounterStrikeSharp.API.Modules.Memory; - -namespace jRandomSkills.src.utils -{ - - #region Native Structs - - [StructLayout(LayoutKind.Sequential)] - public unsafe struct CUtlMemory where T : unmanaged - { - public T* m_pMemory; - public int m_nAllocationCount; - public int m_nGrowSize; - } - - [StructLayout(LayoutKind.Sequential)] - public unsafe struct CUtlVector where T : unmanaged - { - public int m_Size; - public CUtlMemory m_Memory; - - public int Count => m_Size; - - public ref T Element(int index) - { - if (index < 0 || index >= m_Size) - throw new IndexOutOfRangeException(); - - return ref m_Memory.m_pMemory[index]; - } - } - - #endregion - - #region Network Services - - public class INetworkServerService : NativeObject - { - private readonly VirtualFunctionWithReturn GetIGameServerFunc; - - public INetworkServerService() : base(NativeAPI.GetValveInterface(0, "NetworkServerService_001")) - { - GetIGameServerFunc = new VirtualFunctionWithReturn(Handle, GameData.GetOffset("INetworkServerService_GetIGameServer")); - } - - public INetworkGameServer GetIGameServer() - { - return new INetworkGameServer(GetIGameServerFunc.Invoke(Handle)); - } - } - - public unsafe class INetworkGameServer(nint ptr) : NativeObject(ptr) - { - private static readonly int SlotsOffset = GameData.GetOffset("INetworkGameServer_Slots"); - - private ref CUtlVector Slots => ref Unsafe.AsRef>((void*)(Handle + SlotsOffset)); - - public CServerSideClient? GetClientBySlot(int slot) - { - if (slot < 0 || slot >= Slots.Count) - return null; - - var ptr = Slots.Element(slot); - - if (ptr == nint.Zero) - return null; - - return new CServerSideClient(ptr); - } - } - - #endregion - - #region CServerSideClient - - public unsafe class CServerSideClient(nint ptr) : NativeObject(ptr) - { - private static readonly int m_nDeltaTick = GameData.GetOffset("CServerSideClient_m_nDeltaTick"); - - private ref T Field(int offset) where T : unmanaged - { - return ref Unsafe.AsRef((void*)(Handle + offset)); - } - - public int DeltaTick - { - get => Field(m_nDeltaTick); - set => Field(m_nDeltaTick) = value; - } - - public void ForceFullUpdate() => DeltaTick = -1; - } - - #endregion -} \ No newline at end of file diff --git a/jRandomSkills - SRC Files/src/utils/EntityManager.cs b/jRandomSkills - SRC Files/src/utils/EntityManager.cs index 26f9df3..7a0cdb6 100644 --- a/jRandomSkills - SRC Files/src/utils/EntityManager.cs +++ b/jRandomSkills - SRC Files/src/utils/EntityManager.cs @@ -21,6 +21,22 @@ namespace src.utils public DateTime CreatedAt; } + private const int EntityBudget = 3500; + private static int _cachedCount; + private static int _cachedCountTick = -1000000; + + public static bool OverBudget() + { + int tick = Server.TickCount; + if (tick - _cachedCountTick > 64 || tick < _cachedCountTick) + { + _cachedCountTick = tick; + try { _cachedCount = Utilities.GetAllEntities().Count(); } + catch { _cachedCount = 0; } + } + return _cachedCount >= EntityBudget; + } + public static void RegisterEntity(uint entityIndex, uint playerIndex, string entityType) { if (entityIndex == 0) return; @@ -59,6 +75,7 @@ namespace src.utils { try { + if (OverBudget()) return null; var particle = Utilities.CreateEntityByName("info_particle_system"); if (particle == null || !particle.IsValid) return null; @@ -81,6 +98,7 @@ namespace src.utils { try { + if (OverBudget()) return null; var prop = Utilities.CreateEntityByName(designerName); if (prop == null || !prop.IsValid) return null; @@ -103,6 +121,7 @@ namespace src.utils { try { + if (OverBudget()) return null; var shake = Utilities.CreateEntityByName("env_shake"); if (shake == null || !shake.IsValid) return null; @@ -121,6 +140,7 @@ namespace src.utils { try { + if (OverBudget()) return null; var chicken = Utilities.CreateEntityByName("chicken"); if (chicken == null || !chicken.IsValid) return null; @@ -139,6 +159,7 @@ namespace src.utils { try { + if (OverBudget()) return null; var prop = Utilities.CreateEntityByName("prop_physics_multiplayer"); if (prop == null || !prop.IsValid) return null; @@ -158,6 +179,7 @@ namespace src.utils try { + if (OverBudget()) return null; var trigger = Utilities.CreateEntityByName("trigger_multiple"); if (trigger == null || trigger.AbsOrigin == null) return null; @@ -218,16 +240,41 @@ namespace src.utils } } - public static bool DestroyEntity(uint entityIndex) + // Dying entities stay out of transmit until the engine processes the kill (Event.CheckTransmit). + private static readonly ConcurrentDictionary recentlyDestroyed = new(); + + public static List GetRecentlyDestroyedSnapshot() + { + if (recentlyDestroyed.IsEmpty) return []; + + var now = DateTime.UtcNow; + var result = new List(); + foreach (var kvp in recentlyDestroyed) + { + if (now > kvp.Value) recentlyDestroyed.TryRemove(kvp.Key, out _); + else result.Add(kvp.Key); + } + return result; + } + + public static bool SuppressKills = false; + + public static bool DestroyEntity(uint entityIndex, float delay = 0.1f) { trackedEntities.TryRemove(entityIndex, out _); + if (SuppressKills) + return false; + try { var entity = Utilities.GetEntityFromIndex((int)entityIndex); if (entity != null && entity.IsValid) { - entity.AddEntityIOEvent("Kill", entity, delay: 0.1f); + recentlyDestroyed[entityIndex] = DateTime.UtcNow.AddSeconds(delay + 2.0); + // Detach first so no follower is left on a freed parent. + entity.AcceptInput("ClearParent"); + entity.AddEntityIOEvent("Kill", entity, delay: delay); return true; } } @@ -247,14 +294,33 @@ namespace src.utils public static void DestroyPlayerEntities(uint playerIndex) { - foreach (var entityIndex in GetPlayerEntities(playerIndex).ToList()) + // Children die first (reverse creation order), same frame. + var ordered = trackedEntities + .Where(kvp => kvp.Value.PlayerIndex == playerIndex) + .OrderByDescending(kvp => kvp.Value.CreatedAt) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var entityIndex in ordered) DestroyEntity(entityIndex); } public static void DestroyAllTracked() { - foreach (var entityIndex in trackedEntities.Keys.ToList()) - DestroyEntity(entityIndex); + // Stagger between owners; each owner's chain dies child-first in one frame. + int group = 0; + foreach (var owner in trackedEntities.Values.Select(e => e.PlayerIndex).Distinct().ToList()) + { + float delay = 0.1f + (group++ % 16) * 0.03f; + var ordered = trackedEntities + .Where(kvp => kvp.Value.PlayerIndex == owner) + .OrderByDescending(kvp => kvp.Value.CreatedAt) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var entityIndex in ordered) + DestroyEntity(entityIndex, delay); + } trackedEntities.Clear(); } diff --git a/jRandomSkills - SRC Files/src/utils/Localization.cs b/jRandomSkills - SRC Files/src/utils/Localization.cs index bb34b05..6eb28b2 100644 --- a/jRandomSkills - SRC Files/src/utils/Localization.cs +++ b/jRandomSkills - SRC Files/src/utils/Localization.cs @@ -172,6 +172,14 @@ namespace src.utils return key; } + // Resolves and caches the player's language at connect time, so the GeoLite + // database lookup never runs on the tick path. + public static void PreResolveLanguage(CCSPlayerController? player) + { + if (player == null || !player.IsValid || player.IsBot) return; + GetLangCode(player); + } + private static bool _geoLiteBroken = false; private static string GetLangCode(CCSPlayerController? player) diff --git a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs index 218c927..32ffaee 100644 --- a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs +++ b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs @@ -333,76 +333,6 @@ namespace src.utils return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos); } - public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null, INetworkGameServer? networkGameServer = null) - { - if (player == null || !player.IsValid) return; - - var pawn = player.PlayerPawn?.Value; - if (pawn == null || !pawn.IsValid || pawn.AbsOrigin == null) return; - - QAngle lastAngle = new(pawn.V_angle.X, pawn.V_angle.Y, pawn.V_angle.Z); - - networkGameServer ??= new INetworkServerService().GetIGameServer(); - - var client = networkGameServer.GetClientBySlot(player.Slot); - if (client == null) return; - - client.ForceFullUpdate(); - // Only skip the angle restore when the captured view is a spawn-time (0,0,0) placeholder; - // a genuine angle with a single zero component (e.g. yaw exactly 0) must still be restored. - if (lastAngle.X == 0 && lastAngle.Y == 0 && lastAngle.Z == 0) return; - - uint playerIndex = player.Index; - - if (batchList != null) - { - batchList.Add((playerIndex, lastAngle)); - return; - } - - jRandomSkills.Instance.AddTickTimer(3, () => - { - var target = Utilities.GetPlayerFromIndex((int)playerIndex); - if (target == null || !target.IsValid) return; - - var targetPawn = target.PlayerPawn?.Value; - if (targetPawn == null || !targetPawn.IsValid || targetPawn.AbsOrigin == null) return; - - targetPawn.Look(lastAngle); - }); - } - - private static int lastForceFullUpdateAll = int.MinValue; - - public static void ForceFullUpdateToAll() - { - int tickCount = Server.TickCount; - if (tickCount == lastForceFullUpdateAll) return; - - lastForceFullUpdateAll = tickCount; - var playersToRestore = new List<(uint PlayerIndex, QAngle LastAngle)>(); - - INetworkGameServer networkGameServer = new INetworkServerService().GetIGameServer(); - foreach (var player in Utilities.GetPlayers()) - ForceFullUpdate(player, playersToRestore, networkGameServer); - - if (playersToRestore.Count <= 0) return; - - jRandomSkills.Instance.AddTickTimer(3, () => - { - foreach (var item in playersToRestore) - { - var target = Utilities.GetPlayerFromIndex((int)item.PlayerIndex); - if (target == null || !target.IsValid) continue; - - var targetPawn = target.PlayerPawn?.Value; - if (targetPawn == null || !targetPawn.IsValid || targetPawn.AbsOrigin == null) continue; - - targetPawn.Look(item.LastAngle); - } - }); - } - public static bool SetHealth(CCSPlayerPawn? pawn, int newHealth, int? maxHealth = null) { if (pawn == null || !pawn.IsValid) diff --git a/jRandomSkills - Server Files/gamedata/jRandomSkills.gamedata.json b/jRandomSkills - Server Files/gamedata/jRandomSkills.gamedata.json index 2f7bc51..b4a5330 100644 --- a/jRandomSkills - Server Files/gamedata/jRandomSkills.gamedata.json +++ b/jRandomSkills - Server Files/gamedata/jRandomSkills.gamedata.json @@ -3,14 +3,14 @@ "signatures": { "library": "server", "windows": "48 8B C4 48 89 58 ? 48 89 68 ? 48 89 70 ? 57 41 56 41 57 48 81 EC ? ? ? ? 48 8B B4 24 ? ? ? ? 4D 8B F8", - "linux": "55 4C 89 C1 48 89 E5 41 57 45 89 CF 41 56 49 89 FE" + "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 45 89 CE 41 55 4D 89 C5 41 54 53 48 83 EC ? 48 89 55 ? 48 89 F2 48 89 FE" } }, "HEGrenadeProjectile_CreateFunc": { "signatures": { "library": "server", - "windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B AC 24 ? ? ? ? 49 8B F8", - "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 D7" + "windows": "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC ? 48 8B 6C 24 ? 49 8B F8 4C 8B C2 0F 29 74 24 ? 48 8B D1 48 8B D9 48 8D 0D ? ? ? ? 4C 8B CD E8 ? ? ? ? F3 0F 10 0D ? ? ? ? 48 8B C8 48 8B F0 E8 ? ? ? ? 48 8B D7 48 8B CE", + "linux": "55 4C 89 C1 48 89 E5 41 57 49 89 FF 41 56 49 89 D6 48 89 F2 48 89 FE 41 55 48 8D 3D ? ? ? ? 4D 89 C5 41 54 45 89 CC 53" } }, "Shoot_Secondary": { @@ -24,25 +24,7 @@ "signatures": { "library": "server", "windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B DA 48 8B F1 48 8D 55", - "linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC" - } - }, - "CServerSideClient_m_nDeltaTick": { - "offsets": { - "windows": 348, - "linux": 348 - } - }, - "INetworkGameServer_Slots": { - "offsets": { - "windows": 592, - "linux": 592 - } - }, - "INetworkServerService_GetIGameServer": { - "offsets": { - "windows": 23, - "linux": 24 + "linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8" } } -} \ No newline at end of file +} 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