249 lines
8.3 KiB
C#
249 lines
8.3 KiB
C#
using CounterStrikeSharp.API;
|
|
using CounterStrikeSharp.API.Core;
|
|
using CounterStrikeSharp.API.Modules.Timers;
|
|
using SendProxyInterop;
|
|
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;
|
|
|
|
namespace SendProxyConcealedCarry;
|
|
|
|
public sealed class SendProxyConcealedCarryPlugin : BasePlugin
|
|
{
|
|
private const string WeaponListField = "m_pWeaponServices.m_hMyWeapons";
|
|
private const string ActiveWeaponField = "m_pWeaponServices.m_hActiveWeapon";
|
|
|
|
private readonly Dictionary<RuleKey, int> _activeRules = new();
|
|
private SendProxyNative? _sendProxy;
|
|
private Timer? _syncTimer;
|
|
|
|
public override string ModuleName => "SendProxy Concealed Carry";
|
|
public override string ModuleVersion => "0.1.0";
|
|
public override string ModuleAuthor => "OpenAI Codex";
|
|
|
|
public override void Load(bool hotReload)
|
|
{
|
|
_sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory);
|
|
_syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT);
|
|
RegisterListener<Listeners.OnEntityCreated>(_ => Server.NextFrame(SyncRules));
|
|
RegisterListener<Listeners.OnEntityDeleted>(_ => Server.NextFrame(SyncRules));
|
|
Server.NextFrame(SyncRules);
|
|
}
|
|
|
|
public override void Unload(bool hotReload)
|
|
{
|
|
_syncTimer?.Kill();
|
|
ClearRules();
|
|
_sendProxy?.Dispose();
|
|
_sendProxy = null;
|
|
}
|
|
|
|
private void SyncRules()
|
|
{
|
|
if (_sendProxy == null)
|
|
return;
|
|
|
|
Dictionary<RuleKey, byte[]> desired;
|
|
try
|
|
{
|
|
var viewers = Utilities.GetPlayers()
|
|
.Where(player => player.IsValid && !player.IsBot && !player.IsHLTV)
|
|
.ToArray();
|
|
|
|
var targets = Utilities.GetPlayers()
|
|
.Where(player => player.IsValid && !player.IsHLTV)
|
|
.ToArray();
|
|
|
|
var targetStatesByPawnIndex = targets
|
|
.Select(player => new
|
|
{
|
|
player.Slot,
|
|
Pawn = player.PlayerPawn.Value,
|
|
})
|
|
.Where(entry => entry.Pawn != null && entry.Pawn.IsValid)
|
|
.GroupBy(entry => (int)entry.Pawn!.Index)
|
|
.ToDictionary(
|
|
group => group.Key,
|
|
group =>
|
|
{
|
|
var first = group.First();
|
|
var activeWeapon = first.Pawn!.WeaponServices?.ActiveWeapon.Value;
|
|
int activeWeaponIndex = activeWeapon != null && activeWeapon.IsValid ? (int)activeWeapon.Index : -1;
|
|
return new TargetState(first.Slot, activeWeaponIndex);
|
|
});
|
|
|
|
desired = BuildDesiredRules(viewers, targetStatesByPawnIndex);
|
|
}
|
|
catch (NativeException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReconcileRules(desired);
|
|
}
|
|
|
|
private Dictionary<RuleKey, byte[]> BuildDesiredRules(
|
|
IReadOnlyCollection<CCSPlayerController> viewers,
|
|
IReadOnlyDictionary<int, TargetState> targetStatesByPawnIndex)
|
|
{
|
|
var desired = new Dictionary<RuleKey, byte[]>();
|
|
foreach (var (pawnIndex, target) in targetStatesByPawnIndex)
|
|
{
|
|
foreach (var viewer in viewers)
|
|
{
|
|
if (viewer.Slot == target.OwnerSlot)
|
|
continue;
|
|
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
pawnIndex,
|
|
"CBasePlayerPawn",
|
|
WeaponListField)] = Array.Empty<byte>();
|
|
}
|
|
}
|
|
|
|
foreach (var weapon in EnumerateConcealableCarriedWeapons(targetStatesByPawnIndex))
|
|
{
|
|
if (weapon.EntityIndex == weapon.ActiveWeaponIndex)
|
|
continue;
|
|
|
|
foreach (var viewer in viewers)
|
|
{
|
|
if (viewer.Slot == weapon.OwnerSlot)
|
|
continue;
|
|
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_hOwnerEntity")] = UInt32Bytes(0xffffffff);
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_CBodyComponent.m_pSceneNode.m_hParent.m_hOwner")] = UInt32Bytes(0xffffffff);
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_CBodyComponent.m_pSceneNode.m_hParent.m_name")] = UInt32Bytes(0);
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_CBodyComponent.m_pSceneNode.m_nParentAttachmentOrBone")] = Int16Bytes(-1);
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_CBodyComponent.m_pSceneNode.m_hierarchyAttachName")] = UInt32Bytes(0);
|
|
desired[new RuleKey(
|
|
ToEngineSlot(viewer),
|
|
weapon.EntityIndex,
|
|
"CBaseEntity",
|
|
"m_CBodyComponent.m_pSceneNode.m_bForceParentToBeNetworked")] = new byte[] { 0 };
|
|
}
|
|
}
|
|
|
|
return desired;
|
|
}
|
|
|
|
private void ReconcileRules(Dictionary<RuleKey, byte[]> desired)
|
|
{
|
|
if (_sendProxy == null)
|
|
return;
|
|
|
|
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 = value.Length == 0
|
|
? _sendProxy.SetVectorFirstFromField(
|
|
key.RecipientSlot,
|
|
key.EntityIndex,
|
|
key.ClassName,
|
|
key.FieldPath,
|
|
ActiveWeaponField)
|
|
: _sendProxy.SetBytes(
|
|
key.RecipientSlot,
|
|
key.EntityIndex,
|
|
key.ClassName,
|
|
key.FieldPath,
|
|
value);
|
|
|
|
if (ruleId != 0)
|
|
_activeRules[key] = ruleId;
|
|
}
|
|
}
|
|
|
|
private IEnumerable<ConcealableWeapon> EnumerateConcealableCarriedWeapons(IReadOnlyDictionary<int, TargetState> targetStatesByPawnIndex)
|
|
{
|
|
foreach (var entity in Utilities.GetAllEntities())
|
|
{
|
|
if (!entity.IsValid || !entity.DesignerName.StartsWith("weapon_", StringComparison.Ordinal))
|
|
continue;
|
|
|
|
CBasePlayerWeapon weapon = entity.As<CBasePlayerWeapon>();
|
|
if (!weapon.IsValid)
|
|
continue;
|
|
|
|
var owner = weapon.OwnerEntity.Value;
|
|
if (owner == null || !owner.IsValid)
|
|
continue;
|
|
|
|
if (!targetStatesByPawnIndex.TryGetValue((int)owner.Index, out var target))
|
|
continue;
|
|
|
|
yield return new ConcealableWeapon(
|
|
(int)weapon.Index,
|
|
target.OwnerSlot,
|
|
target.ActiveWeaponIndex);
|
|
}
|
|
}
|
|
|
|
private static byte[] UInt16Bytes(ushort value)
|
|
{
|
|
byte[] bytes = new byte[2];
|
|
BitConverter.TryWriteBytes(bytes, value);
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] Int16Bytes(short value)
|
|
{
|
|
byte[] bytes = new byte[2];
|
|
BitConverter.TryWriteBytes(bytes, value);
|
|
return bytes;
|
|
}
|
|
|
|
private static byte[] UInt32Bytes(uint value)
|
|
{
|
|
byte[] bytes = new byte[4];
|
|
BitConverter.TryWriteBytes(bytes, value);
|
|
return bytes;
|
|
}
|
|
|
|
private static int ToEngineSlot(CCSPlayerController player)
|
|
{
|
|
return Math.Max(0, player.Slot - 1);
|
|
}
|
|
|
|
private void ClearRules()
|
|
{
|
|
if (_sendProxy == null)
|
|
return;
|
|
|
|
foreach (int ruleId in _activeRules.Values)
|
|
_sendProxy.RemoveOverride(ruleId);
|
|
_activeRules.Clear();
|
|
}
|
|
|
|
private readonly record struct TargetState(int OwnerSlot, int ActiveWeaponIndex);
|
|
private readonly record struct ConcealableWeapon(int EntityIndex, int OwnerSlot, int ActiveWeaponIndex);
|
|
private readonly record struct RuleKey(int RecipientSlot, int EntityIndex, string ClassName, string FieldPath);
|
|
}
|