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

30
.github/en.md vendored
View file

@ -358,6 +358,36 @@ This plugin uses content from the following projects:
## 📋 Changelog ## 📋 Changelog
<details>
<summary><b>v1.2.2.b7</b></summary>
- #### General:
- ###### Updated the gamedata signatures for the latest CS2 update (build 24134959) and bumped CounterStrikeSharp to 1.0.371. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Fixed a NullReferenceException thrown on weapon pickup when the weapon data pointed at freed schema memory. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### FullFoceUpdate has been changed to an optional setting in the configuration. Enabling it reduces the risk of PVS errors, but places a significant load on the server (disabled by default). [by: [@Juzlus](https://github.com/Juzlus)]
- #### Skill improvements:
- ###### Ghost:
- ###### Can now plant the C4. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Free Planter / Short Bomb:
- ###### The on-screen bomb countdown now matches the configured detonation time (`mp_c4timer` is set at round start instead of only the blow time being overridden after the plant). [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Rich Boy:
- ###### The money bonus now respects `mp_maxmoney` instead of being capped at a hardcoded 16000. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Second Chance:
- ###### Now reliably respawns at base on a lethal hit, including fall damage (the revive moved to a pre-damage hook so the death is cancelled before it is committed). [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Shadow:
- ###### Fixed a false "no suitable area" when teleporting behind a target at close range. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Radar Hack:
- ###### Now reveals enemies only to the skill holder instead of the holder's whole team, and no longer reveals invisible or camouflaged enemies. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### C4 Camouflage:
- ###### The disguised bomb carrier no longer appears on the enemy radar. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Hot Bomb:
- ###### The "hot bomb" hint no longer shows in a round where nobody has the skill. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Jester:
- ###### Fixes crashes that occur when a skill is being disabled. [by: [@Juzlus](https://github.com/Juzlus)]
</details>
<details> <details>
<summary><b>v1.2.2.b6</b></summary> <summary><b>v1.2.2.b6</b></summary>

30
.github/pl.md vendored
View file

@ -356,6 +356,36 @@ Plugin korzysta z zawartości następujących projektów:
## 📋 Lista Zmian ## 📋 Lista Zmian
<details>
<summary><b>v1.2.2.b7</b></summary>
- #### Ogólne:
- ###### Zaktualizowano sygnatury gamedata do najnowszej aktualizacji CS2 (build 24134959) oraz zaktualizowano CounterStrikeSharp do wersji 1.0.371. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### FNaprawiono `NullReferenceException` występujący podczas podnoszenia broni, gdy dane broni wskazywały na zwolnioną pamięć schematu. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### `FullForceUpdate` został zmieniony na opcjonalne ustawienie w konfiguracji. Jego włączenie zmniejsza ryzyko błędów PVS, ale powoduje znaczne obciążenie serwera (domyślnie wyłączone). [by: [@Juzlus](https://github.com/Juzlus)]
- #### Poprawki mocy:
- ###### Duszek:
- ###### Teraz może podkładać C4. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Samowolka / Krótka Bomba:
- ###### Odliczanie bomby wyświetlane na ekranie jest teraz zgodne z ustawionym czasem detonacji (`mp_c4timer`). [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Bogacz:
- ###### Bonus za pieniądze teraz respektuje `mp_maxmoney` zamiast być ograniczony na sztywno do wartości 16000. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Druga Szansa:
- ###### Teraz niezawodnie odradza gracza w bazie po otrzymaniu śmiertelnego obrażenia, również od obrażeń od upadku. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Cień:
- ###### Naprawiono fałszywy komunikat "brak miejsca" podczas teleportacji za cel z bliskiej odległości. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Radarowiec:
- ###### Teraz ujawnia przeciwników tylko posiadaczowi mocy zamiast całej jego drużynie oraz nie ujawnia już niewidzialnych ani zamaskowanych przeciwników. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### C4 Kamuflaż:
- ###### Niewidzialny gracz niosący bombe nie pojawia się już na radarze przeciwników. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Gorąca bomba:
- ###### Podpowiedź dotycząca "gorącej bomby" nie pojawia się już w rundach, w których nikt nie posiada tej mocy. [by: [@ByDexterTR](https://github.com/ByDexterTR)]
- ###### Błazen:
- ###### Naprawiono crashe występujące podczas wyłączania mocy. [by: [@Juzlus](https://github.com/Juzlus)]
</details>
<details> <details>
<summary><b>v1.2.2.b6</b></summary> <summary><b>v1.2.2.b6</b></summary>

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", "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" "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 static readonly ConcurrentDictionary<string, byte> ActiveSkillsThisRound = new();
public override string ModuleName => "[CS2] [ jRandomSkills ]"; 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 ModuleDescription => "Plugin adds random skills every round for CS2 by D3X. Modified by Juzlus.";
public override string ModuleVersion => "1.2.2.b7"; public override string ModuleVersion => "1.2.2.b7";

