Add optional full force client update path

- Updated markdown files.
- Added FullForceUpdate as an optional feature.
- Jester: Fixes crashes that occur when a skill is being disabled.
This commit is contained in:
Juzlus 2026-07-13 00:28:30 +02:00
parent 559e7be23f
commit e490b7f3df
20 changed files with 353 additions and 28 deletions

View file

@ -26,5 +26,23 @@
"windows": "48 89 5C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 55 48 8D 6C 24 ? 48 81 EC ? ? ? ? 48 8B DA 48 8B F1 48 8D 55",
"linux": "55 48 89 E5 41 57 49 89 FF 48 89 F7 41 56 41 55 41 54 53 48 81 EC ? ? ? ? E8"
}
},
"CServerSideClient_m_nDeltaTick": {
"offsets": {
"windows": 348,
"linux": 348
}
},
"INetworkGameServer_Slots": {
"offsets": {
"windows": 584,
"linux": 584
}
},
"INetworkServerService_GetIGameServer": {
"offsets": {
"windows": 23,
"linux": 24
}
}
}

View file

@ -28,7 +28,7 @@ namespace src
public static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new();
public override string ModuleName => "[CS2] [ jRandomSkills ]";
public override string ModuleAuthor => "D3X, Juzlus";
public override string ModuleAuthor => "D3X (Original), Juzlus (Modifier), ByDexterTR (Contributor)";
public override string ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus.";
public override string ModuleVersion => "1.2.2.b7";

View file

@ -1093,24 +1093,31 @@ namespace src.player
if (randomSkill.Skill == Skills.Illiterate)
Illiterate.Enable();
var playerIndex = player.Index;
Instance?.AddTimer(.2f, () =>
{
if (player == null || !player.IsValid) return;
var playerTarget = Utilities.GetPlayerFromIndex((int)playerIndex);
if (playerTarget == null || !playerTarget.IsValid) return;
if (randomSkill.Display)
SkillUtils.PrintToChat(player, $"{ChatColors.DarkRed}{player.GetSkillName(randomSkill.Skill)}{ChatColors.Lime}: {player.GetSkillDescription(randomSkill.Skill)}",
border: !Utilities.GetPlayers().Any(p => p != null && p.IsValid && p.Team == player.Team && p != player) ? "tb" : "t");
SkillUtils.PrintToChat(playerTarget, $"{ChatColors.DarkRed}{playerTarget.GetSkillName(randomSkill.Skill)}{ChatColors.Lime}: {playerTarget.GetSkillDescription(randomSkill.Skill)}",
border: !Utilities.GetPlayers().Any(p => p != null && p.IsValid && p.Team == playerTarget.Team && p != playerTarget) ? "tb" : "t");
if (SkillsInfo.GetValue<bool>(randomSkill.Skill, "disableOnFreezeTime") && SkillUtils.IsFreezeTime())
Instance?.AddTimer(Config.LoadedConfig.SkillTimeBeforeStart, () =>
{
if (PlayerManager.GetPlayerByIndex(player!.Index)?.Skill != randomSkill.Skill) return;
Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [player]);
var playerTarget = Utilities.GetPlayerFromIndex((int)playerIndex);
if (playerTarget == null || !playerTarget.IsValid) return;
if (PlayerManager.GetPlayerByIndex(playerTarget!.Index)?.Skill != randomSkill.Skill) return;
Debug.WriteToDebug("Enabling skill after freeze time: " + randomSkill.Skill);
Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [playerTarget]);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
else
{
if (PlayerManager.GetPlayerByIndex(player!.Index)?.Skill != randomSkill.Skill) return;
Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [player]);
if (PlayerManager.GetPlayerByIndex(playerTarget!.Index)?.Skill != randomSkill.Skill) return;
Debug.WriteToDebug("Enabling skill: " + randomSkill.Skill);
Instance?.SkillAction(randomSkill.Skill.ToString(), "EnableSkill", [playerTarget]);
}
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);

View file

@ -159,6 +159,8 @@ namespace src.player.skills
invisiblePlayers.TryAdd(player.Index, 0);
SkillUtils.SetPlayerInvisibility(player, .5f);
SkillUtils.ForceFullUpdateToAll();
}
public static void DisableSkill(CCSPlayerController player)

View file

@ -106,6 +106,8 @@ namespace src.player.skills
if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0)
CreatePlayerPosProp(player);
SkillUtils.ForceFullUpdateToAll();
}
private static void CreatePlayerPosProp(CCSPlayerController player)

View file

@ -111,6 +111,8 @@ namespace src.player.skills
SkillUtils.TryGiveWeapon(player, CsItem.SmokeGrenade);
SkillUtils.UpdateGrenadeCount(player, CsItem.SmokeGrenade, grenadeLimit);
SkillUtils.ForceFullUpdateToAll();
}
public static void DisableSkill(CCSPlayerController player)

View file

@ -145,6 +145,8 @@ namespace src.player.skills
mainSkillTimer ??= Instance.AddTimer(2.5f, () => UpdateAllTrails(),
CounterStrikeSharp.API.Modules.Timers.TimerFlags.REPEAT | CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
SkillUtils.ForceFullUpdateToAll();
}
private static void UpdateAllTrails()

View file

