using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Timers; using CounterStrikeSharp.API.Modules.Utils; using SendProxyInterop; using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; namespace SendProxyWeaponVisibility; public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin { private const int TeamT = 2; private const int TeamCt = 3; private const uint EfNoDraw = 0x20; private const int SpoofedHealth = 42; private readonly Dictionary _activeRules = new(); private SendProxyNative? _sendProxy; private Timer? _syncTimer; public override string ModuleName => "SendProxy Weapon Visibility"; public override string ModuleVersion => "0.1.0"; public override string ModuleAuthor => "OpenAI Codex"; public override void Load(bool hotReload) { _sendProxy = new SendProxyNative(GetSendProxyLibraryPath()); _syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT); RegisterListener(_ => Server.NextFrame(SyncRules)); RegisterListener(_ => Server.NextFrame(SyncRules)); SyncRules(); } public override void Unload(bool hotReload) { _syncTimer?.Kill(); ClearNativeRules(); _sendProxy?.Dispose(); _sendProxy = null; } private void SyncRules() { if (_sendProxy == null) return; var desired = new Dictionary(); var viewers = Utilities.GetPlayers() .Where(player => player.IsValid && !player.IsBot && !player.IsHLTV) .ToArray(); AddHealthRules(desired, viewers); foreach (var weapon in EnumerateWeapons()) { var owner = GetOwner(weapon); bool dropped = owner == null; int weaponIndex = (int)weapon.Index; string designerName = weapon.DesignerName; foreach (var viewer in viewers) { int recipientSlot = viewer.Slot; if (owner != null && IsOtherPlayerWeapon(viewer, owner)) { AddDesiredRule( desired, recipientSlot, weaponIndex, "CBaseEntity", "m_fEffects", RuleValue.UInt32(EfNoDraw)); continue; } if (!dropped) continue; int team = (int)viewer.Team; if (team == TeamT && IsAk47(designerName)) { AddDesiredRule( desired, recipientSlot, weaponIndex, "CBaseModelEntity", "m_clrRender", RuleValue.Color(255, 0, 0, 255)); } else if (team == TeamCt && IsM4A1S(designerName)) { AddDesiredRule( desired, recipientSlot, weaponIndex, "CBaseModelEntity", "m_clrRender", RuleValue.Color(0, 96, 255, 255)); } } } foreach (var key in _activeRules.Keys.Except(desired.Keys).ToArray()) { _sendProxy.RemoveOverride(_activeRules[key]); _activeRules.Remove(key); } foreach (var (key, value) in desired) { if (_activeRules.ContainsKey(key)) continue; int ruleId = _sendProxy.SetBytes( key.RecipientSlot, key.EntityIndex, key.ClassName, key.FieldPath, value.Bytes); if (ruleId != 0) _activeRules[key] = ruleId; } } private static void AddHealthRules(Dictionary desired, IReadOnlyCollection viewers) { foreach (var viewer in viewers) { var viewerPawn = viewer.PlayerPawn.Value; int viewerPawnIndex = viewerPawn != null && viewerPawn.IsValid ? (int)viewerPawn.Index : -1; foreach (var target in viewers) { var targetPawn = target.PlayerPawn.Value; if (targetPawn == null || !targetPawn.IsValid) continue; int targetPawnIndex = (int)targetPawn.Index; if (targetPawnIndex == viewerPawnIndex) continue; AddDesiredRule( desired, viewer.Slot, targetPawnIndex, "CBaseEntity", "m_iHealth", RuleValue.Int32(SpoofedHealth)); } } } private static void AddDesiredRule( Dictionary desired, int recipientSlot, int entityIndex, string className, string fieldPath, RuleValue value) { desired[new RuleKey(recipientSlot, entityIndex, className, fieldPath)] = value; } private IEnumerable EnumerateWeapons() { foreach (var entity in Utilities.GetAllEntities()) { if (!entity.IsValid || !entity.DesignerName.StartsWith("weapon_", StringComparison.Ordinal)) continue; CBasePlayerWeapon weapon = entity.As(); if (weapon.IsValid) yield return weapon; } } private static CBaseEntity? GetOwner(CBasePlayerWeapon weapon) { var owner = weapon.OwnerEntity.Value; return owner != null && owner.IsValid ? owner : null; } private static bool IsOtherPlayerWeapon(CCSPlayerController viewer, CBaseEntity owner) { var ownPawn = viewer.PlayerPawn.Value; if (ownPawn == null || !ownPawn.IsValid) return owner.DesignerName.Contains("player", StringComparison.Ordinal); return owner.Index != ownPawn.Index && owner.DesignerName.Contains("player", StringComparison.Ordinal); } private static bool IsAk47(string designerName) { return designerName.Equals("weapon_ak47", StringComparison.Ordinal); } private static bool IsM4A1S(string designerName) { return designerName.Equals("weapon_m4a1_silencer", StringComparison.Ordinal); } private void ClearNativeRules() { if (_sendProxy == null) return; foreach (int ruleId in _activeRules.Values) _sendProxy.RemoveOverride(ruleId); _activeRules.Clear(); } private string GetSendProxyLibraryPath() { string addonsDirectory = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "..")); string libraryPath = Path.Combine(addonsDirectory, "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so"); if (!File.Exists(libraryPath)) throw new FileNotFoundException("SendProxy Metamod addon was not found. Install it under game/csgo/addons/sendproxy.", libraryPath); return libraryPath; } private readonly record struct RuleKey(int RecipientSlot, int EntityIndex, string ClassName, string FieldPath); private readonly record struct RuleValue(byte[] Bytes) { public static RuleValue Int32(int value) { byte[] bytes = new byte[4]; BitConverter.TryWriteBytes(bytes, value); return new RuleValue(bytes); } public static RuleValue UInt32(uint value) { byte[] bytes = new byte[4]; BitConverter.TryWriteBytes(bytes, value); return new RuleValue(bytes); } public static RuleValue Color(byte r, byte g, byte b, byte a) { return new RuleValue(new[] { r, g, b, a }); } } }