working weapon concealment

This commit is contained in:
2026-08-10 16:59:45 +02:00
parent 4e9500f0ea
commit 9a23169b6b
13 changed files with 927 additions and 66 deletions

View File

@@ -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:

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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);
}