Remove ForceFullUpdate, rework entity lifecycle, move skill selection to round end

This commit is contained in:
ByDexter 2026-07-12 16:52:57 +03:00
parent a5c0e3ca4b
commit 4b42670804
8 changed files with 251 additions and 216 deletions

View file

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

View file

@ -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)
{

View file

@ -144,8 +144,16 @@ namespace src.player
foreach (var p in Instance.SkillPlayer)
{
if (p.IsDrawing || !seen.Add(p.Skill)) continue;
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}");
}
}
}
private static HookResult PlayerMakeSound(UserMessage um)
@ -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>(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<uint, jSkill_SkillInfo> nextRoundPicks = [];
private static jSkill_SkillInfo PickSkillForPlayer(CCSPlayerController player, jSkill_PlayerInfo skillPlayer, List<CCSPlayerController> validPlayers, Dictionary<Skills, int> assignmentCounts, Config.GameModes gameMode)
{
List<jSkill_SkillInfo> skillList = [.. SkillData.Skills];
skillList.RemoveAll(s => s?.Skill == Skills.None);
if (!player.IsBot)
skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue<string>(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<jSkill_SkillInfo>? 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<jSkill_SkillInfo>? value))
value.Add(randomSkill);
else
playersSkills.TryAdd(player.Index, [randomSkill]);
}
return randomSkill;
}
private static bool IsPickStillValid(jSkill_SkillInfo pick, CCSPlayerController player, List<CCSPlayerController> validPlayers, Dictionary<Skills, int> 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<Skills, int> 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<jSkill_SkillInfo> skillList = [.. SkillData.Skills];
skillList.RemoveAll(s => s?.Skill == Skills.None);
if (!player.IsBot)
skillList.RemoveAll(s => !string.IsNullOrEmpty(SkillsInfo.GetValue<string>(s.Skill, "requiredPermission")) && !AdminManager.PlayerHasPermissions(player, SkillsInfo.GetValue<string>(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<jSkill_SkillInfo>? 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<jSkill_SkillInfo>? 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);

View file

@ -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;

View file

@ -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<T> where T : unmanaged
{
public T* m_pMemory;
public int m_nAllocationCount;
public int m_nGrowSize;
}
[StructLayout(LayoutKind.Sequential)]
public unsafe struct CUtlVector<T> where T : unmanaged
{
public int m_Size;
public CUtlMemory<T> 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<nint, nint> GetIGameServerFunc;
public INetworkServerService() : base(NativeAPI.GetValveInterface(0, "NetworkServerService_001"))
{
GetIGameServerFunc = new VirtualFunctionWithReturn<nint, nint>(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<nint> Slots => ref Unsafe.AsRef<CUtlVector<nint>>((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<T>(int offset) where T : unmanaged
{
return ref Unsafe.AsRef<T>((void*)(Handle + offset));
}
public int DeltaTick
{
get => Field<int>(m_nDeltaTick);
set => Field<int>(m_nDeltaTick) = value;
}
public void ForceFullUpdate() => DeltaTick = -1;
}
#endregion
}

View file

@ -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<CParticleSystem>("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<CDynamicProp>(designerName);
if (prop == null || !prop.IsValid) return null;
@ -103,6 +121,7 @@ namespace src.utils
{
try
{
if (OverBudget()) return null;
var shake = Utilities.CreateEntityByName<CEnvShake>("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<CChicken>("chicken");
if (chicken == null || !chicken.IsValid) return null;
@ -139,6 +159,7 @@ namespace src.utils
{
try
{
if (OverBudget()) return null;
var prop = Utilities.CreateEntityByName<CPhysicsPropMultiplayer>("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<CTriggerMultiple>("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<uint, DateTime> recentlyDestroyed = new();
public static List<uint> GetRecentlyDestroyedSnapshot()
{
if (recentlyDestroyed.IsEmpty) return [];
var now = DateTime.UtcNow;
var result = new List<uint>();
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<CBaseEntity>((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();
}

View file

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

View file

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