From 9a23169b6b7754d36502b19515a63f65543e0e42 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Mon, 10 Aug 2026 16:59:45 +0200 Subject: [PATCH] working weapon concealment --- README.md | 33 +- .../SendProxyConcealedCarry.csproj | 13 + .../SendProxyConcealedCarryPlugin.cs | 248 +++++++++++++++ .../SendProxyHealth23.csproj | 13 + .../SendProxyHealth23Plugin.cs | 91 ++++++ .../CounterStrikeSharp/SendProxyNative.cs | 45 +++ .../SendProxyWeaponVisibilityPlugin.cs | 26 +- src/detour.cpp | 49 ++- src/overrides.cpp | 294 +++++++++++++++++- src/overrides.h | 24 +- src/plugin.cpp | 25 ++ src/schema.cpp | 131 ++++++-- src/schema.h | 1 + 13 files changed, 927 insertions(+), 66 deletions(-) create mode 100644 examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarry.csproj create mode 100644 examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarryPlugin.cs create mode 100644 examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23.csproj create mode 100644 examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23Plugin.cs diff --git a/README.md b/README.md index 77e5211..0615622 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,10 @@ The C# wrapper is in: examples/CounterStrikeSharp/SendProxyNative.cs ``` -It loads the native addon with `NativeLibrary.Load` and exposes byte-oriented and typed helpers. In a CSS plugin, resolve the native library from `Server.GameDirectory`: +It loads the native addon with `NativeLibrary.Load` and exposes byte-oriented and typed helpers. In a CSS plugin, construct it from `Server.GameDirectory`: ```csharp -string libraryPath = Path.Combine(Server.GameDirectory, "addons", "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so"); -var sendProxy = new SendProxyNative(libraryPath); +var sendProxy = SendProxyNative.FromGameDirectory(Server.GameDirectory); int ruleId = sendProxy.SetInt32( recipientSlot: viewer.Slot, @@ -75,9 +74,17 @@ sendProxy.RemoveOverride(ruleId); Use `SetBytes` for custom/fixed-layout fields and typed helpers such as `SetInt32`, `SetUInt32`, `SetFloat`, `SetBool`, and `SetColor` for common values. -## Example CSS Plugin +## Example CSS Plugins -Example source: +Health spoof smoke test: + +```text +examples/CounterStrikeSharp/SendProxyHealth23/ +``` + +It spoofs every player pawn's `CBaseEntity::m_iHealth` to `23` for all recipients without changing real health. + +Weapon visibility experiment: ```text examples/CounterStrikeSharp/SendProxyWeaponVisibility/ @@ -90,6 +97,20 @@ It demonstrates: - Coloring dropped AK-47s red for Terrorist viewers. - Coloring dropped M4A1-S blue for CT viewers. +Concealed-carry handle spoof: + +```text +examples/CounterStrikeSharp/SendProxyConcealedCarry/ +``` + +It hides owned primary weapons, secondary weapons, and grenades from every recipient except the owning player by spoofing weapon-related handles to `0xffffffff`: + +- `CBasePlayerPawn::m_pWeaponServices.m_hActiveWeapon` +- `CBasePlayerPawn::m_pWeaponServices.m_hLastWeapon` +- `CBaseEntity::m_hOwnerEntity` on carried weapon entities + +It intentionally does not set `EF_NODRAW` and does not remove or drop the real weapons. + This sample periodically reconciles desired rules and diffs them against active rule IDs. For production, prefer event-driven updates on player connect/disconnect/team changes/spawn/death and weapon pickup/drop/create/delete. ## Building the Metamod Addon @@ -159,6 +180,8 @@ Install the built CSS plugin under: game/csgo/addons/counterstrikesharp/plugins/SendProxyWeaponVisibility/ ``` +Use the matching example directory name when building/installing another sample, for example `SendProxyHealth23` or `SendProxyConcealedCarry`. + Make sure `SendProxyNative.cs` is included in your CSS project or copied into your plugin source. The example plugin expects the Metamod addon at: diff --git a/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarry.csproj b/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarry.csproj new file mode 100644 index 0000000..006a797 --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarry.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + true + + + + + + + diff --git a/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarryPlugin.cs b/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarryPlugin.cs new file mode 100644 index 0000000..bc4e18a --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyConcealedCarry/SendProxyConcealedCarryPlugin.cs @@ -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 _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(_ => Server.NextFrame(SyncRules)); + RegisterListener(_ => 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 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 BuildDesiredRules( + IReadOnlyCollection viewers, + IReadOnlyDictionary targetStatesByPawnIndex) + { + var desired = new Dictionary(); + 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(); + } + } + + 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 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 EnumerateConcealableCarriedWeapons(IReadOnlyDictionary targetStatesByPawnIndex) + { + foreach (var entity in Utilities.GetAllEntities()) + { + if (!entity.IsValid || !entity.DesignerName.StartsWith("weapon_", StringComparison.Ordinal)) + continue; + + CBasePlayerWeapon weapon = entity.As(); + 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); +} diff --git a/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23.csproj b/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23.csproj new file mode 100644 index 0000000..006a797 --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + true + + + + + + + diff --git a/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23Plugin.cs b/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23Plugin.cs new file mode 100644 index 0000000..d2161f4 --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyHealth23/SendProxyHealth23Plugin.cs @@ -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 _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(); + } +} diff --git a/examples/CounterStrikeSharp/SendProxyNative.cs b/examples/CounterStrikeSharp/SendProxyNative.cs index 2c85808..2627bc6 100644 --- a/examples/CounterStrikeSharp/SendProxyNative.cs +++ b/examples/CounterStrikeSharp/SendProxyNative.cs @@ -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("SendProxy_SetOverride"); + _setVectorFirstFromField = GetExport("SendProxy_SetVectorFirstFromField"); _removeOverride = GetExport("SendProxy_RemoveOverride"); _clearOverrides = GetExport("SendProxy_ClearOverrides"); _markDirty = GetExport("SendProxy_MarkDirty"); + _dumpSchema = GetExport("SendProxy_DumpSchema"); + _dumpEnum = GetExport("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 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); } diff --git a/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs index a1996ad..8f2a687 100644 --- a/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs +++ b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs @@ -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(_ => Server.NextFrame(SyncRules)); RegisterListener(_ => 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(); - var viewers = Utilities.GetPlayers() - .Where(player => player.IsValid && !player.IsBot && !player.IsHLTV) - .ToArray(); + CCSPlayerController[] viewers; + try + { + viewers = Utilities.GetPlayers() + .Where(player => player.IsValid && !player.IsBot && !player.IsHLTV) + .ToArray(); + } + catch (NativeException) + { + return; + } AddHealthRules(desired, viewers); @@ -211,14 +219,6 @@ public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin _activeRules.Clear(); } - private string GetSendProxyLibraryPath() - { - string libraryPath = Path.Combine(Server.GameDirectory, "addons", "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so"); - if (!File.Exists(libraryPath)) - throw new FileNotFoundException("SendProxy Metamod addon was not found. Install it under game/csgo/addons/sendproxy.", libraryPath); - return libraryPath; - } - private readonly record struct RuleKey(int RecipientSlot, int EntityIndex, string ClassName, string FieldPath); private readonly record struct RuleValue(byte[] Bytes) diff --git a/src/detour.cpp b/src/detour.cpp index cfac8ca..7e4f2d0 100644 --- a/src/detour.cpp +++ b/src/detour.cpp @@ -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(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(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); diff --git a/src/overrides.cpp b/src/overrides.cpp index a8d5141..747d283 100644 --- a/src/overrides.cpp +++ b/src/overrides.cpp @@ -53,6 +53,34 @@ static std::vector 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& 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(entityData); + for (const auto& step : rule.addressSteps) + { + address += step.offset; + if (!step.dereference) + continue; + + address = *reinterpret_cast(address); + if (!address) + return nullptr; + } + return address; +} + +static uint8_t* ResolveAddressSteps(const std::vector& steps, void* entityData) +{ + auto* address = static_cast(entityData); + for (const auto& step : steps) + { + address += step.offset; + if (!step.dereference) + continue; + + address = *reinterpret_cast(address); + if (!address) + return nullptr; + } + return address; +} + +static void SaveAndWrite(std::vector& 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& 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& applied) +{ + auto* vectorAddress = ResolveAddressSteps(rule.addressSteps, entityData); + auto* sourceAddress = ResolveAddressSteps(rule.sourceAddressSteps, entityData); + if (!vectorAddress || !sourceAddress) + return; + + const uint32_t sourceHandle = *reinterpret_cast(sourceAddress); + constexpr uint32_t invalidHandle = 0xffffffff; + const int32_t count = *reinterpret_cast(vectorAddress); + auto* elements = *reinterpret_cast(vectorAddress + 8); + if (!rule.debugLogged) + { + uint32_t h0 = elements && count > 0 ? *reinterpret_cast(elements) : invalidHandle; + uint32_t h1 = elements && count > 1 ? *reinterpret_cast(elements + 4) : invalidHandle; + uint32_t h2 = elements && count > 2 ? *reinterpret_cast(elements + 8) : invalidHandle; + const auto* raw = reinterpret_cast(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(&*reinterpret_cast(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(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 addressSteps; + if (!ResolveFieldPath(className, fieldPath, offset, size, addressSteps, error)) return false; return MarkEntityFieldDirtyByOffset(entityIndex, offset); } @@ -212,19 +426,69 @@ std::vector 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(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; diff --git a/src/overrides.h b/src/overrides.h index 07eac99..665be16 100644 --- a/src/overrides.h +++ b/src/overrides.h @@ -13,6 +13,18 @@ struct AppliedOverride std::vector 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 addressSteps; + std::vector sourceAddressSteps; std::vector 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 ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData); void Restore(const std::vector& 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& addressSteps, std::string& error) const; void RebuildIndex(); bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset); + bool MarkEntityFullDirty(int entityIndex); std::vector m_rules; std::unordered_map> m_rulesByEntity; diff --git a/src/plugin.cpp b/src/plugin.cpp index b12144d..0ef10b0 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -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) diff --git a/src/schema.cpp b/src/schema.cpp index bfb74d6..5dbde8a 100644 --- a/src/schema.cpp +++ b/src/schema.cpp @@ -4,6 +4,7 @@ #include "schemasystem/schemasystem.h" #include +#include static ISchemaSystem* g_schema = nullptr; static std::map 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(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& 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& 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 : "", + field.m_pszName, + static_cast(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 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 visited; + DumpFieldsRecursive(className, info, contains, 0, printed, visited); SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : ""); } + +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 : "", static_cast(enumerator.m_nValue)); + } +} diff --git a/src/schema.h b/src/schema.h index 5c652ea..55c677f 100644 --- a/src/schema.h +++ b/src/schema.h @@ -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); }