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
|
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
|
```csharp
|
||||||
string libraryPath = Path.Combine(Server.GameDirectory, "addons", "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so");
|
var sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory);
|
||||||
var sendProxy = new SendProxyNative(libraryPath);
|
|
||||||
|
|
||||||
int ruleId = sendProxy.SetInt32(
|
int ruleId = sendProxy.SetInt32(
|
||||||
recipientSlot: viewer.Slot,
|
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.
|
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
|
```text
|
||||||
examples/CounterStrikeSharp/SendProxyWeaponVisibility/
|
examples/CounterStrikeSharp/SendProxyWeaponVisibility/
|
||||||
@@ -90,6 +97,20 @@ It demonstrates:
|
|||||||
- Coloring dropped AK-47s red for Terrorist viewers.
|
- Coloring dropped AK-47s red for Terrorist viewers.
|
||||||
- Coloring dropped M4A1-S blue for CT 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.
|
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
|
## Building the Metamod Addon
|
||||||
@@ -159,6 +180,8 @@ Install the built CSS plugin under:
|
|||||||
game/csgo/addons/counterstrikesharp/plugins/SendProxyWeaponVisibility/
|
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.
|
Make sure `SendProxyNative.cs` is included in your CSS project or copied into your plugin source.
|
||||||
|
|
||||||
The example plugin expects the Metamod addon at:
|
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
|
public sealed class SendProxyNative : IDisposable
|
||||||
{
|
{
|
||||||
|
private const string RelativeLibraryPath = "addons/sendproxy/bin/linuxsteamrt64/sendproxy.so";
|
||||||
|
|
||||||
private readonly IntPtr _library;
|
private readonly IntPtr _library;
|
||||||
private readonly SetOverrideDelegate _setOverride;
|
private readonly SetOverrideDelegate _setOverride;
|
||||||
|
private readonly SetVectorFirstFromFieldDelegate _setVectorFirstFromField;
|
||||||
private readonly RemoveOverrideDelegate _removeOverride;
|
private readonly RemoveOverrideDelegate _removeOverride;
|
||||||
private readonly ClearOverridesDelegate _clearOverrides;
|
private readonly ClearOverridesDelegate _clearOverrides;
|
||||||
private readonly MarkDirtyDelegate _markDirty;
|
private readonly MarkDirtyDelegate _markDirty;
|
||||||
|
private readonly DumpSchemaDelegate _dumpSchema;
|
||||||
|
private readonly DumpEnumDelegate _dumpEnum;
|
||||||
|
|
||||||
public SendProxyNative(string libraryPath)
|
public SendProxyNative(string libraryPath)
|
||||||
{
|
{
|
||||||
_library = NativeLibrary.Load(libraryPath);
|
_library = NativeLibrary.Load(libraryPath);
|
||||||
_setOverride = GetExport<SetOverrideDelegate>("SendProxy_SetOverride");
|
_setOverride = GetExport<SetOverrideDelegate>("SendProxy_SetOverride");
|
||||||
|
_setVectorFirstFromField = GetExport<SetVectorFirstFromFieldDelegate>("SendProxy_SetVectorFirstFromField");
|
||||||
_removeOverride = GetExport<RemoveOverrideDelegate>("SendProxy_RemoveOverride");
|
_removeOverride = GetExport<RemoveOverrideDelegate>("SendProxy_RemoveOverride");
|
||||||
_clearOverrides = GetExport<ClearOverridesDelegate>("SendProxy_ClearOverrides");
|
_clearOverrides = GetExport<ClearOverridesDelegate>("SendProxy_ClearOverrides");
|
||||||
_markDirty = GetExport<MarkDirtyDelegate>("SendProxy_MarkDirty");
|
_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)
|
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);
|
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)
|
public bool RemoveOverride(int ruleId)
|
||||||
{
|
{
|
||||||
return _removeOverride(ruleId);
|
return _removeOverride(ruleId);
|
||||||
@@ -76,6 +102,16 @@ public sealed class SendProxyNative : IDisposable
|
|||||||
return _markDirty(entityIndex, className, fieldPath);
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
NativeLibrary.Free(_library);
|
NativeLibrary.Free(_library);
|
||||||
@@ -89,6 +125,9 @@ public sealed class SendProxyNative : IDisposable
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||||
private delegate int SetOverrideDelegate(int recipientSlot, int entityIndex, string className, string fieldPath, byte[] value, int valueSize);
|
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)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
[return: MarshalAs(UnmanagedType.I1)]
|
||||||
private delegate bool RemoveOverrideDelegate(int ruleId);
|
private delegate bool RemoveOverrideDelegate(int ruleId);
|
||||||
@@ -99,4 +138,10 @@ public sealed class SendProxyNative : IDisposable
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||||
[return: MarshalAs(UnmanagedType.I1)]
|
[return: MarshalAs(UnmanagedType.I1)]
|
||||||
private delegate bool MarkDirtyDelegate(int entityIndex, string className, string fieldPath);
|
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)
|
public override void Load(bool hotReload)
|
||||||
{
|
{
|
||||||
_sendProxy = new SendProxyNative(GetSendProxyLibraryPath());
|
_sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory);
|
||||||
_syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT);
|
_syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT);
|
||||||
RegisterListener<Listeners.OnEntityCreated>(_ => Server.NextFrame(SyncRules));
|
RegisterListener<Listeners.OnEntityCreated>(_ => Server.NextFrame(SyncRules));
|
||||||
RegisterListener<Listeners.OnEntityDeleted>(_ => Server.NextFrame(SyncRules));
|
RegisterListener<Listeners.OnEntityDeleted>(_ => Server.NextFrame(SyncRules));
|
||||||
SyncRules();
|
Server.NextFrame(SyncRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Unload(bool hotReload)
|
public override void Unload(bool hotReload)
|
||||||
@@ -45,9 +45,17 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var desired = new Dictionary<RuleKey, RuleValue>();
|
var desired = new Dictionary<RuleKey, RuleValue>();
|
||||||
var viewers = Utilities.GetPlayers()
|
CCSPlayerController[] viewers;
|
||||||
.Where(player => player.IsValid && !player.IsBot && !player.IsHLTV)
|
try
|
||||||
.ToArray();
|
{
|
||||||
|
viewers = Utilities.GetPlayers()
|
||||||
|
.Where(player => player.IsValid && !player.IsBot && !player.IsHLTV)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch (NativeException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AddHealthRules(desired, viewers);
|
AddHealthRules(desired, viewers);
|
||||||
|
|
||||||
@@ -211,14 +219,6 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin
|
|||||||
_activeRules.Clear();
|
_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 RuleKey(int RecipientSlot, int EntityIndex, string ClassName, string FieldPath);
|
||||||
|
|
||||||
private readonly record struct RuleValue(byte[] Bytes)
|
private readonly record struct RuleValue(byte[] Bytes)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ extern bool g_enabled;
|
|||||||
extern bool g_armed;
|
extern bool g_armed;
|
||||||
|
|
||||||
using SendSnapshotFn = void (*)(void*, void*);
|
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*);
|
using PackEntityFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*, void*);
|
||||||
static SendSnapshotFn g_sendSnapshot = nullptr;
|
static SendSnapshotFn g_sendSnapshot = nullptr;
|
||||||
static PackEntitiesFn g_packEntities = 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_packEntityCalls = 0;
|
||||||
static uint64_t g_packEntityGenericSpoofs = 0;
|
static uint64_t g_packEntityGenericSpoofs = 0;
|
||||||
static uint64_t g_fullUpdateWrites = 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 int g_fullUpdateBudget = 0;
|
||||||
static thread_local CPlayerSlot g_currentRecipient(-1);
|
static thread_local CPlayerSlot g_currentRecipient(-1);
|
||||||
|
static thread_local CPlayerSlot g_lastSnapshotRecipient(-1);
|
||||||
|
|
||||||
static CPlayerSlot SlotFromClient(void* client)
|
static CPlayerSlot SlotFromClient(void* client)
|
||||||
{
|
{
|
||||||
@@ -55,7 +58,9 @@ struct ScopedRecipient
|
|||||||
static void Detour_SendSnapshot(void* client, void* snapshot)
|
static void Detour_SendSnapshot(void* client, void* snapshot)
|
||||||
{
|
{
|
||||||
++g_snapshotCalls;
|
++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)
|
if (client && g_fullUpdateBudget > 0)
|
||||||
{
|
{
|
||||||
int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_DeltaTick") : -1;
|
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);
|
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_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)
|
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;
|
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);
|
auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData);
|
||||||
g_packEntityGenericSpoofs += applied.size();
|
g_packEntityGenericSpoofs += applied.size();
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
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;
|
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()
|
void OverrideManager::Clear()
|
||||||
{
|
{
|
||||||
for (const auto& rule : m_rules)
|
for (const auto& rule : m_rules)
|
||||||
@@ -75,7 +103,7 @@ void OverrideManager::Compact()
|
|||||||
RebuildIndex();
|
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])
|
if (!className || !className[0] || !fieldPath || !fieldPath[0])
|
||||||
{
|
{
|
||||||
@@ -92,6 +120,7 @@ bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldP
|
|||||||
|
|
||||||
offset = 0;
|
offset = 0;
|
||||||
size = 0;
|
size = 0;
|
||||||
|
addressSteps.clear();
|
||||||
std::string currentClass = className;
|
std::string currentClass = className;
|
||||||
for (size_t i = 0; i < parts.size(); ++i)
|
for (size_t i = 0; i < parts.size(); ++i)
|
||||||
{
|
{
|
||||||
@@ -102,8 +131,11 @@ bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldP
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += field.offset;
|
|
||||||
size = field.size;
|
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 (i + 1 < parts.size())
|
||||||
{
|
{
|
||||||
if (field.typeName.empty())
|
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";
|
error = "intermediate field has no schema type name";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
currentClass = field.typeName;
|
currentClass = NormalizeSchemaClassName(field.typeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!addressSteps.empty())
|
||||||
|
offset = addressSteps.front().offset;
|
||||||
|
|
||||||
return true;
|
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()
|
void OverrideManager::RebuildIndex()
|
||||||
{
|
{
|
||||||
m_rulesByEntity.clear();
|
m_rulesByEntity.clear();
|
||||||
@@ -139,6 +277,17 @@ bool OverrideManager::MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offs
|
|||||||
return true;
|
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)
|
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)
|
if (recipientSlot < -1 || recipientSlot >= 64)
|
||||||
@@ -163,7 +312,7 @@ int OverrideManager::AddRuleBytes(int recipientSlot, int entityIndex, const char
|
|||||||
rule.entityIndex = entityIndex;
|
rule.entityIndex = entityIndex;
|
||||||
rule.className = className ? className : "";
|
rule.className = className ? className : "";
|
||||||
rule.fieldPath = fieldPath ? fieldPath : "";
|
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;
|
return 0;
|
||||||
if (rule.size > 0 && valueSize > rule.size)
|
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);
|
auto* bytes = static_cast<const uint8_t*>(value);
|
||||||
rule.value.assign(bytes, bytes + valueSize);
|
rule.value.assign(bytes, bytes + valueSize);
|
||||||
m_rules.push_back(rule);
|
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();
|
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;
|
return rule.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +399,7 @@ bool OverrideManager::RemoveRule(int id)
|
|||||||
if (rule.id != id)
|
if (rule.id != id)
|
||||||
continue;
|
continue;
|
||||||
rule.enabled = false;
|
rule.enabled = false;
|
||||||
MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset);
|
MarkEntityFullDirty(rule.entityIndex);
|
||||||
RebuildIndex();
|
RebuildIndex();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -197,7 +410,8 @@ bool OverrideManager::MarkEntityFieldDirty(int entityIndex, const char* classNam
|
|||||||
{
|
{
|
||||||
int32_t offset = 0;
|
int32_t offset = 0;
|
||||||
int32_t size = 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 false;
|
||||||
return MarkEntityFieldDirtyByOffset(entityIndex, offset);
|
return MarkEntityFieldDirtyByOffset(entityIndex, offset);
|
||||||
}
|
}
|
||||||
@@ -212,19 +426,69 @@ std::vector<AppliedOverride> OverrideManager::ApplyForPackedEntity(CPlayerSlot r
|
|||||||
if (found == m_rulesByEntity.end())
|
if (found == m_rulesByEntity.end())
|
||||||
return applied;
|
return applied;
|
||||||
|
|
||||||
|
int effectiveRecipient = recipient.Get();
|
||||||
|
int uniqueRuleRecipient = -2;
|
||||||
for (size_t ruleIndex : found->second)
|
for (size_t ruleIndex : found->second)
|
||||||
{
|
{
|
||||||
const auto& rule = m_rules[ruleIndex];
|
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;
|
continue;
|
||||||
|
|
||||||
auto* address = reinterpret_cast<uint8_t*>(entityData) + rule.offset;
|
if (rule.recipientSlot != -1 && rule.recipientSlot != effectiveRecipient)
|
||||||
AppliedOverride saved {};
|
{
|
||||||
saved.address = address;
|
if (!rule.recipientMismatchLogged)
|
||||||
saved.original.resize(rule.value.size());
|
{
|
||||||
memcpy(saved.original.data(), address, saved.original.size());
|
SPMessage("rule %d entity=%d matched packed entity but skipped recipient rule=%d current=%d field=%s::%s\n",
|
||||||
memcpy(address, rule.value.data(), rule.value.size());
|
rule.id,
|
||||||
applied.push_back(saved);
|
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;
|
return applied;
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ struct AppliedOverride
|
|||||||
std::vector<uint8_t> original;
|
std::vector<uint8_t> original;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class OverrideRuleKind
|
||||||
|
{
|
||||||
|
Bytes,
|
||||||
|
VectorFirstFromField,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FieldAddressStep
|
||||||
|
{
|
||||||
|
int32_t offset {};
|
||||||
|
bool dereference {};
|
||||||
|
};
|
||||||
|
|
||||||
struct CompiledOverrideRule
|
struct CompiledOverrideRule
|
||||||
{
|
{
|
||||||
int id {};
|
int id {};
|
||||||
@@ -20,10 +32,17 @@ struct CompiledOverrideRule
|
|||||||
int entityIndex {-1};
|
int entityIndex {-1};
|
||||||
std::string className;
|
std::string className;
|
||||||
std::string fieldPath;
|
std::string fieldPath;
|
||||||
|
std::string sourceFieldPath;
|
||||||
int32_t offset {};
|
int32_t offset {};
|
||||||
int32_t size {};
|
int32_t size {};
|
||||||
|
int32_t sourceSize {};
|
||||||
|
std::vector<FieldAddressStep> addressSteps;
|
||||||
|
std::vector<FieldAddressStep> sourceAddressSteps;
|
||||||
std::vector<uint8_t> value;
|
std::vector<uint8_t> value;
|
||||||
|
OverrideRuleKind kind {OverrideRuleKind::Bytes};
|
||||||
bool enabled {true};
|
bool enabled {true};
|
||||||
|
mutable bool debugLogged {false};
|
||||||
|
mutable bool recipientMismatchLogged {false};
|
||||||
};
|
};
|
||||||
|
|
||||||
class OverrideManager
|
class OverrideManager
|
||||||
@@ -32,18 +51,21 @@ public:
|
|||||||
void Clear();
|
void Clear();
|
||||||
void Compact();
|
void Compact();
|
||||||
int AddRuleBytes(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const void* value, int valueSize, std::string& error);
|
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 RemoveRule(int id);
|
||||||
bool MarkEntityFieldDirty(int entityIndex, const char* className, const char* fieldPath, std::string& error);
|
bool MarkEntityFieldDirty(int entityIndex, const char* className, const char* fieldPath, std::string& error);
|
||||||
bool Empty() const { return m_rules.empty(); }
|
bool Empty() const { return m_rules.empty(); }
|
||||||
bool HasPackedOverrides() const { return !m_rulesByEntity.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);
|
std::vector<AppliedOverride> ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData);
|
||||||
void Restore(const std::vector<AppliedOverride>& applied);
|
void Restore(const std::vector<AppliedOverride>& applied);
|
||||||
|
|
||||||
private:
|
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();
|
void RebuildIndex();
|
||||||
bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset);
|
bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset);
|
||||||
|
bool MarkEntityFullDirty(int entityIndex);
|
||||||
|
|
||||||
std::vector<CompiledOverrideRule> m_rules;
|
std::vector<CompiledOverrideRule> m_rules;
|
||||||
std::unordered_map<int, std::vector<size_t>> m_rulesByEntity;
|
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;
|
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)
|
extern "C" __attribute__((visibility("default"))) bool SendProxy_RemoveOverride(int ruleId)
|
||||||
{
|
{
|
||||||
return g_overrides.RemoveRule(ruleId);
|
return g_overrides.RemoveRule(ruleId);
|
||||||
@@ -96,6 +111,16 @@ extern "C" __attribute__((visibility("default"))) bool SendProxy_MarkDirty(int e
|
|||||||
return true;
|
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);
|
PLUGIN_EXPOSE(SendProxyPlugin, g_SendProxy);
|
||||||
|
|
||||||
bool SendProxyPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late)
|
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 "schemasystem/schemasystem.h"
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
static ISchemaSystem* g_schema = nullptr;
|
static ISchemaSystem* g_schema = nullptr;
|
||||||
static std::map<std::string, SchemaField> g_cache;
|
static std::map<std::string, SchemaField> g_cache;
|
||||||
@@ -18,6 +19,81 @@ static bool IsNetworked(const SchemaClassFieldData_t& field)
|
|||||||
return false;
|
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)
|
void schema::Init(ISchemaSystem* system)
|
||||||
{
|
{
|
||||||
g_schema = system;
|
g_schema = system;
|
||||||
@@ -48,22 +124,9 @@ SchemaField schema::FindField(const char* className, const char* fieldName)
|
|||||||
return result;
|
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;
|
g_cache[key] = result;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -90,18 +153,30 @@ void schema::DumpFields(const char* className, const char* contains)
|
|||||||
}
|
}
|
||||||
|
|
||||||
int printed = 0;
|
int printed = 0;
|
||||||
for (int i = 0; i < info->m_nFieldCount; ++i)
|
std::set<const SchemaClassInfoData_t*> visited;
|
||||||
{
|
DumpFieldsRecursive(className, info, contains, 0, printed, visited);
|
||||||
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>");
|
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);
|
void Init(ISchemaSystem* system);
|
||||||
SchemaField FindField(const char* className, const char* fieldName);
|
SchemaField FindField(const char* className, const char* fieldName);
|
||||||
void DumpFields(const char* className, const char* contains);
|
void DumpFields(const char* className, const char* contains);
|
||||||
|
void DumpEnum(const char* enumName);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user