View file

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

View file

@ -159,6 +159,8 @@ namespace src.player.skills
invisiblePlayers.TryAdd(player.Index, 0); invisiblePlayers.TryAdd(player.Index, 0);
SkillUtils.SetPlayerInvisibility(player, .5f); SkillUtils.SetPlayerInvisibility(player, .5f);
SkillUtils.ForceFullUpdateToAll();
} }
public static void DisableSkill(CCSPlayerController player) 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) if (EntityManager.GetPlayerEntities(player.Index, "empty_prop").Count == 0)
CreatePlayerPosProp(player); CreatePlayerPosProp(player);
SkillUtils.ForceFullUpdateToAll();
} }
private static void CreatePlayerPosProp(CCSPlayerController player) private static void CreatePlayerPosProp(CCSPlayerController player)

View file

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

View file

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

View file

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

View file

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

View file

@ -115,6 +115,8 @@ namespace src.player.skills
if (glows.IsEmpty) if (glows.IsEmpty)
SetGlowEffectForAll(); SetGlowEffectForAll();
SkillUtils.ForceFullUpdateToAll();
} }
public static void DisableSkill(CCSPlayerController player) 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 SummaryAfterTheRound { get; set; }
public bool EnableBotSkills { get; set; } public bool EnableBotSkills { get; set; }
public bool EnableBotKickDebug { get; set; } public bool EnableBotKickDebug { get; set; }
public bool EnableFullForceUpdate { get; set; }
public bool DebugMode { get; set; } public bool DebugMode { get; set; }
public bool PerfMode { get; set; } public bool PerfMode { get; set; }
public string? AlternativeSkillButton { get; set; } public string? AlternativeSkillButton { get; set; }
@ -107,6 +108,7 @@ namespace src.utils
SummaryAfterTheRound = true; SummaryAfterTheRound = true;
EnableBotSkills = true; EnableBotSkills = true;
EnableBotKickDebug = false; EnableBotKickDebug = false;
EnableFullForceUpdate = false;
DebugMode = false; DebugMode = false;
PerfMode = false; PerfMode = false;
AlternativeSkillButton = null; AlternativeSkillButton = null;

View file

@ -333,6 +333,79 @@ namespace src.utils
return EntityManager.CreateTrackedTrigger(ownerPlayerIndex, name, radius, pos); 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) public static bool SetHealth(CCSPlayerPawn? pawn, int newHealth, int? maxHealth = null)
{ {
if (pawn == null || !pawn.IsValid) if (pawn == null || !pawn.IsValid)

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", "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" "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

@ -7,6 +7,7 @@
"SummaryAfterTheRound": true, "SummaryAfterTheRound": true,
"EnableBotSkills": true, "EnableBotSkills": true,
"EnableBotKickDebug": false, "EnableBotKickDebug": false,
"EnableFullForceUpdate": false,
"DebugMode": false, "DebugMode": false,
"PerfMode": false, "PerfMode": false,
"AlternativeSkillButton": null, "AlternativeSkillButton": null,