From 84d2a1b85b59102435be0f903655a36b326afe32 Mon Sep 17 00:00:00 2001 From: qstage <166386817+qstage@users.noreply.github.com> Date: Sun, 15 Sep 2024 16:19:42 +0300 Subject: [PATCH] init --- ForceFullUpdate.cs | 85 ++++++++++++++++++ HidePlayers.csproj | 23 +++++ Plugin.cs | 177 ++++++++++++++++++++++++++++++++++++++ PluginConfig.cs | 11 +++ gamedata/HidePlayers.json | 40 +++++++++ lang/en.json | 6 ++ lang/ru.json | 6 ++ 7 files changed, 348 insertions(+) create mode 100644 ForceFullUpdate.cs create mode 100644 HidePlayers.csproj create mode 100644 Plugin.cs create mode 100644 PluginConfig.cs create mode 100644 gamedata/HidePlayers.json create mode 100644 lang/en.json create mode 100644 lang/ru.json diff --git a/ForceFullUpdate.cs b/ForceFullUpdate.cs new file mode 100644 index 0000000..fa3cc70 --- /dev/null +++ b/ForceFullUpdate.cs @@ -0,0 +1,85 @@ +using System.Runtime.InteropServices; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory; + +namespace HidePlayers; + +[StructLayout(LayoutKind.Sequential)] +struct CUtlMemory +{ + public unsafe nint* m_pMemory; + public int m_nAllocationCount; + public int m_nGrowSize; +} + +[StructLayout(LayoutKind.Sequential)] +struct CUtlVector +{ + public unsafe nint this[int index] + { + get => this.m_Memory.m_pMemory[index]; + set => this.m_Memory.m_pMemory[index] = value; + } + + public int m_iSize; + public CUtlMemory m_Memory; + + public nint Element(int index) => this[index]; +} + +// thx https://discord.com/channels/1160907911501991946/1175947333880524962/1231712355784851497 + +class INetworkServerService : NativeObject +{ + private readonly VirtualFunctionWithReturn GetIGameServerFunc; + + public INetworkServerService() : base(NativeAPI.GetValveInterface(0, "NetworkServerService_001")) + { + this.GetIGameServerFunc = new VirtualFunctionWithReturn(this.Handle, GameData.GetOffset("INetworkServerService_GetIGameServer")); + } + + public INetworkGameServer GetIGameServer() + { + return new INetworkGameServer(this.GetIGameServerFunc.Invoke(this.Handle)); + } +} + +public class INetworkGameServer : NativeObject +{ + private static int SlotsOffset = GameData.GetOffset("INetworkGameServer_Slots"); + + private CUtlVector Slots; + + public INetworkGameServer(nint ptr) : base(ptr) + { + this.Slots = Marshal.PtrToStructure(base.Handle + SlotsOffset); + } + + public CServerSideClient? GetClientBySlot(int playerSlot) + { + if (playerSlot >= 0 && playerSlot < this.Slots.m_iSize) + return this.Slots[playerSlot] == IntPtr.Zero ? null : new CServerSideClient(this.Slots[playerSlot]); + + return null; + } +} + +public class CServerSideClient : NativeObject +{ + private static int m_nForceWaitForTick = GameData.GetOffset("CServerSideClient_m_nForceWaitForTick"); + + public unsafe int ForceWaitForTick + { + get { return *(int*)(base.Handle + m_nForceWaitForTick); } + set { *(int*)(base.Handle + m_nForceWaitForTick) = value; } + } + + public CServerSideClient(nint ptr) : base(ptr) + { } + + public void ForceFullUpdate() + { + this.ForceWaitForTick = -1; + } +} \ No newline at end of file diff --git a/HidePlayers.csproj b/HidePlayers.csproj new file mode 100644 index 0000000..589f2c6 --- /dev/null +++ b/HidePlayers.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..2bd6983 --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,177 @@ +using System.Runtime.InteropServices; +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Memory.DynamicFunctions; +using Microsoft.Extensions.Logging; + +namespace HidePlayers; + +public sealed class Plugin : BasePlugin, IPluginConfig +{ + public override string ModuleName => "HidePlayers"; + public override string ModuleAuthor => "xstage"; + public override string ModuleVersion => "1.0.0"; + public override string ModuleDescription => "Plugin uses code borrowed from CS2Fixes / cs2kz-metamod / hl2sdk"; + + public PluginConfig Config { get; set; } = new(); + private readonly bool[] _hide = new bool[65]; + private readonly CSPlayerState[] _oldPlayerState = new CSPlayerState[65]; + private readonly INetworkServerService networkServerService = new(); + + private static readonly MemoryFunctionVoid CheckTransmit = new(GameData.GetSignature("CheckTransmit")); + private static readonly MemoryFunctionVoid StateTransition = new(GameData.GetSignature("StateTransition")); + + #region CCheckTransmitInfo + [StructLayout(LayoutKind.Sequential)] + public struct CCheckTransmitInfo + { + public CFixedBitVecBase m_pTransmitEntity; + }; + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct CFixedBitVecBase + { + private const int LOG2_BITS_PER_INT = 5; + private const int MAX_EDICT_BITS = 14; + private const int BITS_PER_INT = 32; + private const int MAX_EDICTS = 1 << MAX_EDICT_BITS; + + private uint* m_Ints; + + public void Clear(int bitNum) + { + if (!(bitNum >= 0 && bitNum < MAX_EDICTS)) + return; + + uint* pInt = m_Ints + BitVec_Int(bitNum); + *pInt &= ~(uint)BitVec_Bit(bitNum); + } + + public bool IsBitSet(int bitNum) + { + if (!(bitNum >= 0 && bitNum < MAX_EDICTS)) + return false; + + uint* pInt = m_Ints + BitVec_Int(bitNum); + return ( *pInt & BitVec_Bit( bitNum ) ) != 0 ; + } + + private int BitVec_Int(int bitNum) => bitNum >> LOG2_BITS_PER_INT; + private int BitVec_Bit(int bitNum) => 1 << ((bitNum) & (BITS_PER_INT - 1)); + } + #endregion + + public override void Load(bool hotReload) + { + StateTransition.Hook(Hook_StateTransition, HookMode.Post); + CheckTransmit.Hook(Hook_CheckTransmit, HookMode.Post); + + RegisterEventHandler((@event, info) => + { + if (@event.Userid is {Index: uint index}) + { + _hide[index] = false; + } + + return HookResult.Continue; + }); + + AddCommand(Config.Command, "Hide players models", (player, info) => + { + player?.PrintToChat(Localizer["Player.Hide", Localizer["Plugin.Tag"], (_hide[player.Index] ^= true) ? Localizer["Plugin.Enable"] : Localizer["Plugin.Disable"]]); + }); + } + + public override void Unload(bool hotReload) + { + StateTransition.Unhook(Hook_StateTransition, HookMode.Post); + CheckTransmit.Unhook(Hook_CheckTransmit, HookMode.Post); + } + + private void ForceFullUpdate(CCSPlayerController? player) + { + if (player is null || !player.IsValid) return; + + var networkGameServer = networkServerService.GetIGameServer(); + networkGameServer.GetClientBySlot(player.Slot)?.ForceFullUpdate(); + + player.PlayerPawn.Value?.Teleport(null, player.PlayerPawn.Value.EyeAngles, null); + } + + private unsafe HookResult Hook_CheckTransmit(DynamicHook hook) + { + nint* ppInfoList = (nint*)hook.GetParam(1); + int infoCount = hook.GetParam(2); + + for (int i = 0; i < infoCount; i++) + { + nint pInfo = ppInfoList[i]; + byte slot = *(byte*)(pInfo + GameData.GetOffset("CheckTransmitPlayerSlot")); + + var player = Utilities.GetPlayerFromSlot(slot); + var info = Marshal.PtrToStructure(pInfo); + + if (player == null || player.PlayerPawn.Value == null || player.IsHLTV) + continue; + + foreach (var target in Utilities.GetPlayers() + .Where(p => p != null && p.PlayerPawn.Value != null)) + { + var pawn = target.PlayerPawn.Value!; + + #region fix client crash + if (target.Slot == slot && ((LifeState_t)pawn.LifeState != LifeState_t.LIFE_DEAD || pawn.PlayerState.HasFlag(CSPlayerState.STATE_DEATH_ANIM))) + continue; + + if (player.PlayerPawn.Value.PlayerState.HasFlag(CSPlayerState.STATE_DORMANT) && target.Slot != slot) + continue; + + if ((LifeState_t)pawn.LifeState != LifeState_t.LIFE_ALIVE) + { + info.m_pTransmitEntity.Clear((int)pawn.Index); + continue; + } + #endregion + + if (_hide[player.Index] && (Config.Hidden.Equals("@enemy") && player.Team != target.Team || Config.Hidden.Equals("@team") && player.Team == target.Team || Config.Hidden.Equals("@all"))) + { + info.m_pTransmitEntity.Clear((int)pawn.Index); + } + } + } + + return HookResult.Continue; + } + + private HookResult Hook_StateTransition(DynamicHook hook) + { + var pawn = new CCSPlayerPawn(hook.GetParam(0)); + + if (!pawn.IsValid) return HookResult.Continue; + + var player = pawn.OriginalController.Value; + var state = hook.GetParam(1); + + if (player is null) return HookResult.Continue; + + if (_oldPlayerState[player.Index] != CSPlayerState.STATE_OBSERVER_MODE && state == CSPlayerState.STATE_OBSERVER_MODE || + _oldPlayerState[player.Index] == CSPlayerState.STATE_OBSERVER_MODE && state != CSPlayerState.STATE_OBSERVER_MODE) + { + ForceFullUpdate(player); + } + + _oldPlayerState[player.Index] = state; + + return HookResult.Continue; + } + + public void OnConfigParsed(PluginConfig config) + { + if (config.Version < Config.Version) + { + Logger.LogWarning("Update plugin config. New version: {Version}", Config.Version); + } + + Config = config; + } +} diff --git a/PluginConfig.cs b/PluginConfig.cs new file mode 100644 index 0000000..758ec0e --- /dev/null +++ b/PluginConfig.cs @@ -0,0 +1,11 @@ +using CounterStrikeSharp.API.Core; + +namespace HidePlayers; + +public class PluginConfig : IBasePluginConfig +{ + public string Command { get; set; } = "css_hidemodels"; + public string Hidden { get; set; } = "@all"; + + public int Version { get; set; } = 1; +} \ No newline at end of file diff --git a/gamedata/HidePlayers.json b/gamedata/HidePlayers.json new file mode 100644 index 0000000..e55a496 --- /dev/null +++ b/gamedata/HidePlayers.json @@ -0,0 +1,40 @@ +{ + "StateTransition": { + "signatures": { + "library": "server", + "windows": "48 83 EC 28 48 89 74 24 ? 8B F2 48 89 7C 24 ? 48 8B F9", + "linux": "55 48 89 E5 41 55 41 54 41 89 F4 53 48 89 FB 48 83 EC ? 39 B7" + } + }, + "CheckTransmit": { + "signatures": { + "library": "server", + "windows": "48 8B C4 4C 89 48 ? 44 89 40 ? 48 89 50 ? 48 89 48 ? 55", + "linux": "55 48 89 E5 41 57 49 89 CF 41 56 41 55 41 54 53 48 81 EC" + } + }, + "CheckTransmitPlayerSlot": { + "offsets": { + "windows": 584, + "linux": 584 + } + }, + "INetworkServerService_GetIGameServer": { + "offsets": { + "windows": 23, + "linux": 24 + } + }, + "INetworkGameServer_Slots": { + "offsets": { + "windows": 624, + "linux": 640 + } + }, + "CServerSideClient_m_nForceWaitForTick": { + "offsets": { + "windows": 308, + "linux": 324 + } + } +} \ No newline at end of file diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..eb2fb60 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,6 @@ +{ + "Plugin.Tag": "[Hide]", + "Plugin.Enable": "{Green}turned on{Default}", + "Plugin.Disable": "{LightRed}turned off{Default}", + "Player.Hide": "{Gold}{0}{Default} You {1} hide players!" +} \ No newline at end of file diff --git a/lang/ru.json b/lang/ru.json new file mode 100644 index 0000000..a76b411 --- /dev/null +++ b/lang/ru.json @@ -0,0 +1,6 @@ +{ + "Plugin.Tag": "[Hide]", + "Plugin.Enable": "{Green}включили{Default}", + "Plugin.Disable": "{LightRed}выключили{Default}", + "Player.Hide": "{Gold}{0}{Default} Вы {1} скрытие игроков!" +} \ No newline at end of file