diff --git a/jRandomSkills - SRC Files/src/command/Command.cs b/jRandomSkills - SRC Files/src/command/Command.cs index cb8d4d2..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; @@ -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; @@ -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 ca2776a..ae555f3 100644 --- a/jRandomSkills - SRC Files/src/jRandomSkills.cs +++ b/jRandomSkills - SRC Files/src/jRandomSkills.cs @@ -114,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) diff --git a/jRandomSkills - SRC Files/src/player/PerfLog.cs b/jRandomSkills - SRC Files/src/player/PerfLog.cs new file mode 100644 index 0000000..05bdb25 --- /dev/null +++ b/jRandomSkills - SRC Files/src/player/PerfLog.cs @@ -0,0 +1,85 @@ +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; + + public static long Start() => Enabled ? Stopwatch.GetTimestamp() : 0; + + // 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 a637b00..1247aa2 100644 --- a/jRandomSkills - SRC Files/src/player/PlayerEvents.cs +++ b/jRandomSkills - SRC Files/src/player/PlayerEvents.cs @@ -440,6 +440,7 @@ namespace src.player private static void OnTick() { + long perfStart = PerfLog.Start(); lock (setLock) { _activeSkillsSet.Clear(); @@ -462,6 +463,7 @@ namespace src.player Instance.SkillAction(_skillNames[skill], "OnTick"); } } + PerfLog.Sample("OnTick(skills)", perfStart); } private static void OnPlayerConnectedBot(int playerSlot) @@ -618,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) { @@ -742,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) { @@ -847,6 +864,13 @@ 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) @@ -1149,10 +1173,12 @@ namespace src.player 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 025d258..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); 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 a83afcf..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")); 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/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/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 52bea36..bb34b05 100644 --- a/jRandomSkills - SRC Files/src/utils/Localization.cs +++ b/jRandomSkills - SRC Files/src/utils/Localization.cs @@ -172,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; @@ -180,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; } @@ -197,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 - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll b/jRandomSkills - Server Files/plugins/jRandomSkills/WASDMenuAPI.dll index 0440e5e..a87b3d0 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 6e6e5d9..dd044c5 100644 Binary files a/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll and b/jRandomSkills - Server Files/plugins/jRandomSkills/jRandomSkills.dll differ