Compare commits
3 Commits
4e9500f0ea
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
c8c2334bcc
|
|||
|
1e348c5abb
|
|||
|
9a23169b6b
|
37
README.md
37
README.md
@@ -57,11 +57,10 @@ The C# wrapper is in:
|
||||
examples/CounterStrikeSharp/SendProxyNative.cs
|
||||
```
|
||||
|
||||
It loads the native addon with `NativeLibrary.Load` and exposes byte-oriented and typed helpers. In a CSS plugin, resolve the native library from `Server.GameDirectory`:
|
||||
It loads the native addon with `NativeLibrary.Load` and exposes byte-oriented and typed helpers. In a CSS plugin, construct it from `Server.GameDirectory`:
|
||||
|
||||
```csharp
|
||||
string libraryPath = Path.Combine(Server.GameDirectory, "addons", "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so");
|
||||
var sendProxy = new SendProxyNative(libraryPath);
|
||||
var sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory);
|
||||
|
||||
int ruleId = sendProxy.SetInt32(
|
||||
recipientSlot: viewer.Slot,
|
||||
@@ -75,9 +74,17 @@ sendProxy.RemoveOverride(ruleId);
|
||||
|
||||
Use `SetBytes` for custom/fixed-layout fields and typed helpers such as `SetInt32`, `SetUInt32`, `SetFloat`, `SetBool`, and `SetColor` for common values.
|
||||
|
||||
## Example CSS Plugin
|
||||
## Example CSS Plugins
|
||||
|
||||
Example source:
|
||||
Health spoof smoke test:
|
||||
|
||||
```text
|
||||
examples/CounterStrikeSharp/SendProxyHealth23/
|
||||
```
|
||||
|
||||
It spoofs every player pawn's `CBaseEntity::m_iHealth` to `23` for all recipients without changing real health.
|
||||
|
||||
Weapon visibility experiment:
|
||||
|
||||
```text
|
||||
examples/CounterStrikeSharp/SendProxyWeaponVisibility/
|
||||
@@ -90,6 +97,24 @@ It demonstrates:
|
||||
- Coloring dropped AK-47s red for Terrorist viewers.
|
||||
- Coloring dropped M4A1-S blue for CT viewers.
|
||||
|
||||
Concealed-carry weapon-list spoof:
|
||||
|
||||
```text
|
||||
examples/CounterStrikeSharp/SendProxyConcealedCarry/
|
||||
```
|
||||
|
||||
It hides a player's non-active carried weapons from every recipient except the owning player by spoofing the player/weapon association:
|
||||
|
||||
- `CBasePlayerPawn::m_pWeaponServices.m_hMyWeapons`
|
||||
- source handle: `CBasePlayerPawn::m_pWeaponServices.m_hActiveWeapon`
|
||||
- `CBaseEntity::m_hOwnerEntity = INVALID_EHANDLE` on non-active carried weapon entities
|
||||
- `CBaseEntity::m_CBodyComponent.m_pSceneNode.m_hParent = INVALID_EHANDLE`
|
||||
- `CBaseEntity::m_CBodyComponent.m_pSceneNode.m_hierarchyAttachName = 0`
|
||||
- `CCSWeaponBase::m_hPrevOwner = INVALID_EHANDLE`
|
||||
- `CBaseCSGrenade::m_bIsHeldByPlayer = false` for non-active grenades
|
||||
|
||||
This corresponds to CounterStrikeSharp's `player.PlayerPawn.Value.WeaponServices?.MyWeapons`, plus the weapon entity relationship fields that attach carried weapons to the owning pawn. During packing for non-owner recipients, native code temporarily masks non-active `m_hMyWeapons` elements to `INVALID_EHANDLE`; the CSS sample also spoofs non-active carried weapon entities as detached/unowned. It intentionally does not set `EF_NODRAW`, use render mode tricks, or remove/drop the real weapons.
|
||||
|
||||
This sample periodically reconciles desired rules and diffs them against active rule IDs. For production, prefer event-driven updates on player connect/disconnect/team changes/spawn/death and weapon pickup/drop/create/delete.
|
||||
|
||||
## Building the Metamod Addon
|
||||
@@ -159,6 +184,8 @@ Install the built CSS plugin under:
|
||||
game/csgo/addons/counterstrikesharp/plugins/SendProxyWeaponVisibility/
|
||||
```
|
||||
|
||||
Use the matching example directory name when building/installing another sample, for example `SendProxyHealth23` or `SendProxyConcealedCarry`.
|
||||
|
||||
Make sure `SendProxyNative.cs` is included in your CSS project or copied into your plugin source.
|
||||
|
||||
The example plugin expects the Metamod addon at:
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
|
||||
<Compile Include="..\SendProxyNative.cs" Link="SendProxyNative.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,228 @@
|
||||
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 const string EntityClass = "CBaseEntity";
|
||||
private static readonly byte[] InvalidHandle = UInt32Bytes(0xffffffff);
|
||||
private static readonly byte[] EmptyToken = UInt32Bytes(0);
|
||||
private static readonly byte[] NoAttachment = Int16Bytes(-1);
|
||||
private static readonly byte[] FalseByte = [0];
|
||||
|
||||
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;
|
||||
|
||||
int recipient = ToEngineSlot(viewer);
|
||||
AddAssociationDisconnectRules(desired, recipient, weapon.EntityIndex);
|
||||
}
|
||||
}
|
||||
|
||||
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[] 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 void AddAssociationDisconnectRules(Dictionary<RuleKey, byte[]> desired, int recipientSlot, int weaponEntityIndex)
|
||||
{
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_hOwnerEntity")] = InvalidHandle;
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_CBodyComponent.m_pSceneNode.m_hParent.m_hOwner")] = InvalidHandle;
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_CBodyComponent.m_pSceneNode.m_hParent.m_name")] = EmptyToken;
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_CBodyComponent.m_pSceneNode.m_nParentAttachmentOrBone")] = NoAttachment;
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_CBodyComponent.m_pSceneNode.m_hierarchyAttachName")] = EmptyToken;
|
||||
desired[new RuleKey(recipientSlot, weaponEntityIndex, EntityClass, "m_CBodyComponent.m_pSceneNode.m_bForceParentToBeNetworked")] = FalseByte;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
|
||||
<Compile Include="..\SendProxyNative.cs" Link="SendProxyNative.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,91 @@
|
||||
using CounterStrikeSharp.API;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
using CounterStrikeSharp.API.Modules.Timers;
|
||||
using SendProxyInterop;
|
||||
using Timer = CounterStrikeSharp.API.Modules.Timers.Timer;
|
||||
|
||||
namespace SendProxyHealth23;
|
||||
|
||||
public sealed class SendProxyHealth23Plugin : BasePlugin
|
||||
{
|
||||
private const int RecipientAll = -1;
|
||||
private const int SpoofedHealth = 23;
|
||||
|
||||
private readonly Dictionary<int, int> _rulesByPawnIndex = new();
|
||||
private SendProxyNative? _sendProxy;
|
||||
private Timer? _syncTimer;
|
||||
|
||||
public override string ModuleName => "SendProxy Health 23";
|
||||
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);
|
||||
Server.NextFrame(SyncRules);
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
{
|
||||
_syncTimer?.Kill();
|
||||
ClearRules();
|
||||
_sendProxy?.Dispose();
|
||||
_sendProxy = null;
|
||||
}
|
||||
|
||||
private void SyncRules()
|
||||
{
|
||||
if (_sendProxy == null)
|
||||
return;
|
||||
|
||||
int[] desiredPawnIndexes;
|
||||
try
|
||||
{
|
||||
desiredPawnIndexes = Utilities.GetPlayers()
|
||||
.Select(player => player.PlayerPawn.Value)
|
||||
.Where(pawn => pawn != null && pawn.IsValid)
|
||||
.Select(pawn => (int)pawn!.Index)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
catch (NativeException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var desired = desiredPawnIndexes.ToHashSet();
|
||||
|
||||
foreach (int pawnIndex in _rulesByPawnIndex.Keys.Except(desired).ToArray())
|
||||
{
|
||||
_sendProxy.RemoveOverride(_rulesByPawnIndex[pawnIndex]);
|
||||
_rulesByPawnIndex.Remove(pawnIndex);
|
||||
}
|
||||
|
||||
foreach (int pawnIndex in desiredPawnIndexes)
|
||||
{
|
||||
if (_rulesByPawnIndex.ContainsKey(pawnIndex))
|
||||
continue;
|
||||
|
||||
int ruleId = _sendProxy.SetInt32(
|
||||
RecipientAll,
|
||||
pawnIndex,
|
||||
"CBaseEntity",
|
||||
"m_iHealth",
|
||||
SpoofedHealth);
|
||||
|
||||
if (ruleId != 0)
|
||||
_rulesByPawnIndex[pawnIndex] = ruleId;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearRules()
|
||||
{
|
||||
if (_sendProxy == null)
|
||||
return;
|
||||
|
||||
foreach (int ruleId in _rulesByPawnIndex.Values)
|
||||
_sendProxy.RemoveOverride(ruleId);
|
||||
_rulesByPawnIndex.Clear();
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,11 @@ namespace SendProxyInterop;
|
||||
|
||||
public sealed class SendProxyNative : IDisposable
|
||||
{
|
||||
private const string RelativeLibraryPath = "addons/sendproxy/bin/linuxsteamrt64/sendproxy.so";
|
||||
|
||||
private readonly IntPtr _library;
|
||||
private readonly SetOverrideDelegate _setOverride;
|
||||
private readonly SetVectorFirstFromFieldDelegate _setVectorFirstFromField;
|
||||
private readonly RemoveOverrideDelegate _removeOverride;
|
||||
private readonly ClearOverridesDelegate _clearOverrides;
|
||||
private readonly MarkDirtyDelegate _markDirty;
|
||||
@@ -16,11 +19,25 @@ public sealed class SendProxyNative : IDisposable
|
||||
{
|
||||
_library = NativeLibrary.Load(libraryPath);
|
||||
_setOverride = GetExport<SetOverrideDelegate>("SendProxy_SetOverride");
|
||||
_setVectorFirstFromField = GetExport<SetVectorFirstFromFieldDelegate>("SendProxy_SetVectorFirstFromField");
|
||||
_removeOverride = GetExport<RemoveOverrideDelegate>("SendProxy_RemoveOverride");
|
||||
_clearOverrides = GetExport<ClearOverridesDelegate>("SendProxy_ClearOverrides");
|
||||
_markDirty = GetExport<MarkDirtyDelegate>("SendProxy_MarkDirty");
|
||||
}
|
||||
|
||||
public static SendProxyNative FromGameDirectory(string gameDirectory)
|
||||
{
|
||||
string csgoDirectory = Path.GetFileName(gameDirectory).Equals("csgo", StringComparison.OrdinalIgnoreCase)
|
||||
? gameDirectory
|
||||
: Path.Combine(gameDirectory, "csgo");
|
||||
|
||||
string libraryPath = Path.Combine(csgoDirectory, RelativeLibraryPath);
|
||||
if (!File.Exists(libraryPath))
|
||||
throw new FileNotFoundException("SendProxy Metamod addon was not found. Install it under game/csgo/addons/sendproxy.", libraryPath);
|
||||
|
||||
return new SendProxyNative(libraryPath);
|
||||
}
|
||||
|
||||
public int SetBytes(int recipientSlot, int entityIndex, string className, string fieldPath, ReadOnlySpan<byte> value)
|
||||
{
|
||||
byte[] bytes = value.ToArray();
|
||||
@@ -61,6 +78,11 @@ public sealed class SendProxyNative : IDisposable
|
||||
return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes);
|
||||
}
|
||||
|
||||
public int SetVectorFirstFromField(int recipientSlot, int entityIndex, string className, string vectorFieldPath, string sourceFieldPath)
|
||||
{
|
||||
return _setVectorFirstFromField(recipientSlot, entityIndex, className, vectorFieldPath, sourceFieldPath);
|
||||
}
|
||||
|
||||
public bool RemoveOverride(int ruleId)
|
||||
{
|
||||
return _removeOverride(ruleId);
|
||||
@@ -89,6 +111,9 @@ public sealed class SendProxyNative : IDisposable
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
private delegate int SetOverrideDelegate(int recipientSlot, int entityIndex, string className, string fieldPath, byte[] value, int valueSize);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
private delegate int SetVectorFirstFromFieldDelegate(int recipientSlot, int entityIndex, string className, string vectorFieldPath, string sourceFieldPath);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private delegate bool RemoveOverrideDelegate(int ruleId);
|
||||
@@ -99,4 +124,5 @@ public sealed class SendProxyNative : IDisposable
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private delegate bool MarkDirtyDelegate(int entityIndex, string className, string fieldPath);
|
||||
|
||||
}
|
||||
|
||||
@@ -24,11 +24,11 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin
|
||||
|
||||
public override void Load(bool hotReload)
|
||||
{
|
||||
_sendProxy = new SendProxyNative(GetSendProxyLibraryPath());
|
||||
_sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory);
|
||||
_syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT);
|
||||
RegisterListener<Listeners.OnEntityCreated>(_ => Server.NextFrame(SyncRules));
|
||||
RegisterListener<Listeners.OnEntityDeleted>(_ => Server.NextFrame(SyncRules));
|
||||
SyncRules();
|
||||
Server.NextFrame(SyncRules);
|
||||
}
|
||||
|
||||
public override void Unload(bool hotReload)
|
||||
@@ -45,9 +45,17 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin
|
||||
return;
|
||||
|
||||
var desired = new Dictionary<RuleKey, RuleValue>();
|
||||
var viewers = Utilities.GetPlayers()
|
||||
CCSPlayerController[] viewers;
|
||||
try
|
||||
{
|
||||
viewers = Utilities.GetPlayers()
|
||||
.Where(player => player.IsValid && !player.IsBot && !player.IsHLTV)
|
||||
.ToArray();
|
||||
}
|
||||
catch (NativeException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddHealthRules(desired, viewers);
|
||||
|
||||
@@ -211,14 +219,6 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin
|
||||
_activeRules.Clear();
|
||||
}
|
||||
|
||||
private string GetSendProxyLibraryPath()
|
||||
{
|
||||
string libraryPath = Path.Combine(Server.GameDirectory, "addons", "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)
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "gameconfig.h"
|
||||
#include "networksystem/inetworkserializer.h"
|
||||
#include "overrides.h"
|
||||
#include "playerslot.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern CGameConfig* g_gameConfig;
|
||||
extern bool g_enabled;
|
||||
extern bool g_armed;
|
||||
|
||||
using SendSnapshotFn = void (*)(void*, void*);
|
||||
using PackEntitiesFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*);
|
||||
using PackEntitiesFn = void (*)(void*, void*, int, void*, void*, void*, void*);
|
||||
using PackEntityFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*, void*);
|
||||
static SendSnapshotFn g_sendSnapshot = nullptr;
|
||||
static PackEntitiesFn g_packEntities = nullptr;
|
||||
@@ -28,6 +24,7 @@ static uint64_t g_packEntityGenericSpoofs = 0;
|
||||
static uint64_t g_fullUpdateWrites = 0;
|
||||
static int g_fullUpdateBudget = 0;
|
||||
static thread_local CPlayerSlot g_currentRecipient(-1);
|
||||
static thread_local CPlayerSlot g_lastSnapshotRecipient(-1);
|
||||
|
||||
static CPlayerSlot SlotFromClient(void* client)
|
||||
{
|
||||
@@ -55,7 +52,9 @@ struct ScopedRecipient
|
||||
static void Detour_SendSnapshot(void* client, void* snapshot)
|
||||
{
|
||||
++g_snapshotCalls;
|
||||
ScopedRecipient recipient(client ? SlotFromClient(client) : CPlayerSlot(-1));
|
||||
CPlayerSlot slot(client ? SlotFromClient(client) : CPlayerSlot(-1));
|
||||
g_lastSnapshotRecipient = slot;
|
||||
ScopedRecipient recipient(slot);
|
||||
if (client && g_fullUpdateBudget > 0)
|
||||
{
|
||||
int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_DeltaTick") : -1;
|
||||
@@ -70,23 +69,27 @@ static void Detour_SendSnapshot(void* client, void* snapshot)
|
||||
g_sendSnapshot(client, snapshot);
|
||||
}
|
||||
|
||||
static void Detour_PackEntities(void* self, void* arg1, int arg2, void* arg3, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8)
|
||||
static void Detour_PackEntities(void* self, void* arg1, int arg2, void* arg3, void* arg4, void* arg5, void* arg6)
|
||||
{
|
||||
++g_packCalls;
|
||||
g_packEntities(self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8);
|
||||
CPlayerSlot recipient((arg2 >= 0 && arg2 < 64) ? arg2 : -1);
|
||||
ScopedRecipient scopedRecipient(recipient);
|
||||
g_packEntities(self, arg1, arg2, arg3, arg4, arg5, arg6);
|
||||
}
|
||||
|
||||
static void Detour_PackEntity(void* self, void* arg1, int entityIndex, void* entityData, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8, void* arg9)
|
||||
{
|
||||
++g_packEntityCalls;
|
||||
|
||||
if (!g_enabled || !g_armed || !entityData || !g_overrides.HasPackedOverrides())
|
||||
if (!g_armed || !entityData || !g_overrides.HasPackedOverrides())
|
||||
{
|
||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||
return;
|
||||
}
|
||||
|
||||
CPlayerSlot recipient = g_currentRecipient;
|
||||
if (recipient.Get() < 0 && g_lastSnapshotRecipient.Get() >= 0)
|
||||
recipient = g_lastSnapshotRecipient;
|
||||
auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData);
|
||||
g_packEntityGenericSpoofs += applied.size();
|
||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||
@@ -184,20 +187,3 @@ void ShutdownSnapshotDetour()
|
||||
g_packEntities = nullptr;
|
||||
g_packEntity = nullptr;
|
||||
}
|
||||
|
||||
void DumpDetourStats()
|
||||
{
|
||||
SPMessage("hooks: snapshot calls=%llu; pack calls=%llu; pack_entity calls=%llu generic_spoofs=%llu; fullupdate writes=%llu budget=%d\n",
|
||||
static_cast<unsigned long long>(g_snapshotCalls),
|
||||
static_cast<unsigned long long>(g_packCalls),
|
||||
static_cast<unsigned long long>(g_packEntityCalls),
|
||||
static_cast<unsigned long long>(g_packEntityGenericSpoofs),
|
||||
static_cast<unsigned long long>(g_fullUpdateWrites),
|
||||
g_fullUpdateBudget);
|
||||
}
|
||||
|
||||
void RequestSnapshotFullUpdates(int budget)
|
||||
{
|
||||
if (budget > g_fullUpdateBudget)
|
||||
g_fullUpdateBudget = budget;
|
||||
}
|
||||
|
||||
@@ -6,5 +6,3 @@ class CGameConfig;
|
||||
|
||||
bool InitSnapshotDetour(CGameConfig* config);
|
||||
void ShutdownSnapshotDetour();
|
||||
void DumpDetourStats();
|
||||
void RequestSnapshotFullUpdates(int budget = 512);
|
||||
|
||||
@@ -80,8 +80,6 @@ CModule::CModule(const char* relativePath, const char* module) : m_name(module)
|
||||
|
||||
if (!GetModuleInformation(m_handle, &m_base, &m_size, m_sections))
|
||||
Error("[SendProxy] Could not inspect %s\n", path);
|
||||
|
||||
SPMessage("module %s base=%p size=%zu\n", module, m_base, m_size);
|
||||
}
|
||||
|
||||
void* CModule::FindSignature(const uint8_t* pattern, size_t length, int& error) const
|
||||
|
||||
@@ -53,6 +53,34 @@ static std::vector<std::string> SplitString(const char* value, char separator)
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string NormalizeSchemaClassName(std::string typeName)
|
||||
{
|
||||
while (!typeName.empty() && (typeName.back() == '*' || typeName.back() == '&' || typeName.back() == ' '))
|
||||
typeName.pop_back();
|
||||
while (!typeName.empty() && typeName.front() == ' ')
|
||||
typeName.erase(typeName.begin());
|
||||
|
||||
constexpr const char* classPrefix = "class ";
|
||||
constexpr const char* structPrefix = "struct ";
|
||||
if (typeName.rfind(classPrefix, 0) == 0)
|
||||
typeName.erase(0, strlen(classPrefix));
|
||||
else if (typeName.rfind(structPrefix, 0) == 0)
|
||||
typeName.erase(0, strlen(structPrefix));
|
||||
|
||||
return typeName;
|
||||
}
|
||||
|
||||
static bool IsPointerSchemaType(const std::string& typeName)
|
||||
{
|
||||
for (auto it = typeName.rbegin(); it != typeName.rend(); ++it)
|
||||
{
|
||||
if (*it == ' ')
|
||||
continue;
|
||||
return *it == '*';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OverrideManager::Clear()
|
||||
{
|
||||
for (const auto& rule : m_rules)
|
||||
@@ -75,7 +103,7 @@ void OverrideManager::Compact()
|
||||
RebuildIndex();
|
||||
}
|
||||
|
||||
bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::string& error) const
|
||||
bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::vector<FieldAddressStep>& addressSteps, std::string& error) const
|
||||
{
|
||||
if (!className || !className[0] || !fieldPath || !fieldPath[0])
|
||||
{
|
||||
@@ -92,6 +120,7 @@ bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldP
|
||||
|
||||
offset = 0;
|
||||
size = 0;
|
||||
addressSteps.clear();
|
||||
std::string currentClass = className;
|
||||
for (size_t i = 0; i < parts.size(); ++i)
|
||||
{
|
||||
@@ -102,8 +131,11 @@ bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldP
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += field.offset;
|
||||
size = field.size;
|
||||
FieldAddressStep step {};
|
||||
step.offset = field.offset;
|
||||
step.dereference = i + 1 < parts.size() && IsPointerSchemaType(field.typeName);
|
||||
addressSteps.push_back(step);
|
||||
if (i + 1 < parts.size())
|
||||
{
|
||||
if (field.typeName.empty())
|
||||
@@ -111,13 +143,96 @@ bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldP
|
||||
error = "intermediate field has no schema type name";
|
||||
return false;
|
||||
}
|
||||
currentClass = field.typeName;
|
||||
currentClass = NormalizeSchemaClassName(field.typeName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!addressSteps.empty())
|
||||
offset = addressSteps.front().offset;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t* ResolveRuleAddress(const CompiledOverrideRule& rule, void* entityData)
|
||||
{
|
||||
auto* address = static_cast<uint8_t*>(entityData);
|
||||
for (const auto& step : rule.addressSteps)
|
||||
{
|
||||
address += step.offset;
|
||||
if (!step.dereference)
|
||||
continue;
|
||||
|
||||
address = *reinterpret_cast<uint8_t**>(address);
|
||||
if (!address)
|
||||
return nullptr;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
static uint8_t* ResolveAddressSteps(const std::vector<FieldAddressStep>& steps, void* entityData)
|
||||
{
|
||||
auto* address = static_cast<uint8_t*>(entityData);
|
||||
for (const auto& step : steps)
|
||||
{
|
||||
address += step.offset;
|
||||
if (!step.dereference)
|
||||
continue;
|
||||
|
||||
address = *reinterpret_cast<uint8_t**>(address);
|
||||
if (!address)
|
||||
return nullptr;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
static void SaveAndWrite(std::vector<AppliedOverride>& applied, uint8_t* address, const void* value, size_t size)
|
||||
{
|
||||
AppliedOverride saved {};
|
||||
saved.address = address;
|
||||
saved.original.resize(size);
|
||||
memcpy(saved.original.data(), address, saved.original.size());
|
||||
memcpy(address, value, size);
|
||||
applied.push_back(saved);
|
||||
}
|
||||
|
||||
static void SaveAndWriteIfChanged(std::vector<AppliedOverride>& applied, uint8_t* address, const void* value, size_t size)
|
||||
{
|
||||
if (memcmp(address, value, size) == 0)
|
||||
return;
|
||||
|
||||
SaveAndWrite(applied, address, value, size);
|
||||
}
|
||||
|
||||
static void ApplyVectorFirstFromField(const CompiledOverrideRule& rule, void* entityData, std::vector<AppliedOverride>& applied)
|
||||
{
|
||||
auto* vectorAddress = ResolveAddressSteps(rule.addressSteps, entityData);
|
||||
auto* sourceAddress = ResolveAddressSteps(rule.sourceAddressSteps, entityData);
|
||||
if (!vectorAddress || !sourceAddress)
|
||||
return;
|
||||
|
||||
const uint32_t sourceHandle = *reinterpret_cast<uint32_t*>(sourceAddress);
|
||||
constexpr uint32_t invalidHandle = 0xffffffff;
|
||||
const int32_t count = *reinterpret_cast<int32_t*>(vectorAddress);
|
||||
auto* elements = *reinterpret_cast<uint8_t**>(vectorAddress + 8);
|
||||
if (count < 0 || count > 128)
|
||||
return;
|
||||
|
||||
auto* countAddress = reinterpret_cast<uint8_t*>(&*reinterpret_cast<int32_t*>(vectorAddress));
|
||||
if (sourceHandle == invalidHandle)
|
||||
{
|
||||
const int32_t emptyCount = 0;
|
||||
SaveAndWriteIfChanged(applied, countAddress, &emptyCount, sizeof(emptyCount));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!elements)
|
||||
return;
|
||||
|
||||
const int32_t activeOnlyCount = 1;
|
||||
SaveAndWriteIfChanged(applied, countAddress, &activeOnlyCount, sizeof(activeOnlyCount));
|
||||
SaveAndWriteIfChanged(applied, elements, &sourceHandle, sizeof(sourceHandle));
|
||||
}
|
||||
|
||||
void OverrideManager::RebuildIndex()
|
||||
{
|
||||
m_rulesByEntity.clear();
|
||||
@@ -139,6 +254,17 @@ bool OverrideManager::MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offs
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OverrideManager::MarkEntityFullDirty(int entityIndex)
|
||||
{
|
||||
auto* identity = FindIdentityByIndex(entityIndex);
|
||||
if (!identity || !identity->m_pInstance)
|
||||
return false;
|
||||
|
||||
NetworkStateChangedData data(true);
|
||||
identity->m_pInstance->NetworkStateChanged(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
int OverrideManager::AddRuleBytes(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const void* value, int valueSize, std::string& error)
|
||||
{
|
||||
if (recipientSlot < -1 || recipientSlot >= 64)
|
||||
@@ -163,7 +289,7 @@ int OverrideManager::AddRuleBytes(int recipientSlot, int entityIndex, const char
|
||||
rule.entityIndex = entityIndex;
|
||||
rule.className = className ? className : "";
|
||||
rule.fieldPath = fieldPath ? fieldPath : "";
|
||||
if (!ResolveFieldPath(rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size, error))
|
||||
if (!ResolveFieldPath(rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size, rule.addressSteps, error))
|
||||
return 0;
|
||||
if (rule.size > 0 && valueSize > rule.size)
|
||||
{
|
||||
@@ -175,7 +301,46 @@ int OverrideManager::AddRuleBytes(int recipientSlot, int entityIndex, const char
|
||||
rule.value.assign(bytes, bytes + valueSize);
|
||||
m_rules.push_back(rule);
|
||||
RebuildIndex();
|
||||
MarkEntityFieldDirtyByOffset(entityIndex, rule.offset);
|
||||
MarkEntityFullDirty(entityIndex);
|
||||
return rule.id;
|
||||
}
|
||||
|
||||
int OverrideManager::AddRuleVectorFirstFromField(int recipientSlot, int entityIndex, const char* className, const char* vectorFieldPath, const char* sourceFieldPath, std::string& error)
|
||||
{
|
||||
if (recipientSlot < -1 || recipientSlot >= 64)
|
||||
{
|
||||
error = "recipient slot must be all/-1 or 0..63";
|
||||
return 0;
|
||||
}
|
||||
if (entityIndex < 0)
|
||||
{
|
||||
error = "entity index must be >= 0";
|
||||
return 0;
|
||||
}
|
||||
|
||||
CompiledOverrideRule rule;
|
||||
rule.id = m_nextId++;
|
||||
rule.kind = OverrideRuleKind::VectorFirstFromField;
|
||||
rule.recipientSlot = recipientSlot;
|
||||
rule.entityIndex = entityIndex;
|
||||
rule.className = className ? className : "";
|
||||
rule.fieldPath = vectorFieldPath ? vectorFieldPath : "";
|
||||
rule.sourceFieldPath = sourceFieldPath ? sourceFieldPath : "";
|
||||
if (!ResolveFieldPath(rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size, rule.addressSteps, error))
|
||||
return 0;
|
||||
|
||||
int32_t sourceOffset = 0;
|
||||
if (!ResolveFieldPath(rule.className.c_str(), rule.sourceFieldPath.c_str(), sourceOffset, rule.sourceSize, rule.sourceAddressSteps, error))
|
||||
return 0;
|
||||
if (rule.sourceSize > 0 && rule.sourceSize < 4)
|
||||
{
|
||||
error = "source field is smaller than a handle";
|
||||
return 0;
|
||||
}
|
||||
|
||||
m_rules.push_back(rule);
|
||||
RebuildIndex();
|
||||
MarkEntityFullDirty(entityIndex);
|
||||
return rule.id;
|
||||
}
|
||||
|
||||
@@ -186,7 +351,7 @@ bool OverrideManager::RemoveRule(int id)
|
||||
if (rule.id != id)
|
||||
continue;
|
||||
rule.enabled = false;
|
||||
MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset);
|
||||
MarkEntityFullDirty(rule.entityIndex);
|
||||
RebuildIndex();
|
||||
return true;
|
||||
}
|
||||
@@ -197,7 +362,8 @@ bool OverrideManager::MarkEntityFieldDirty(int entityIndex, const char* classNam
|
||||
{
|
||||
int32_t offset = 0;
|
||||
int32_t size = 0;
|
||||
if (!ResolveFieldPath(className, fieldPath, offset, size, error))
|
||||
std::vector<FieldAddressStep> addressSteps;
|
||||
if (!ResolveFieldPath(className, fieldPath, offset, size, addressSteps, error))
|
||||
return false;
|
||||
return MarkEntityFieldDirtyByOffset(entityIndex, offset);
|
||||
}
|
||||
@@ -212,19 +378,45 @@ std::vector<AppliedOverride> OverrideManager::ApplyForPackedEntity(CPlayerSlot r
|
||||
if (found == m_rulesByEntity.end())
|
||||
return applied;
|
||||
|
||||
int effectiveRecipient = recipient.Get();
|
||||
int uniqueRuleRecipient = -2;
|
||||
for (size_t ruleIndex : found->second)
|
||||
{
|
||||
const auto& rule = m_rules[ruleIndex];
|
||||
if (!rule.enabled || (rule.recipientSlot != -1 && rule.recipientSlot != recipient.Get()))
|
||||
if (!rule.enabled || rule.recipientSlot < 0)
|
||||
continue;
|
||||
if (uniqueRuleRecipient == -2)
|
||||
uniqueRuleRecipient = rule.recipientSlot;
|
||||
else if (uniqueRuleRecipient != rule.recipientSlot)
|
||||
{
|
||||
uniqueRuleRecipient = -2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((effectiveRecipient < 0 || effectiveRecipient == 0 || effectiveRecipient == 1) && uniqueRuleRecipient >= 0)
|
||||
effectiveRecipient = uniqueRuleRecipient;
|
||||
|
||||
for (size_t ruleIndex : found->second)
|
||||
{
|
||||
const auto& rule = m_rules[ruleIndex];
|
||||
if (!rule.enabled)
|
||||
continue;
|
||||
|
||||
auto* address = reinterpret_cast<uint8_t*>(entityData) + rule.offset;
|
||||
AppliedOverride saved {};
|
||||
saved.address = address;
|
||||
saved.original.resize(rule.value.size());
|
||||
memcpy(saved.original.data(), address, saved.original.size());
|
||||
memcpy(address, rule.value.data(), rule.value.size());
|
||||
applied.push_back(saved);
|
||||
if (rule.recipientSlot != -1 && rule.recipientSlot != effectiveRecipient)
|
||||
continue;
|
||||
|
||||
if (rule.kind == OverrideRuleKind::VectorFirstFromField)
|
||||
{
|
||||
ApplyVectorFirstFromField(rule, entityData, applied);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* address = ResolveRuleAddress(rule, entityData);
|
||||
if (!address)
|
||||
continue;
|
||||
|
||||
SaveAndWrite(applied, address, rule.value.data(), rule.value.size());
|
||||
}
|
||||
|
||||
return applied;
|
||||
|
||||
@@ -13,6 +13,18 @@ struct AppliedOverride
|
||||
std::vector<uint8_t> original;
|
||||
};
|
||||
|
||||
enum class OverrideRuleKind
|
||||
{
|
||||
Bytes,
|
||||
VectorFirstFromField,
|
||||
};
|
||||
|
||||
struct FieldAddressStep
|
||||
{
|
||||
int32_t offset {};
|
||||
bool dereference {};
|
||||
};
|
||||
|
||||
struct CompiledOverrideRule
|
||||
{
|
||||
int id {};
|
||||
@@ -20,9 +32,14 @@ struct CompiledOverrideRule
|
||||
int entityIndex {-1};
|
||||
std::string className;
|
||||
std::string fieldPath;
|
||||
std::string sourceFieldPath;
|
||||
int32_t offset {};
|
||||
int32_t size {};
|
||||
int32_t sourceSize {};
|
||||
std::vector<FieldAddressStep> addressSteps;
|
||||
std::vector<FieldAddressStep> sourceAddressSteps;
|
||||
std::vector<uint8_t> value;
|
||||
OverrideRuleKind kind {OverrideRuleKind::Bytes};
|
||||
bool enabled {true};
|
||||
};
|
||||
|
||||
@@ -32,18 +49,19 @@ public:
|
||||
void Clear();
|
||||
void Compact();
|
||||
int AddRuleBytes(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const void* value, int valueSize, std::string& error);
|
||||
int AddRuleVectorFirstFromField(int recipientSlot, int entityIndex, const char* className, const char* vectorFieldPath, const char* sourceFieldPath, std::string& error);
|
||||
bool RemoveRule(int id);
|
||||
bool MarkEntityFieldDirty(int entityIndex, const char* className, const char* fieldPath, std::string& error);
|
||||
bool Empty() const { return m_rules.empty(); }
|
||||
bool HasPackedOverrides() const { return !m_rulesByEntity.empty(); }
|
||||
|
||||
std::vector<AppliedOverride> ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData);
|
||||
void Restore(const std::vector<AppliedOverride>& applied);
|
||||
|
||||
private:
|
||||
bool ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::string& error) const;
|
||||
bool ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::vector<FieldAddressStep>& addressSteps, std::string& error) const;
|
||||
void RebuildIndex();
|
||||
bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset);
|
||||
bool MarkEntityFullDirty(int entityIndex);
|
||||
|
||||
std::vector<CompiledOverrideRule> m_rules;
|
||||
std::unordered_map<int, std::vector<size_t>> m_rulesByEntity;
|
||||
|
||||
@@ -9,21 +9,17 @@
|
||||
#include "schema.h"
|
||||
#include "schemasystem/schemasystem.h"
|
||||
#include "interfaces/interfaces.h"
|
||||
#include "entity2/entityclass.h"
|
||||
#include "entity2/entitysystem.h"
|
||||
|
||||
#include "tier0/memdbgon.h"
|
||||
|
||||
SendProxyPlugin g_SendProxy;
|
||||
IVEngineServer2* g_engine = nullptr;
|
||||
INetworkServerService* g_networkServerService = nullptr;
|
||||
IServerGameClients* g_gameClients = nullptr;
|
||||
ISchemaSystem* g_schemaSystem = nullptr;
|
||||
IFileSystem* g_fileSystem = nullptr;
|
||||
IGameResourceService* g_gameResourceService = nullptr;
|
||||
CGameConfig* g_gameConfig = nullptr;
|
||||
CGameEntitySystem* g_entitySystem = nullptr;
|
||||
bool g_enabled = true;
|
||||
bool g_armed = false;
|
||||
|
||||
void SPMessage(const char* msg, ...)
|
||||
@@ -69,7 +65,20 @@ extern "C" __attribute__((visibility("default"))) int SendProxy_SetOverride(int
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_enabled = true;
|
||||
g_armed = true;
|
||||
return id;
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) int SendProxy_SetVectorFirstFromField(int recipientSlot, int entityIndex, const char* className, const char* vectorFieldPath, const char* sourceFieldPath)
|
||||
{
|
||||
std::string error;
|
||||
int id = g_overrides.AddRuleVectorFirstFromField(recipientSlot, entityIndex, className, vectorFieldPath, sourceFieldPath, error);
|
||||
if (!id)
|
||||
{
|
||||
SPWarning("SendProxy_SetVectorFirstFromField failed: %s\n", error.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
g_armed = true;
|
||||
return id;
|
||||
}
|
||||
@@ -103,8 +112,6 @@ bool SendProxyPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxle
|
||||
PLUGIN_SAVEVARS();
|
||||
|
||||
GET_V_IFACE_CURRENT(GetEngineFactory, g_engine, IVEngineServer2, SOURCE2ENGINETOSERVER_INTERFACE_VERSION);
|
||||
GET_V_IFACE_ANY(GetEngineFactory, g_networkServerService, INetworkServerService, NETWORKSERVERSERVICE_INTERFACE_VERSION);
|
||||
GET_V_IFACE_ANY(GetServerFactory, g_gameClients, IServerGameClients, SOURCE2GAMECLIENTS_INTERFACE_VERSION);
|
||||
GET_V_IFACE_CURRENT(GetEngineFactory, g_schemaSystem, ISchemaSystem, SCHEMASYSTEM_INTERFACE_VERSION);
|
||||
GET_V_IFACE_CURRENT(GetEngineFactory, g_gameResourceService, IGameResourceService, GAMERESOURCESERVICESERVER_INTERFACE_VERSION);
|
||||
GET_V_IFACE_ANY(GetFileSystemFactory, g_fileSystem, IFileSystem, FILESYSTEM_INTERFACE_VERSION);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "schemasystem/schemasystem.h"
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
static ISchemaSystem* g_schema = nullptr;
|
||||
static std::map<std::string, SchemaField> g_cache;
|
||||
@@ -18,6 +19,48 @@ static bool IsNetworked(const SchemaClassFieldData_t& field)
|
||||
return false;
|
||||
}
|
||||
|
||||
static SchemaField FieldFromSchemaData(const SchemaClassFieldData_t& field, uint32_t baseOffset)
|
||||
{
|
||||
SchemaField result {};
|
||||
result.offset = static_cast<int32_t>(baseOffset + field.m_nSingleInheritanceOffset);
|
||||
result.networked = IsNetworked(field);
|
||||
if (field.m_pType)
|
||||
{
|
||||
int size = 0;
|
||||
uint8 alignment = 0;
|
||||
if (field.m_pType->GetSizeAndAlignment(size, alignment))
|
||||
result.size = size;
|
||||
result.typeName = field.m_pType->m_sTypeName.String();
|
||||
}
|
||||
result.found = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool FindFieldRecursive(SchemaClassInfoData_t* info, const char* fieldName, uint32_t baseOffset, SchemaField& result, std::set<const SchemaClassInfoData_t*>& visited)
|
||||
{
|
||||
if (!info || visited.find(info) != visited.end())
|
||||
return false;
|
||||
visited.insert(info);
|
||||
|
||||
for (int i = 0; i < info->m_nFieldCount; ++i)
|
||||
{
|
||||
const auto& field = info->m_pFields[i];
|
||||
if (V_strcmp(field.m_pszName, fieldName))
|
||||
continue;
|
||||
result = FieldFromSchemaData(field, baseOffset);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->m_nBaseClassCount; ++i)
|
||||
{
|
||||
auto* baseInfo = info->m_pBaseClasses[i].m_pClass;
|
||||
if (FindFieldRecursive(baseInfo, fieldName, baseOffset + info->m_pBaseClasses[i].m_nOffset, result, visited))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void schema::Init(ISchemaSystem* system)
|
||||
{
|
||||
g_schema = system;
|
||||
@@ -48,22 +91,9 @@ SchemaField schema::FindField(const char* className, const char* fieldName)
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->m_nFieldCount; ++i)
|
||||
std::set<const SchemaClassInfoData_t*> visited;
|
||||
if (FindFieldRecursive(info, fieldName, 0, result, visited))
|
||||
{
|
||||
const auto& field = info->m_pFields[i];
|
||||
if (V_strcmp(field.m_pszName, fieldName))
|
||||
continue;
|
||||
result.offset = field.m_nSingleInheritanceOffset;
|
||||
result.networked = IsNetworked(field);
|
||||
if (field.m_pType)
|
||||
{
|
||||
int size = 0;
|
||||
uint8 alignment = 0;
|
||||
if (field.m_pType->GetSizeAndAlignment(size, alignment))
|
||||
result.size = size;
|
||||
result.typeName = field.m_pType->m_sTypeName.String();
|
||||
}
|
||||
result.found = true;
|
||||
g_cache[key] = result;
|
||||
return result;
|
||||
}
|
||||
@@ -72,36 +102,3 @@ SchemaField schema::FindField(const char* className, const char* fieldName)
|
||||
g_cache[key] = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
void schema::DumpFields(const char* className, const char* contains)
|
||||
{
|
||||
auto* typeScope = g_schema ? g_schema->FindTypeScopeForModule("libserver.so") : nullptr;
|
||||
if (!typeScope)
|
||||
{
|
||||
SPWarning("schema type scope for libserver.so is unavailable\n");
|
||||
return;
|
||||
}
|
||||
|
||||
SchemaClassInfoData_t* info = typeScope->FindDeclaredClass(className).Get();
|
||||
if (!info)
|
||||
{
|
||||
SPWarning("schema class %s not found\n", className);
|
||||
return;
|
||||
}
|
||||
|
||||
int printed = 0;
|
||||
for (int i = 0; i < info->m_nFieldCount; ++i)
|
||||
{
|
||||
const auto& field = info->m_pFields[i];
|
||||
if (contains && contains[0] && !V_stristr(field.m_pszName, contains))
|
||||
continue;
|
||||
const char* typeName = field.m_pType ? field.m_pType->m_sTypeName.String() : "";
|
||||
int size = 0;
|
||||
uint8 alignment = 0;
|
||||
if (field.m_pType)
|
||||
field.m_pType->GetSizeAndAlignment(size, alignment);
|
||||
SPMessage("%s::%s offset=%d size=%d type=%s networked=%d\n", className, field.m_pszName, field.m_nSingleInheritanceOffset, size, typeName ? typeName : "", IsNetworked(field) ? 1 : 0);
|
||||
++printed;
|
||||
}
|
||||
SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : "<none>");
|
||||
}
|
||||
|
||||
@@ -18,5 +18,4 @@ namespace schema
|
||||
{
|
||||
void Init(ISchemaSystem* system);
|
||||
SchemaField FindField(const char* className, const char* fieldName);
|
||||
void DumpFields(const char* className, const char* contains);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user