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/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)