working weapon concealment
This commit is contained in:
33
README.md
33
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,20 @@ It demonstrates:
|
||||
- Coloring dropped AK-47s red for Terrorist viewers.
|
||||
- Coloring dropped M4A1-S blue for CT viewers.
|
||||
|
||||
Concealed-carry handle spoof:
|
||||
|
||||
```text
|
||||
examples/CounterStrikeSharp/SendProxyConcealedCarry/
|
||||
```
|
||||
|
||||
It hides owned primary weapons, secondary weapons, and grenades from every recipient except the owning player by spoofing weapon-related handles to `0xffffffff`:
|
||||
|
||||
- `CBasePlayerPawn::m_pWeaponServices.m_hActiveWeapon`
|
||||
- `CBasePlayerPawn::m_pWeaponServices.m_hLastWeapon`
|
||||
- `CBaseEntity::m_hOwnerEntity` on carried weapon entities
|
||||
|
||||
It intentionally does not set `EF_NODRAW` and does not remove or 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 +180,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,248 @@
|
||||
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);
|
||||
}
|
||||
@@ -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,19 +6,40 @@ 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;
|
||||
private readonly DumpSchemaDelegate _dumpSchema;
|
||||
private readonly DumpEnumDelegate _dumpEnum;
|
||||
|
||||
public SendProxyNative(string libraryPath)
|
||||
{
|
||||
_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");
|
||||
_dumpSchema = GetExport<DumpSchemaDelegate>("SendProxy_DumpSchema");
|
||||
_dumpEnum = GetExport<DumpEnumDelegate>("SendProxy_DumpEnum");
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -61,6 +82,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);
|
||||
@@ -76,6 +102,16 @@ public sealed class SendProxyNative : IDisposable
|
||||
return _markDirty(entityIndex, className, fieldPath);
|
||||
}
|
||||
|
||||
public void DumpSchema(string className, string contains)
|
||||
{
|
||||
_dumpSchema(className, contains);
|
||||
}
|
||||
|
||||
public void DumpEnum(string enumName)
|
||||
{
|
||||
_dumpEnum(enumName);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
NativeLibrary.Free(_library);
|
||||
@@ -89,6 +125,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 +138,10 @@ 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);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
private delegate void DumpSchemaDelegate(string className, string contains);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
private delegate void DumpEnumDelegate(string enumName);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
.Where(player => player.IsValid && !player.IsBot && !player.IsHLTV)
|
||||
.ToArray();
|
||||
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)
|
||||
|
||||
@@ -13,7 +13,7 @@ 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;
|
||||
@@ -26,8 +26,11 @@ static uint64_t g_packCalls = 0;
|
||||
static uint64_t g_packEntityCalls = 0;
|
||||
static uint64_t g_packEntityGenericSpoofs = 0;
|
||||
static uint64_t g_fullUpdateWrites = 0;
|
||||
static uint64_t g_packEntitiesDebugLogs = 0;
|
||||
static uint64_t g_packEntityRuleDebugLogs = 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 +58,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,10 +75,26 @@ 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);
|
||||
if (g_packEntitiesDebugLogs < 16)
|
||||
{
|
||||
SPMessage("pack_entities call %llu self=%p arg1=%p arg2=%d recipient=%d args=[%p %p %p %p]\n",
|
||||
static_cast<unsigned long long>(g_packCalls),
|
||||
self,
|
||||
arg1,
|
||||
arg2,
|
||||
recipient.Get(),
|
||||
arg3,
|
||||
arg4,
|
||||
arg5,
|
||||
arg6);
|
||||
++g_packEntitiesDebugLogs;
|
||||
}
|
||||
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)
|
||||
@@ -87,6 +108,26 @@ static void Detour_PackEntity(void* self, void* arg1, int entityIndex, void* ent
|
||||
}
|
||||
|
||||
CPlayerSlot recipient = g_currentRecipient;
|
||||
if (recipient.Get() < 0 && g_lastSnapshotRecipient.Get() >= 0)
|
||||
recipient = g_lastSnapshotRecipient;
|
||||
if (g_packEntityRuleDebugLogs < 64 && g_overrides.HasRulesForEntity(entityIndex))
|
||||
{
|
||||
SPMessage("pack_entity ruled call %llu entity=%d current=%d lastSnapshot=%d args self=%p arg1=%p data=%p arg4=%p arg5=%p arg6=%p arg7=%p arg8=%p arg9=%p\n",
|
||||
static_cast<unsigned long long>(g_packEntityCalls),
|
||||
entityIndex,
|
||||
g_currentRecipient.Get(),
|
||||
g_lastSnapshotRecipient.Get(),
|
||||
self,
|
||||
arg1,
|
||||
entityData,
|
||||
arg4,
|
||||
arg5,
|
||||
arg6,
|
||||
arg7,
|
||||
arg8,
|
||||
arg9);
|
||||
++g_packEntityRuleDebugLogs;
|
||||
}
|
||||
auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData);
|
||||
g_packEntityGenericSpoofs += applied.size();
|
||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||
|
||||
@@ -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,119 @@ 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 (!rule.debugLogged)
|
||||
{
|
||||
uint32_t h0 = elements && count > 0 ? *reinterpret_cast<uint32_t*>(elements) : invalidHandle;
|
||||
uint32_t h1 = elements && count > 1 ? *reinterpret_cast<uint32_t*>(elements + 4) : invalidHandle;
|
||||
uint32_t h2 = elements && count > 2 ? *reinterpret_cast<uint32_t*>(elements + 8) : invalidHandle;
|
||||
const auto* raw = reinterpret_cast<const uint8_t*>(vectorAddress);
|
||||
SPMessage("vector apply rule %d recipient=%d entity=%d vec=%p src=%p count=%d elems=%p active=%08x first=[%08x %08x %08x] raw=%02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
|
||||
rule.id,
|
||||
rule.recipientSlot,
|
||||
rule.entityIndex,
|
||||
vectorAddress,
|
||||
sourceAddress,
|
||||
count,
|
||||
elements,
|
||||
sourceHandle,
|
||||
h0,
|
||||
h1,
|
||||
h2,
|
||||
raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
|
||||
raw[8], raw[9], raw[10], raw[11], raw[12], raw[13], raw[14], raw[15],
|
||||
raw[16], raw[17], raw[18], raw[19], raw[20], raw[21], raw[22], raw[23]);
|
||||
rule.debugLogged = true;
|
||||
}
|
||||
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 +277,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 +312,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)
|
||||
{
|
||||
@@ -174,8 +323,72 @@ int OverrideManager::AddRuleBytes(int recipientSlot, int entityIndex, const char
|
||||
auto* bytes = static_cast<const uint8_t*>(value);
|
||||
rule.value.assign(bytes, bytes + valueSize);
|
||||
m_rules.push_back(rule);
|
||||
if (rule.fieldPath.find("m_hMyWeapons") != std::string::npos)
|
||||
{
|
||||
SPMessage("rule %d recipient=%d entity=%d %s::%s size=%d valueSize=%d topOffset=%d steps=%zu\n",
|
||||
rule.id,
|
||||
rule.recipientSlot,
|
||||
rule.entityIndex,
|
||||
rule.className.c_str(),
|
||||
rule.fieldPath.c_str(),
|
||||
rule.size,
|
||||
valueSize,
|
||||
rule.offset,
|
||||
rule.addressSteps.size());
|
||||
}
|
||||
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);
|
||||
SPMessage("vector-first rule %d recipient=%d entity=%d %s::%s <- %s vectorSize=%d sourceSize=%d topOffset=%d steps=%zu sourceSteps=%zu\n",
|
||||
rule.id,
|
||||
rule.recipientSlot,
|
||||
rule.entityIndex,
|
||||
rule.className.c_str(),
|
||||
rule.fieldPath.c_str(),
|
||||
rule.sourceFieldPath.c_str(),
|
||||
rule.size,
|
||||
rule.sourceSize,
|
||||
rule.offset,
|
||||
rule.addressSteps.size(),
|
||||
rule.sourceAddressSteps.size());
|
||||
RebuildIndex();
|
||||
MarkEntityFullDirty(entityIndex);
|
||||
return rule.id;
|
||||
}
|
||||
|
||||
@@ -186,7 +399,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 +410,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 +426,69 @@ 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)
|
||||
{
|
||||
static int fallbackLogs = 0;
|
||||
if (fallbackLogs < 64)
|
||||
{
|
||||
SPMessage("entity=%d unresolved recipient current=%d using unique rule recipient=%d\n",
|
||||
entityIndex,
|
||||
effectiveRecipient,
|
||||
uniqueRuleRecipient);
|
||||
++fallbackLogs;
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (!rule.recipientMismatchLogged)
|
||||
{
|
||||
SPMessage("rule %d entity=%d matched packed entity but skipped recipient rule=%d current=%d field=%s::%s\n",
|
||||
rule.id,
|
||||
entityIndex,
|
||||
rule.recipientSlot,
|
||||
effectiveRecipient,
|
||||
rule.className.c_str(),
|
||||
rule.fieldPath.c_str());
|
||||
rule.recipientMismatchLogged = true;
|
||||
}
|
||||
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,10 +32,17 @@ 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};
|
||||
mutable bool debugLogged {false};
|
||||
mutable bool recipientMismatchLogged {false};
|
||||
};
|
||||
|
||||
class OverrideManager
|
||||
@@ -32,18 +51,21 @@ 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(); }
|
||||
bool HasRulesForEntity(int entityIndex) const { return m_rulesByEntity.find(entityIndex) != m_rulesByEntity.end(); }
|
||||
|
||||
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;
|
||||
|
||||
@@ -74,6 +74,21 @@ extern "C" __attribute__((visibility("default"))) int SendProxy_SetOverride(int
|
||||
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_enabled = true;
|
||||
g_armed = true;
|
||||
return id;
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) bool SendProxy_RemoveOverride(int ruleId)
|
||||
{
|
||||
return g_overrides.RemoveRule(ruleId);
|
||||
@@ -96,6 +111,16 @@ extern "C" __attribute__((visibility("default"))) bool SendProxy_MarkDirty(int e
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) void SendProxy_DumpSchema(const char* className, const char* contains)
|
||||
{
|
||||
schema::DumpFields(className, contains);
|
||||
}
|
||||
|
||||
extern "C" __attribute__((visibility("default"))) void SendProxy_DumpEnum(const char* enumName)
|
||||
{
|
||||
schema::DumpEnum(enumName);
|
||||
}
|
||||
|
||||
PLUGIN_EXPOSE(SendProxyPlugin, g_SendProxy);
|
||||
|
||||
bool SendProxyPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late)
|
||||
|
||||
131
src/schema.cpp
131
src/schema.cpp
@@ -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,81 @@ 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;
|
||||
}
|
||||
|
||||
static void DumpFieldsRecursive(const char* rootClassName, SchemaClassInfoData_t* info, const char* contains, uint32_t baseOffset, int& printed, std::set<const SchemaClassInfoData_t*>& visited)
|
||||
{
|
||||
if (!info || visited.find(info) != visited.end())
|
||||
return;
|
||||
visited.insert(info);
|
||||
|
||||
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::%s offset=%d size=%d type=%s networked=%d\n",
|
||||
rootClassName,
|
||||
info->m_pszName ? info->m_pszName : "<unknown>",
|
||||
field.m_pszName,
|
||||
static_cast<int>(baseOffset + field.m_nSingleInheritanceOffset),
|
||||
size,
|
||||
typeName ? typeName : "",
|
||||
IsNetworked(field) ? 1 : 0);
|
||||
++printed;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->m_nBaseClassCount; ++i)
|
||||
{
|
||||
DumpFieldsRecursive(rootClassName, info->m_pBaseClasses[i].m_pClass, contains, baseOffset + info->m_pBaseClasses[i].m_nOffset, printed, visited);
|
||||
}
|
||||
}
|
||||
|
||||
void schema::Init(ISchemaSystem* system)
|
||||
{
|
||||
g_schema = system;
|
||||
@@ -48,22 +124,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;
|
||||
}
|
||||
@@ -90,18 +153,30 @@ void schema::DumpFields(const char* className, const char* contains)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
std::set<const SchemaClassInfoData_t*> visited;
|
||||
DumpFieldsRecursive(className, info, contains, 0, printed, visited);
|
||||
SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : "<none>");
|
||||
}
|
||||
|
||||
void schema::DumpEnum(const char* enumName)
|
||||
{
|
||||
auto* typeScope = g_schema ? g_schema->FindTypeScopeForModule("libserver.so") : nullptr;
|
||||
if (!typeScope)
|
||||
{
|
||||
SPWarning("schema type scope for libserver.so is unavailable\n");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* info = typeScope->FindDeclaredEnum(enumName).Get();
|
||||
if (!info)
|
||||
{
|
||||
SPWarning("schema enum %s not found\n", enumName);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->m_nEnumeratorCount; ++i)
|
||||
{
|
||||
const auto& enumerator = info->m_pEnumerators[i];
|
||||
SPMessage("%s::%s = %lld\n", enumName, enumerator.m_pszName ? enumerator.m_pszName : "<unknown>", static_cast<long long>(enumerator.m_nValue));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ namespace schema
|
||||
void Init(ISchemaSystem* system);
|
||||
SchemaField FindField(const char* className, const char* fieldName);
|
||||
void DumpFields(const char* className, const char* contains);
|
||||
void DumpEnum(const char* enumName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user