diff --git a/README.md b/README.md index 2cfa8bd..271a267 100644 --- a/README.md +++ b/README.md @@ -374,6 +374,29 @@ This plugin uses content from the following projects: ## 📋 Changelog +
+v1.2.2.b6 + +- #### General: + - ###### Performance: much faster round changes - only skills that were actually active in the previous round are reset, instead of every loaded skill (`DisableAll` dropped from ~89ms to ~5ms on a full server). [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Performance & memory: removed per-tick LINQ and skill-name allocations in the tick loop, made HUD skill lookups O(1), and moved translation color/button substitutions to load time. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### `ForceFullUpdate` now creates the network service once per broadcast, and the round-start view-angle guard only skips a genuine (0, 0, 0) spawn placeholder instead of over-skipping valid angles. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Debug logging is only installed when `DebugMode` is enabled and writes through a single reused writer instead of reopening the file for every line. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Player skill state unified to a single collection (removed a duplicate list), closing a rare desync/race on player disconnect. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Gamedata signatures are now resolved lazily and in isolation, so one broken signature after a CS2 update no longer takes down the whole utility layer. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Hardened many tick/transmit code paths against invalid player handles to prevent rare crashes. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### The skill description HUD honours "always show" / `-1` duration consistently across every assignment path. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + +- #### Skill improvements: + - ###### Jester (Joker): + - ###### Fixed the no-damage effect leaking onto a player after their skill was changed mid-round (e.g. stolen by Thief or removed by Deactivator) - the internal state is now cleared when the skill is disabled. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Jackal: + - ###### Fixed trail entities piling up over a round - the previous trail is now removed before a new one is spawned. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + - ###### Falcon Eye / Third Eye / Spectator: + - ###### Camera state is now fully cleared when the skill is disabled. [by: [@ByDexterTR](https://github.com/ByDexterTR)] + +
+
v1.2.2.b5 diff --git a/WASDMenuAPI - SRC Files/WASDMenuAPI.cs b/WASDMenuAPI - SRC Files/WASDMenuAPI.cs index b1b1529..96b8735 100644 --- a/WASDMenuAPI - SRC Files/WASDMenuAPI.cs +++ b/WASDMenuAPI - SRC Files/WASDMenuAPI.cs @@ -14,7 +14,6 @@ public class WASDMenuAPI public static void LoadPlugin(BasePlugin basePlugin, bool hotReload) { - var wasdMenuManager = new WasdManager(); basePlugin.RegisterEventHandler((@event, info) => { if (@event.Userid != null) @@ -46,7 +45,7 @@ public class WASDMenuAPI public static void OnTick() { - foreach (var player in Players.Values.Where(p => p.MainMenu != null)) + foreach (var player in Players.Values.Where(p => p.MainMenu != null && p.Player?.IsValid == true)) { if ((player.Buttons & PlayerButtons.Forward) == 0 && (player.Player.Buttons & PlayerButtons.Forward) != 0) { diff --git a/jRandomSkills - SRC Files/jRandomSkills.csproj b/jRandomSkills - SRC Files/jRandomSkills.csproj index 33a5330..c9bf873 100644 --- a/jRandomSkills - SRC Files/jRandomSkills.csproj +++ b/jRandomSkills - SRC Files/jRandomSkills.csproj @@ -71,7 +71,9 @@ + + diff --git a/jRandomSkills - SRC Files/src/command/Command.cs b/jRandomSkills - SRC Files/src/command/Command.cs index f49cbc5..38604c1 100644 --- a/jRandomSkills - SRC Files/src/command/Command.cs +++ b/jRandomSkills - SRC Files/src/command/Command.cs @@ -78,7 +78,7 @@ namespace src.command player = PlayerManager.GetPlayerEvent(player); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index); if (playerInfo == null || playerInfo.IsDrawing) return; var playerPawn = player.PlayerPawn.Value; @@ -150,7 +150,7 @@ namespace src.command skillPlayer.Skill = skill.Skill; skillPlayer.SpecialSkill = Skills.None; Instance.SkillAction(skill.Skill.ToString(), "EnableSkill", [targetPlayer]); - skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); + skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); if (player == null) { @@ -406,7 +406,7 @@ namespace src.command if (player == null || !player.IsValid || player.PlayerPawn.Value == null || !player.PlayerPawn.Value.IsValid) return; if (!string.IsNullOrEmpty(config.NormalCommands.HudCommand.Permissions) && !AdminManager.PlayerHasPermissions(player, config.NormalCommands.HudCommand.Permissions)) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index); if (playerInfo == null) return; playerInfo.DisplayHUD = !playerInfo.DisplayHUD; @@ -504,7 +504,7 @@ namespace src.command Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [targetPlayer]); skillPlayer.Skill = skill.Skill; skillPlayer.SpecialSkill = src.player.Skills.None; - skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); + skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); if (skill.Skill == src.player.Skills.None) Event.staticSkills.TryRemove(targetPlayer.Index, out _); @@ -570,6 +570,9 @@ namespace src.command if (SkillsInfo.GetValue(skill, "active")) Instance.SkillAction(skill.ToString()!, "LoadSkill"); + SkillData.Invalidate(); + Event.InvalidateFreezeDisabledCache(); + if (player != null && player.IsValid) player.PrintToChat($" {ChatColors.Green}{player.GetTranslation("reload")}"); else diff --git a/jRandomSkills - SRC Files/src/jRandomSkills.cs b/jRandomSkills - SRC Files/src/jRandomSkills.cs index 3756395..ae555f3 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -19,16 +19,18 @@ namespace src #pragma warning disable CS8618 public static jRandomSkills Instance { get; private set; } #pragma warning restore CS8618 - public ConcurrentBag SkillPlayer { get; set; } = []; + public IEnumerable SkillPlayer => PlayerManager.GetAllPlayers(); public Random Random { get; } = new Random(); public CCSGameRules? GameRules { get; set; } private ConcurrentBag ManifestResources { get; set; } = ["models/sprays/spray_plane.vmdl"]; public IWasdMenuManager? MenuManager; + // Skills that were enabled at least once this round; used to reset only those on round change (not all 124). + public static readonly ConcurrentDictionary ActiveSkillsThisRound = new(); public 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.b5"; + public override string ModuleVersion => "1.2.2.b6"; public override void Load(bool hotReload) { @@ -85,6 +87,9 @@ namespace src if (string.IsNullOrEmpty(skill)) return null; + if (methodName == "EnableSkill") + ActiveSkillsThisRound.TryAdd(skill, 0); + var method = _skillMethodCache.GetOrAdd((skill, methodName), key => { string className = $"src.player.skills.{key.Skill}"; @@ -109,7 +114,15 @@ namespace src return type.GetMethod(key.Method, BindingFlags.Static | BindingFlags.Public); }); - return method?.Invoke(null, param); + if (method == null) return null; + + if (!PerfLog.Enabled) + return method.Invoke(null, param); + + long perfStart = PerfLog.Start(); + var result = method.Invoke(null, param); + PerfLog.End($"SkillAction {skill}.{methodName}", perfStart, 2.0); + return result; } internal new void AddCommand(string name, string description, CommandInfo.CommandCallback handler) @@ -292,12 +305,9 @@ namespace src } } - /// - /// Gets a player info by index. O(n) lookup - consider caching if called frequently. - /// internal jSkill_PlayerInfo? GetPlayerInfoByIndex(uint playerIndex) { - return SkillPlayer.FirstOrDefault(p => p.PlayerIndex == playerIndex); + return PlayerManager.GetPlayerByIndex(playerIndex); } } @@ -329,6 +339,23 @@ namespace src public static class SkillData { public static ConcurrentBag Skills { get; } = []; + + private static Dictionary? _bySkill; + + public static jSkill_SkillInfo? GetInfo(Skills skill) + { + var map = _bySkill; + if (map == null) + { + map = new Dictionary(); + foreach (var s in Skills) + map[s.Skill] = s; + _bySkill = map; + } + return map.TryGetValue(skill, out var info) ? info : null; + } + + public static void Invalidate() => _bySkill = null; } public enum CS2ConsoleColors diff --git a/jRandomSkills - SRC Files/src/player/Debug.cs b/jRandomSkills - SRC Files/src/player/Debug.cs index c6b91fc..89e451a 100644 --- a/jRandomSkills - SRC Files/src/player/Debug.cs +++ b/jRandomSkills - SRC Files/src/player/Debug.cs @@ -13,10 +13,17 @@ namespace src.player { private static string sessionId = "00000"; private static readonly string debugFolder = Path.Combine(Instance.ModuleDirectory, "logs"); + private static StreamWriter? _writer; + private static readonly object _writeLock = new(); public static void Load() { sessionId = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}"; + lock (_writeLock) { _writer?.Dispose(); _writer = null; } + + if (Config.LoadedConfig.DebugMode != true) + return; + Instance.RegisterEventHandler((@event, info) => { var player = PlayerManager.GetPlayerEvent(@event.Userid); @@ -131,13 +138,25 @@ namespace src.player if (Config.LoadedConfig.DebugMode != true) return; - // GetAllEntityIndexes(); + lock (_writeLock) + { + _writer ??= CreateWriter(); + _writer?.WriteLine($"[{DateTime.Now:dd.MM.yyyy HH:mm:ss}] {message}"); + } + } - string filename = $"debug_{sessionId}.txt"; - string path = Path.Combine(debugFolder, filename); - - Directory.CreateDirectory(debugFolder); - File.AppendAllText(path, $"[{DateTime.Now:dd.MM.yyyy HH:mm:ss}] {message}{Environment.NewLine}", System.Text.Encoding.UTF8); + private static StreamWriter? CreateWriter() + { + try + { + Directory.CreateDirectory(debugFolder); + string path = Path.Combine(debugFolder, $"debug_{sessionId}.txt"); + return new StreamWriter(path, append: true, System.Text.Encoding.UTF8) { AutoFlush = true }; + } + catch + { + return null; + } } private static void GetAllEntityIndexes() diff --git a/jRandomSkills - SRC Files/src/player/PerfLog.cs b/jRandomSkills - SRC Files/src/player/PerfLog.cs new file mode 100644 index 0000000..734900c --- /dev/null +++ b/jRandomSkills - SRC Files/src/player/PerfLog.cs @@ -0,0 +1,100 @@ +using src.utils; +using System.Collections.Concurrent; +using System.Diagnostics; +using static src.jRandomSkills; + +namespace src.player +{ + public static class PerfLog + { + private static readonly string logsFolder = Path.Combine(Instance.ModuleDirectory, "logs"); + private static readonly string sessionId = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}"; + private static StreamWriter? _writer; + private static readonly object _writeLock = new(); + + public static bool Enabled => Config.LoadedConfig.PerfMode; + + private static bool _headerWritten; + + public static long Start() + { + 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. + if (!_headerWritten) + { + _headerWritten = true; + Write($"PerfMode enabled (plugin v{Instance.ModuleVersion})"); + } + + return Stopwatch.GetTimestamp(); + } + + // 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) + { + if (startTimestamp == 0 || !Enabled) return; + + double ms = (Stopwatch.GetTimestamp() - startTimestamp) * 1000.0 / Stopwatch.Frequency; + if (ms < thresholdMs) return; + + Write($"{label} took {ms:F2}ms"); + } + + private sealed class Aggregate + { + public double TotalMs; + public double MaxMs; + public int Count; + public DateTime WindowStart = DateTime.Now; + } + + private static readonly ConcurrentDictionary _aggregates = new(); + + // Per-tick measurement: accumulates and logs an avg/max summary every few seconds, + // so tick paths do not produce one log line per tick. + public static void Sample(string label, long startTimestamp, double reportSeconds = 5.0, double maxThresholdMs = 0.5) + { + if (startTimestamp == 0 || !Enabled) return; + + double ms = (Stopwatch.GetTimestamp() - startTimestamp) * 1000.0 / Stopwatch.Frequency; + var agg = _aggregates.GetOrAdd(label, _ => new Aggregate()); + lock (agg) + { + agg.TotalMs += ms; + agg.Count++; + if (ms > agg.MaxMs) agg.MaxMs = ms; + + if ((DateTime.Now - agg.WindowStart).TotalSeconds < reportSeconds) return; + + if (agg.MaxMs >= maxThresholdMs) + Write($"{label} avg={agg.TotalMs / agg.Count:F2}ms max={agg.MaxMs:F2}ms samples={agg.Count}"); + + agg.TotalMs = 0; + agg.MaxMs = 0; + agg.Count = 0; + agg.WindowStart = DateTime.Now; + } + } + + private static void Write(string message) + { + lock (_writeLock) + { + try + { + if (_writer == null) + { + Directory.CreateDirectory(logsFolder); + _writer = new StreamWriter(Path.Combine(logsFolder, $"perf_{sessionId}.txt"), append: true, System.Text.Encoding.UTF8) { AutoFlush = true }; + } + _writer.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] [PERF] {message}"); + } + catch + { + } + } + } + } +} diff --git a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs index fc124ac..7bbd0f4 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs @@ -417,23 +417,53 @@ namespace src.player } } + private static readonly Dictionary _skillNames = + Enum.GetValues().ToDictionary(s => s, s => s.ToString()); + private static readonly HashSet _activeSkillsSet = []; + private static readonly List _activeSkillsList = []; + private static readonly Comparison _tickOrderCmp = (a, b) => TickOrder(a).CompareTo(TickOrder(b)); + private static HashSet? _freezeDisabledSkills; + + // AreaReaper and ChillOut depend on other skills' tick results, so they must tick last. + private static int TickOrder(Skills s) => s == Skills.AreaReaper ? 2 : s == Skills.ChillOut ? 1 : 0; + + private static HashSet BuildFreezeDisabledSkills() + { + var set = new HashSet(); + foreach (var s in SkillData.Skills) + if (SkillsInfo.GetValue(s.Skill, "disableOnFreezeTime")) + set.Add(s.Skill); + return set; + } + + public static void InvalidateFreezeDisabledCache() => _freezeDisabledSkills = null; + private static void OnTick() { + long perfStart = PerfLog.Start(); lock (setLock) { - var activeSkills = Instance.SkillPlayer - .Where(p => !p.IsDrawing) - .Select(p => p.Skill) - .Distinct() - .OrderBy(skill => skill.ToString() == "AreaReaper") - .ThenBy(skill => skill.ToString() == "ChillOut"); + _activeSkillsSet.Clear(); + _activeSkillsList.Clear(); + foreach (var p in Instance.SkillPlayer) + { + if (p.IsDrawing) continue; + if (_activeSkillsSet.Add(p.Skill)) + _activeSkillsList.Add(p.Skill); + } - foreach (var skill in activeSkills) - if (SkillsInfo.GetValue(skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime()) - continue; - else - Instance.SkillAction(skill.ToString(), "OnTick"); + _activeSkillsList.Sort(_tickOrderCmp); + + bool freeze = SkillUtils.IsFreezeTime(); + _freezeDisabledSkills ??= BuildFreezeDisabledSkills(); + + foreach (var skill in _activeSkillsList) + { + if (freeze && _freezeDisabledSkills.Contains(skill)) continue; + Instance.SkillAction(_skillNames[skill], "OnTick"); + } } + PerfLog.Sample("OnTick(skills)", perfStart); } private static void OnPlayerConnectedBot(int playerSlot) @@ -466,7 +496,6 @@ namespace src.player DisplayHUD = true, SkillUsed = false, }; - Instance.SkillPlayer.Add(playerInfo); PlayerManager.Register(playerInfo); } } @@ -502,8 +531,6 @@ namespace src.player Instance.SkillAction(skillPlayer.Skill.ToString(), "DisableSkill", [player]); - var items = Instance.SkillPlayer.ToList(); - Instance.SkillPlayer = [.. items.Where(p => p.PlayerIndex != player.Index)]; PlayerManager.UnregisterPlayer(player.Index); EntityManager.DestroyPlayerEntities(player.Index); @@ -568,7 +595,7 @@ namespace src.player { lock (setLock) { - bool isWarmup = Instance.GameRules != null && Instance.GameRules.WarmupPeriod == true; + bool isWarmup = Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true; isTransmitRegistered = false; Instance.AddTimer(.1f, () => DisableAll(), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); @@ -593,6 +620,13 @@ namespace src.player } private static void DisableAll() + { + long perfStart = PerfLog.Start(); + DisableAllCore(); + PerfLog.End("DisableAll total", perfStart, 2.0); + } + + private static void DisableAllCore() { lock (setLock) { @@ -606,6 +640,10 @@ namespace src.player var playerInfo = PlayerManager.GetPlayerByIndex(player!.Index); if (playerInfo == null) continue; + ActiveSkillsThisRound.TryAdd(playerInfo.Skill.ToString(), 0); + if (playerInfo.SpecialSkill != noneSkill.Skill) + ActiveSkillsThisRound.TryAdd(playerInfo.SpecialSkill.ToString(), 0); + Instance.SkillAction(playerInfo.Skill.ToString(), "DisableSkill", [player]); playerInfo.Skill = noneSkill.Skill; @@ -617,8 +655,9 @@ namespace src.player RestorePlayer(player); } - foreach (var skill in SkillData.Skills) - Instance.SkillAction(skill.Skill.ToString(), "NewRound"); + foreach (var skillName in ActiveSkillsThisRound.Keys) + Instance.SkillAction(skillName, "NewRound"); + ActiveSkillsThisRound.Clear(); } } @@ -649,6 +688,7 @@ namespace src.player EntityManager.DestroyAllTracked(); foreach (var skill in SkillData.Skills) Instance.SkillAction(skill.Skill.ToString(), "NewRound"); + ActiveSkillsThisRound.Clear(); playersSkills.Clear(); staticSkills.Clear(); @@ -657,7 +697,6 @@ namespace src.player tSkill = noneSkill; allSkill = noneSkill; - Instance.SkillPlayer.Clear(); PlayerManager.Clear(); ConVar.Find("sv_legacy_jump")?.SetValue("1"); @@ -712,6 +751,14 @@ namespace src.player } private static HookResult PlayerDeath(EventPlayerDeath @event, GameEventInfo info) + { + long perfStart = PerfLog.Start(); + var result = PlayerDeathCore(@event, info); + PerfLog.End("PlayerDeath total", perfStart, 2.0); + return result; + } + + private static HookResult PlayerDeathCore(EventPlayerDeath @event, GameEventInfo info) { lock (setLock) { @@ -817,13 +864,22 @@ namespace src.player } private static void SetSkill() + { + long perfStart = PerfLog.Start(); + SetSkillCore(); + PerfLog.End("SetSkill total", perfStart, 2.0); + } + + private static void SetSkillCore() { setSkillTimer = null; lock (setLock) { if (Instance == null) return; - if (Instance.GameRules != null && Instance.GameRules.WarmupPeriod == true) + // 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. + if (Instance.GameRules == null || Instance.GameRules.WarmupPeriod == true) { setSkillTimer?.Kill(); return; @@ -1113,16 +1169,18 @@ namespace src.player }, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE); Debug.WriteToDebug($"Player {skillPlayer.PlayerName} has got the skill \"{player.GetSkillName(randomSkill.Skill)}\"."); - skillPlayer.SkillDescriptionHudExpired = DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); + skillPlayer.SkillDescriptionHudExpired = Config.LoadedConfig.SkillDescriptionDuration == -1 ? DateTime.MaxValue : DateTime.Now.AddSeconds(Config.LoadedConfig.SkillDescriptionDuration); } } public static void CheckTransmit([CastFrom(typeof(nint))] CCheckTransmitInfoList infoList) { + long perfStart = PerfLog.Start(); lock (setLock) { DispatchToActiveSkills("CheckTransmit", infoList); } + PerfLog.Sample("CheckTransmit", perfStart); } public static void UpdateSkillHUD(CCSPlayerController? player, string? headerLine, string? centerLine, string? extraLine, bool isDescription) diff --git a/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs b/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs index 3f87eae..a4ead86 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerOnTick.cs @@ -17,11 +17,13 @@ namespace src.player UpdateGameRules(); if (Server.TickCount % 2 != 0) return; + long perfStart = PerfLog.Start(); foreach (var player in Utilities.GetPlayers()) { if (player != null && player.IsValid) UpdatePlayerHud(player); } + PerfLog.Sample("OnTick(hud)", perfStart); }); Instance.RegisterListener(OnMapStart); @@ -61,7 +63,7 @@ namespace src.player { if (player == null || !player.IsValid || player.IsBot) return; - var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var skillPlayer = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index); if (skillPlayer == null || !skillPlayer.DisplayHUD) return; var now = DateTime.Now; @@ -72,7 +74,7 @@ namespace src.player string skillLine = string.Empty; string remainingLine = string.Empty; - bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now; + bool showDescriptionHUD = skillPlayer.SkillDescriptionHudExpired >= now || Config.LoadedConfig.DisplayAlwaysDescription; bool isDescription = true; var skills = SkillData.Skills; @@ -99,7 +101,7 @@ namespace src.player { if (player.PawnIsAlive) { - var skillInfo = skills.FirstOrDefault(s => s.Skill == skillPlayer.Skill); + var skillInfo = SkillData.GetInfo(skillPlayer.Skill); if (skillInfo != null) { @@ -139,9 +141,9 @@ namespace src.player var observedSkill = PlayerManager.GetPlayerByIndex(observedEvent.Index); if (observedSkill == null) return; - var observedSkillInfo = skills.FirstOrDefault(s => s.Skill == observedSkill.Skill); + var observedSkillInfo = SkillData.GetInfo(observedSkill.Skill); var observedSpecialInfo = observedSkill.SpecialSkill != Skills.None - ? skills.FirstOrDefault(s => s.Skill == observedSkill.SpecialSkill) + ? SkillData.GetInfo(observedSkill.SpecialSkill) : null; string primaryName = player.GetSkillName(observedSkill.Skill, observedSkill.SkillChance); diff --git a/jRandomSkills - SRC Files/src/player/skills/Armored.cs b/jRandomSkills - SRC Files/src/player/skills/Armored.cs index 4530d17..e073309 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Armored.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Armored.cs @@ -47,7 +47,7 @@ namespace src.player.skills var victim = victimController.As(); if (victim == null || !victim.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(victim)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(victim)?.Index ?? victim.Index)); if (playerInfo == null) return; if (playerInfo.Skill == skillName && victim.PawnIsAlive) diff --git a/jRandomSkills - SRC Files/src/player/skills/Baseball.cs b/jRandomSkills - SRC Files/src/player/skills/Baseball.cs index 8f8ccb3..0fb60b3 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Baseball.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Baseball.cs @@ -36,7 +36,7 @@ namespace src.player.skills if (victim.Index == attacker.Index || victim.Team == attacker.Team) return; - var attackerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index); + var attackerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(attacker)?.Index ?? attacker.Index)); if (attackerInfo?.Skill != skillName) return; SkillUtils.TakeHealth(victim!.PlayerPawn.Value, SkillsInfo.GetValue(skillName, "damageDeal")); @@ -57,7 +57,7 @@ namespace src.player.skills var player = pawn.Controller.Value.As(); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; decoys.TryAdd(decoy.Index, 0); diff --git a/jRandomSkills - SRC Files/src/player/skills/BunnyHop.cs b/jRandomSkills - SRC Files/src/player/skills/BunnyHop.cs index 71067d5..f84ac51 100644 --- a/jRandomSkills - SRC Files/src/player/skills/BunnyHop.cs +++ b/jRandomSkills - SRC Files/src/player/skills/BunnyHop.cs @@ -22,7 +22,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) continue; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill == skillName) GiveBunnyHop(player); } diff --git a/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs b/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs index 30b4a0b..3be3ab0 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ChillOut.cs @@ -77,7 +77,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) continue; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo == null) continue; var pawn = player.PlayerPawn.Value; diff --git a/jRandomSkills - SRC Files/src/player/skills/FalconEye.cs b/jRandomSkills - SRC Files/src/player/skills/FalconEye.cs index f4528a2..2c36ca1 100644 --- a/jRandomSkills - SRC Files/src/player/skills/FalconEye.cs +++ b/jRandomSkills - SRC Files/src/player/skills/FalconEye.cs @@ -53,6 +53,7 @@ namespace src.player.skills if (player == null) return; ChangeCamera(player, true); EntityManager.DestroyPlayerEntities(player.Index); + cameras.TryRemove(player.Index, out _); } public static void OnTick() diff --git a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs index a7abce8..6a2a9c2 100644 --- a/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs +++ b/jRandomSkills - SRC Files/src/player/skills/FriendlyFire.cs @@ -50,7 +50,7 @@ namespace src.player.skills var victim = victimController.As(); if (victim == null || !victim.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(attacker)?.Index ?? attacker.Index)); if (playerInfo?.Skill != skillName || attacker!.Team != victim!.Team) return; float damage = param2.Damage; diff --git a/jRandomSkills - SRC Files/src/player/skills/Glaz.cs b/jRandomSkills - SRC Files/src/player/skills/Glaz.cs index a2a1f04..4fc33b7 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Glaz.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Glaz.cs @@ -42,7 +42,7 @@ namespace src.player.skills foreach (var (info, player) in infoList) { if (player == null) continue; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + 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; diff --git a/jRandomSkills - SRC Files/src/player/skills/Glue.cs b/jRandomSkills - SRC Files/src/player/skills/Glue.cs index 52c8268..97fa351 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Glue.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Glue.cs @@ -28,7 +28,7 @@ namespace src.player.skills var player = pawn.Controller.Value.As(); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; grenade.Bounces = 555; } diff --git a/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs b/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs index e1f1ad0..9f862d8 100644 --- a/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs +++ b/jRandomSkills - SRC Files/src/player/skills/HealingSmoke.cs @@ -60,7 +60,7 @@ namespace src.player.skills var player = pawn.Controller.Value.As(); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; Server.NextFrame(() => diff --git a/jRandomSkills - SRC Files/src/player/skills/HomingNades.cs b/jRandomSkills - SRC Files/src/player/skills/HomingNades.cs index 03ea2b3..7c5ca89 100644 --- a/jRandomSkills - SRC Files/src/player/skills/HomingNades.cs +++ b/jRandomSkills - SRC Files/src/player/skills/HomingNades.cs @@ -127,7 +127,7 @@ namespace src.player.skills if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return; var player = pawn.Controller.Value.As(); - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; Vector pos = new(grenade.AbsOrigin?.X, grenade.AbsOrigin?.Y, grenade.AbsOrigin?.Z); diff --git a/jRandomSkills - SRC Files/src/player/skills/Jackal.cs b/jRandomSkills - SRC Files/src/player/skills/Jackal.cs index 79e13eb..ebcbf26 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Jackal.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Jackal.cs @@ -46,7 +46,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) continue; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); bool isJackalOwner = playerInfo?.Skill == skillName; if (!isJackalOwner) @@ -90,6 +90,11 @@ namespace src.player.skills if (playerPawn == null || !playerPawn.IsValid || playerPawn.AbsOrigin == null || playerPawn.LifeState != (byte)LifeState_t.LIFE_ALIVE || playerPawn.Health <= 0) return; ; + foreach (var old in EntityManager.GetPlayerEntities(player.Index, "prop_physics_multiplayer")) + EntityManager.DestroyEntity(old); + foreach (var old in EntityManager.GetPlayerEntities(player.Index, "particle_system")) + EntityManager.DestroyEntity(old); + var relay = EntityManager.CreateTrackedPhysicsProp(player.Index); if (relay == null || !relay.IsValid) return; diff --git a/jRandomSkills - SRC Files/src/player/skills/Jester.cs b/jRandomSkills - SRC Files/src/player/skills/Jester.cs index 04014c0..2a23f48 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Jester.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Jester.cs @@ -53,16 +53,18 @@ namespace src.player.skills var jester = GetJesterInfo(player.Index); if (jester == null) return; + jester.Active = false; + if (jester.Timer != null) + { + jester.Timer.Kill(); + jester.Timer = null; + } + Server.NextWorldUpdate(() => { - if (jester.Timer != null) - { - jester.Timer?.Kill(); - jester.Timer = null; - } - SkillUtils.ResetPrintHTML(player); SetPlayerColor(player, true); + jesters.TryRemove(player.Index, out _); }); } diff --git a/jRandomSkills - SRC Files/src/player/skills/Miner.cs b/jRandomSkills - SRC Files/src/player/skills/Miner.cs index a0acdee..9e4b933 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Miner.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Miner.cs @@ -88,7 +88,7 @@ namespace src.player.skills if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return; var player = pawn.Controller.Value.As(); - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; nades.TryAdd(grenade.Index, 0); diff --git a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs index 96ec055..02d3d66 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Ninja.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Ninja.cs @@ -39,7 +39,7 @@ namespace src.player.skills var player = @event.Userid; if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; UpdateNinja(player); @@ -50,7 +50,7 @@ namespace src.player.skills var player = @event.Userid; if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; UpdateNinja(player); @@ -118,7 +118,7 @@ namespace src.player.skills UpdateNinja(PlayerManager.GetPlayerFromEvent(player)); - var props = EntityManager.GetPlayerEntities(PlayerManager.GetPlayerEvent(player)!.Index, "empty_prop"); + var props = EntityManager.GetPlayerEntities((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index), "empty_prop"); if (props.Count == 0) continue; var prop = Utilities.GetEntityFromIndex((int)props[0]); diff --git a/jRandomSkills - SRC Files/src/player/skills/OneShot.cs b/jRandomSkills - SRC Files/src/player/skills/OneShot.cs index e3e0c23..2d6a64f 100644 --- a/jRandomSkills - SRC Files/src/player/skills/OneShot.cs +++ b/jRandomSkills - SRC Files/src/player/skills/OneShot.cs @@ -32,9 +32,10 @@ namespace src.player.skills if (attackerPawn == null || attackerPawn.Controller?.Value == null || victimPawn == null || victimPawn.Controller?.Value == null) return; - CCSPlayerController attacker = PlayerManager.GetPlayerEvent(attackerPawn.Controller.Value.As())!; + var attacker = PlayerManager.GetPlayerEvent(attackerPawn.Controller.Value.As()); + if (attacker == null || !attacker.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(attacker)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex(attacker.Index); if (playerInfo == null) return; if (playerInfo.Skill == skillName) diff --git a/jRandomSkills - SRC Files/src/player/skills/Silent.cs b/jRandomSkills - SRC Files/src/player/skills/Silent.cs index a255218..f378286 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Silent.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Silent.cs @@ -27,7 +27,7 @@ namespace src.player.skills var player = Utilities.GetPlayers().FirstOrDefault(p => p.Pawn?.Value != null && p.Pawn.Value.IsValid && p.Pawn.Value.Index == userIndex); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; um.Recipients.Clear(); diff --git a/jRandomSkills - SRC Files/src/player/skills/Spectator.cs b/jRandomSkills - SRC Files/src/player/skills/Spectator.cs index f3aaa2c..698dfea 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Spectator.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Spectator.cs @@ -51,6 +51,7 @@ namespace src.player.skills if (player == null) return; ChangeCamera(player, true); EntityManager.DestroyPlayerEntities(player.Index); + cameras.TryRemove(player.Index, out _); } public static void OnTick() diff --git a/jRandomSkills - SRC Files/src/player/skills/ThirdEye.cs b/jRandomSkills - SRC Files/src/player/skills/ThirdEye.cs index 8fc906e..dcc91b7 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ThirdEye.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ThirdEye.cs @@ -39,6 +39,7 @@ namespace src.player.skills if (player == null) return; ChangeCamera(player, true); EntityManager.DestroyPlayerEntities(player.Index); + cameras.TryRemove(player.Index, out _); } public static void OnTick() diff --git a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs index 6ce287a..947e857 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ThrowingKnife.cs @@ -388,7 +388,7 @@ namespace src.player.skills continue; } - bool isOwner = knifeOwnerIndex == PlayerManager.GetPlayerEvent(player)!.Index; + bool isOwner = knifeOwnerIndex == (PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index); bool isObservingOwner = observedPlayerIndex.HasValue && observedPlayerIndex.Value == knifeOwnerIndex; if (!isOwner && !isObservingOwner) diff --git a/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs b/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs index 135a541..07c0f79 100644 --- a/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs +++ b/jRandomSkills - SRC Files/src/player/skills/ToxicSmoke.cs @@ -60,7 +60,7 @@ namespace src.player.skills var player = pawn.Controller.Value.As(); if (player == null || !player.IsValid) return; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; Server.NextFrame(() => diff --git a/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs b/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs index 98c6563..fe99fb0 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Wallhack.cs @@ -32,7 +32,7 @@ namespace src.player.skills { if (player == null || !player.IsValid) continue; - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + 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; diff --git a/jRandomSkills - SRC Files/src/player/skills/Watchmaker.cs b/jRandomSkills - SRC Files/src/player/skills/Watchmaker.cs index 66401d4..3245b89 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Watchmaker.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Watchmaker.cs @@ -39,7 +39,7 @@ namespace src.player.skills if (pawn == null || !pawn.IsValid || pawn.Controller == null || !pawn.Controller.IsValid || pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return; var player = pawn.Controller.Value.As(); - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName || Instance.GameRules == null) return; var roundTime = SkillsInfo.GetValue(skillName, "changeRoundTime"); diff --git a/jRandomSkills - SRC Files/src/player/skills/Weightless.cs b/jRandomSkills - SRC Files/src/player/skills/Weightless.cs index ff10464..48ecb7a 100644 --- a/jRandomSkills - SRC Files/src/player/skills/Weightless.cs +++ b/jRandomSkills - SRC Files/src/player/skills/Weightless.cs @@ -62,7 +62,7 @@ namespace src.player.skills if (pawn.Controller.Value == null || !pawn.Controller.Value.IsValid) return; var player = pawn.Controller.Value.As(); - var playerInfo = PlayerManager.GetPlayerByIndex(PlayerManager.GetPlayerEvent(player)!.Index); + var playerInfo = PlayerManager.GetPlayerByIndex((PlayerManager.GetPlayerEvent(player)?.Index ?? player.Index)); if (playerInfo?.Skill != skillName) return; Server.NextFrame(() => { diff --git a/jRandomSkills - SRC Files/src/utils/Config.cs b/jRandomSkills - SRC Files/src/utils/Config.cs index a333f40..9104558 100644 --- a/jRandomSkills - SRC Files/src/utils/Config.cs +++ b/jRandomSkills - SRC Files/src/utils/Config.cs @@ -80,6 +80,7 @@ namespace src.utils public bool EnableBotSkills { get; set; } public bool EnableBotKickDebug { get; set; } public bool DebugMode { get; set; } + public bool PerfMode { get; set; } public string? AlternativeSkillButton { get; set; } public float SkillTimeBeforeStart { get; set; } public float SkillHudDuration { get; set; } @@ -107,6 +108,7 @@ namespace src.utils EnableBotSkills = true; EnableBotKickDebug = false; DebugMode = false; + PerfMode = false; AlternativeSkillButton = null; SkillTimeBeforeStart = 7; SkillHudDuration = -1; diff --git a/jRandomSkills - SRC Files/src/utils/Localization.cs b/jRandomSkills - SRC Files/src/utils/Localization.cs index d90a39b..bb34b05 100644 --- a/jRandomSkills - SRC Files/src/utils/Localization.cs +++ b/jRandomSkills - SRC Files/src/utils/Localization.cs @@ -55,7 +55,18 @@ namespace src.utils var translations = JsonConvert.DeserializeObject>(jsonText); if (translations != null) + { + string redColor = ChatColors.Red.ToString(); + string? altButton = Config.LoadedConfig.AlternativeSkillButton; + foreach (var tkey in translations.Keys.ToList()) + { + var val = translations[tkey].Replace("CHATCOLORS.RED", redColor); + if (!string.IsNullOrEmpty(altButton)) + val = val.Replace("css_useSkill", $"css_useSkill/{altButton}"); + translations[tkey] = val; + } _translations.AddOrUpdate(code, translations, (k, v) => translations); + } } public static bool HasTranslation(string code) @@ -150,9 +161,6 @@ namespace src.utils else { string output = args.Length == 0 ? translation : string.Format(translation, args); - output = output.Replace("CHATCOLORS.RED", ChatColors.Red.ToString()); - if (Config.LoadedConfig.AlternativeSkillButton != null) - output = output.Replace("css_useSkill", $"css_useSkill/{Config.LoadedConfig.AlternativeSkillButton}"); if (Illiterate.CheckIlliterateSkill(player)) return Illiterate.GetRandomText(output)!; @@ -164,6 +172,8 @@ namespace src.utils return key; } + private static bool _geoLiteBroken = false; + private static string GetLangCode(CCSPlayerController? player) { if (player == null || !player.IsValid || player.IsBot) return defaultLangCode; @@ -172,10 +182,24 @@ namespace src.utils if (!string.IsNullOrEmpty(fileLangCode)) return fileLangCode; - if (Config.LoadedConfig.LanguageSystem.DisableGeoLite == true) + if (Config.LoadedConfig.LanguageSystem.DisableGeoLite == true || _geoLiteBroken) return defaultLangCode; - string? geoliteLandCode = GetLangCodeFromDatabase(GetPlayerIP(player)) ?? defaultLangCode; + string? geoliteLandCode; + try + { + geoliteLandCode = GetLangCodeFromDatabase(GetPlayerIP(player)) ?? defaultLangCode; + } + catch (Exception ex) + { + // MaxMind.Db can fail to load (e.g. hot-reload while the old AssemblyLoadContext is + // unloading) or the .mmdb can be corrupt; fall back to the default language and stop + // trying for the rest of the session instead of throwing every HUD tick. + _geoLiteBroken = true; + Server.PrintToConsole($"[jRandomSkills] GeoLite lookup disabled for this session: {ex.GetType().Name}: {ex.Message}"); + geoliteLandCode = defaultLangCode; + } + ChangePlayerLanguage(player, geoliteLandCode); return geoliteLandCode; } @@ -189,6 +213,9 @@ namespace src.utils return parts.Length > 1 ? parts[0] : playerIP; } + // NoInlining keeps the MaxMind.Db type references out of GetLangCode, so an assembly-load + // failure surfaces inside the try/catch at the call site instead of when GetLangCode is JITed. + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] private static string? GetLangCodeFromDatabase(string? playerIP) { if (string.IsNullOrEmpty(playerIP)) return null; diff --git a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs index 24f4f46..218c927 100644 --- a/jRandomSkills - SRC Files/src/utils/SkillUtils.cs +++ b/jRandomSkills - SRC Files/src/utils/SkillUtils.cs @@ -21,10 +21,21 @@ namespace src.utils { public static class SkillUtils { - private static readonly MemoryFunctionWithReturn HEGrenadeProjectile_CreateFunc = new(GameData.GetSignature("HEGrenadeProjectile_CreateFunc")); - private static readonly MemoryFunctionWithReturn SmokeGrenadeProjectile_CreateFunc = new(GameData.GetSignature("SmokeGrenadeProjectile_CreateFunc")); - private static readonly MemoryFunctionVoid TerminateRoundFunc = new(GameData.GetSignature("CCSGameRules_TerminateRound")); - private static readonly MemoryFunctionVoid SnapViewAngles = new(GameData.GetSignature("SnapViewAngles")); + private static Lazy LazySig(string name, Func factory) where T : class => + new(() => + { + try { return factory(GameData.GetSignature(name)); } + catch (Exception ex) { Server.PrintToConsole($"[jRandomSkills] gamedata signature '{name}' could not be resolved: {ex.Message}"); return null; } + }); + + private static readonly Lazy?> HEGrenadeProjectile_CreateFunc = + LazySig>("HEGrenadeProjectile_CreateFunc", s => new(s)); + private static readonly Lazy?> SmokeGrenadeProjectile_CreateFunc = + LazySig>("SmokeGrenadeProjectile_CreateFunc", s => new(s)); + private static readonly Lazy?> TerminateRoundFunc = + LazySig>("CCSGameRules_TerminateRound", s => new(s)); + private static readonly Lazy?> SnapViewAngles = + LazySig>("SnapViewAngles", s => new(s)); // private static readonly int collisionRulesChangedOffset = GameData.GetOffset("CBaseEntity_CollisionRulesChanged"); public static void PrintToChat(CCSPlayerController player, string? msg, string border = "tb", string? title = null) @@ -74,6 +85,8 @@ namespace src.utils weapon.Value.Clip1 = ammo; Utilities.SetStateChanged(weapon.Value, "CBasePlayerWeapon", "m_iClip1"); + if (ammo == 1) return; + jRandomSkills.Instance.AddTimer(.1f, () => { if (weapon == null || !weapon.IsValid || weapon.Value == null || !weapon.Value.IsValid) return; @@ -169,7 +182,7 @@ namespace src.utils public static void Look(this CBasePlayerPawn pawn, QAngle angle) { if (pawn == null || !pawn.IsValid) return; - SnapViewAngles.Invoke(pawn, angle); + SnapViewAngles.Value?.Invoke(pawn, angle); } public static CBeam? CreateLine(Vector start, Vector end, Color color, uint ownerPlayerIndex = EntityManager.SystemOwnerIndex) @@ -264,12 +277,12 @@ namespace src.utils public static void CreateHEGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum) { - HEGrenadeProjectile_CreateFunc.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum); + HEGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 44, teamNum); } public static void CreateSmokeGrenadeProjectile(Vector pos, QAngle angle, Vector vel, int teamNum) { - SmokeGrenadeProjectile_CreateFunc.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 45, teamNum); + SmokeGrenadeProjectile_CreateFunc.Value?.Invoke(pos.Handle, angle.Handle, vel.Handle, vel.Handle, IntPtr.Zero, 45, teamNum); } public static bool TakeHealth(CCSPlayerPawn? pawn, int damage) @@ -320,7 +333,7 @@ namespace src.utils return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos); } - public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null) + public static void ForceFullUpdate(CCSPlayerController player, List<(uint PlayerIndex, QAngle LastAngle)>? batchList = null, INetworkGameServer? networkGameServer = null) { if (player == null || !player.IsValid) return; @@ -329,14 +342,15 @@ namespace src.utils QAngle lastAngle = new(pawn.V_angle.X, pawn.V_angle.Y, pawn.V_angle.Z); - INetworkServerService networkServerService = new(); - INetworkGameServer networkGameServer = networkServerService.GetIGameServer(); + networkGameServer ??= new INetworkServerService().GetIGameServer(); var client = networkGameServer.GetClientBySlot(player.Slot); if (client == null) return; client.ForceFullUpdate(); - if (lastAngle.Y == 0) return; + // 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; @@ -368,8 +382,9 @@ namespace src.utils lastForceFullUpdateAll = tickCount; var playersToRestore = new List<(uint PlayerIndex, QAngle LastAngle)>(); + INetworkGameServer networkGameServer = new INetworkServerService().GetIGameServer(); foreach (var player in Utilities.GetPlayers()) - ForceFullUpdate(player, playersToRestore); + ForceFullUpdate(player, playersToRestore, networkGameServer); if (playersToRestore.Count <= 0) return; @@ -639,7 +654,7 @@ namespace src.utils { if (jRandomSkills.Instance == null || jRandomSkills.Instance.GameRules == null) return; UpdateServerTeamScores(ctScore, tScore); - TerminateRoundFunc.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, roundEndReason, 0, 0); + TerminateRoundFunc.Value?.Invoke(jRandomSkills.Instance.GameRules.Handle, 5f, roundEndReason, 0, 0); } public static void TerminateRound(CsTeam winnerTeam) diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll index 22a8b68..c5c63ec 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/configs/config.json b/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json index d5e39fd..a343a83 100644 --- a/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json +++ b/jRandomSkills - Server Files/plugins/jRandomSkills/configs/config.json @@ -8,6 +8,7 @@ "EnableBotSkills": true, "EnableBotKickDebug": false, "DebugMode": false, + "PerfMode": false, "AlternativeSkillButton": null, "SkillTimeBeforeStart": 7.0, "SkillHudDuration": -1.0, diff --git a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll index 4d12098..27bb56a 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll differ