@ -53,18 +53,17 @@ namespace src.player.skills
var jester = GetJesterInfo(player.Index);
if (jester == null) return;
jester.Generation++;
jester.Active = false;
if (jester.Timer != null)
{
jester.Timer.Kill();
jester.Timer = null;
}
jester.Timer = null;
jesters.TryRemove(player.Index, out _);
Server.NextWorldUpdate(() =>
{
SkillUtils.ResetPrintHTML(player);
SetPlayerColor(player, true);
jesters.TryRemove(player.Index, out _);
});
}
@ -154,23 +153,37 @@ namespace src.player.skills
uint playerIndex = player.Index;
jesters.TryAdd(playerIndex, new JesterInfo {
PlayerIndex = playerIndex,
Active = false,
Timer = Instance.AddTimer(wait, () => ChangeMode(playerIndex), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE)
});
if (!jesters.TryGetValue(playerIndex, out var jester))
{
jester = new JesterInfo
{
PlayerIndex = playerIndex,
Active = false,
Generation = 0
};
jesters[playerIndex] = jester;
}
jester.Generation++;
int generation = jester.Generation;
jester.Timer = Instance.AddTimer(wait, () =>
{
if (!jesters.TryGetValue(playerIndex, out var info))
return;
if (info.Generation != generation)
return;
ChangeMode(playerIndex);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
private static void ChangeMode(uint playerIndex, bool? forceActive = null)
{
if (!jesters.TryGetValue(playerIndex, out var jester)) return;
if (jester.Timer != null)
{
jester.Timer?.Kill();
jester.Timer = null;
}
var player = Utilities.GetPlayerFromIndex((int)playerIndex);
if (player == null || !player.IsValid || player.LifeState != (byte)LifeState_t.LIFE_ALIVE)
{
@ -194,7 +207,19 @@ namespace src.player.skills
var maxTime = SkillsInfo.GetValue<float>(skillName, "maxTime");
float wait = (float)Instance.Random.NextDouble() * (maxTime - minTime) + minTime;
jester.Timer = Instance.AddTimer(wait, () => ChangeMode(playerIndex), CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
jester.Generation++;
int generation = jester.Generation;
jester.Timer = Instance.AddTimer(wait, () =>
{
if (!jesters.TryGetValue(playerIndex, out var info))
return;
if (info.Generation != generation)
return;
ChangeMode(playerIndex);
}, CounterStrikeSharp.API.Modules.Timers.TimerFlags.STOP_ON_MAPCHANGE);
}
private static void SetPlayerColor(CCSPlayerController? player, bool forceDisable = false)
@ -205,9 +230,9 @@ namespace src.player.skills
if (pawn == null || !pawn.IsValid) return;
var jester = GetJesterInfo(player.Index);
if (jester == null) return;
if (!forceDisable && jester == null) return;
var color = jester.Active && !forceDisable ? Color.FromArgb(255, 128, 0, 128) : Color.FromArgb(255, 255, 255, 255);
var color = jester?.Active == true && !forceDisable ? Color.FromArgb(255, 128, 0, 128) : Color.FromArgb(255, 255, 255, 255);
pawn.Render = color;
Utilities.SetStateChanged(pawn, "CBaseModelEntity", "m_clrRender");
}
@ -249,6 +274,7 @@ namespace src.player.skills
{
public required uint PlayerIndex { get; set; }
public bool Active { get; set; } = false;
public int Generation { get; set; } = 0;
public Timer? Timer { get; set; } = null;
}

View file

@ -132,6 +132,8 @@ namespace src.player.skills
if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0)
CreatePlayerPosProp(player);
SkillUtils.ForceFullUpdateToAll();
}
public static void DisableSkill(CCSPlayerController player)

View file

@ -127,6 +127,7 @@ namespace src.player.skills
public static void EnableSkill(CCSPlayerController _)
{
Event.EnableTransmit();
SkillUtils.ForceFullUpdateToAll();
}
public static void DisableSkill(CCSPlayerController player)

View file

@ -115,6 +115,8 @@ namespace src.player.skills
if (glows.IsEmpty)
SetGlowEffectForAll();
SkillUtils.ForceFullUpdateToAll();
}
public static void DisableSkill(CCSPlayerController player)

View file

@ -0,0 +1,107 @@
// 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();
if (m_Memory.m_pMemory == null)
throw new NullReferenceException("m_pMemory is null. The vector is empty or the GameData offset is outdated.");
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 (Handle == nint.Zero)
return null;
if (slot < 0 || slot >= Slots.Count || Slots.m_Memory.m_pMemory == null)
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

@ -79,6 +79,7 @@ namespace src.utils
public bool SummaryAfterTheRound { get; set; }
public bool EnableBotSkills { get; set; }
public bool EnableBotKickDebug { get; set; }
public bool EnableFullForceUpdate { get; set; }
public bool DebugMode { get; set; }
public bool PerfMode { get; set; }
public string? AlternativeSkillButton { get; set; }
@ -107,6 +108,7 @@ namespace src.utils
SummaryAfterTheRound = true;
EnableBotSkills = true;
EnableBotKickDebug = false;
EnableFullForceUpdate = false;
DebugMode = false;
PerfMode = false;
AlternativeSkillButton = null;

View file

@ -333,6 +333,79 @@ 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 (!Config.LoadedConfig.EnableFullForceUpdate) return;
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()
{
if (!Config.LoadedConfig.EnableFullForceUpdate) return;
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)