From 1ecb7f777e08b259e0ccc22097149a75a2f24631 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Mon, 10 Aug 2026 14:21:07 +0200 Subject: [PATCH] initial commit -- v1 code --- .gitignore | 2 + AMBuildScript | 110 +++++ PackageScript | 23 + README.md | 178 +++++++ configs/sendproxy.cfg | 5 + configure.py | 18 + .../CounterStrikeSharp/SendProxyNative.cs | 102 ++++ .../SendProxyWeaponVisibility.csproj | 13 + .../SendProxyWeaponVisibilityPlugin.cs | 246 ++++++++++ gamedata/sendproxy.games.txt | 43 ++ hl2sdk-manifests/SdkHelpers.ambuild | 292 +++++++++++ hl2sdk-manifests/manifests/cs2.json | 57 +++ src/common.h | 12 + src/detour.cpp | 69 +++ src/detour.h | 9 + src/gameconfig.cpp | 137 ++++++ src/gameconfig.h | 35 ++ src/module.cpp | 119 +++++ src/module.h | 51 ++ src/overrides.cpp | 463 ++++++++++++++++++ src/overrides.h | 81 +++ src/plugin.cpp | 264 ++++++++++ src/plugin.h | 29 ++ src/schema.cpp | 94 ++++ src/schema.h | 20 + 25 files changed, 2472 insertions(+) create mode 100644 .gitignore create mode 100644 AMBuildScript create mode 100644 PackageScript create mode 100644 README.md create mode 100644 configs/sendproxy.cfg create mode 100644 configure.py create mode 100644 examples/CounterStrikeSharp/SendProxyNative.cs create mode 100644 examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibility.csproj create mode 100644 examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs create mode 100644 gamedata/sendproxy.games.txt create mode 100644 hl2sdk-manifests/SdkHelpers.ambuild create mode 100644 hl2sdk-manifests/manifests/cs2.json create mode 100644 src/common.h create mode 100644 src/detour.cpp create mode 100644 src/detour.h create mode 100644 src/gameconfig.cpp create mode 100644 src/gameconfig.h create mode 100644 src/module.cpp create mode 100644 src/module.h create mode 100644 src/overrides.cpp create mode 100644 src/overrides.h create mode 100644 src/plugin.cpp create mode 100644 src/plugin.h create mode 100644 src/schema.cpp create mode 100644 src/schema.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..495a75e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build/ + diff --git a/AMBuildScript b/AMBuildScript new file mode 100644 index 0000000..65867b3 --- /dev/null +++ b/AMBuildScript @@ -0,0 +1,110 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: +import os + +def ResolveEnvPath(env, folder): + if env in os.environ and os.path.isdir(os.environ[env]): + return os.environ[env] + head = os.getcwd() + oldhead = None + while head != oldhead: + path = os.path.join(head, folder) + if os.path.isdir(path): + return path + oldhead = head + head = os.path.dirname(head) + return None + +mms_root = os.path.abspath(builder.options.mms_path) +SdkHelpers = builder.Eval(os.path.join(builder.options.hl2sdk_manifests, 'SdkHelpers.ambuild'), { + 'Project': 'metamod' +}) + +class SendProxyConfig(object): + def __init__(self): + self.plugin_name = 'sendproxy' + self.plugin_alias = 'sendproxy' + self.binaries = [] + self.all_targets = [] + for arch in builder.options.targets.split(','): + self.all_targets.append(builder.DetectCxx(target_arch=arch)) + + def findSdkPath(self, sdk_name): + path = os.path.join(builder.options.hl2sdk_root, 'hl2sdk-{}'.format(sdk_name)) + if os.path.exists(path): + return path + return ResolveEnvPath('HL2SDK{}'.format(sdk_name.upper()), 'hl2sdk-{}'.format(sdk_name)) + + def configure(self): + SdkHelpers.find_sdk_path = self.findSdkPath + SdkHelpers.findSdks(builder, self.all_targets, [s for s in builder.options.sdks.split(',') if s]) + if len(SdkHelpers.sdks) != 1: + raise Exception('Build exactly one SDK, expected cs2.') + + for sdk_target in SdkHelpers.sdk_targets: + self.configureTarget(sdk_target.cxx, sdk_target.sdk) + + def configureCommon(self, cxx): + if cxx.behavior == 'gcc': + cxx.defines += [ + 'stricmp=strcasecmp', '_stricmp=strcasecmp', '_snprintf=snprintf', + '_vsnprintf=vsnprintf', 'HAVE_STDINT_H', 'GNUC', + ] + cxx.cflags += ['-pipe', '-fno-strict-aliasing', '-Wall', '-Wno-unused', '-Wno-switch', '-msse', '-fPIC', '-fno-omit-frame-pointer'] + cxx.cxxflags += ['-std=c++20', '-fno-exceptions', '-fno-threadsafe-statics', '-Wno-non-virtual-dtor', '-Wno-overloaded-virtual', '-Wno-register', '-Wno-invalid-offsetof'] + cxx.cflags += ['-fvisibility=hidden'] + cxx.cxxflags += ['-fvisibility-inlines-hidden'] + if cxx.family == 'gcc': + cxx.cflags += ['-mfpmath=sse'] + if builder.options.opt == '1': + cxx.defines += ['NDEBUG'] + cxx.cflags += ['-O2'] + if builder.options.debug == '1': + cxx.defines += ['DEBUG', '_DEBUG'] + cxx.cflags += ['-g3'] + + if cxx.target.platform == 'linux': + cxx.defines += ['LINUX', '_LINUX', 'POSIX', '_FILE_OFFSET_BITS=64', 'META_IS_SOURCE2'] + cxx.linkflags += ['-static-libgcc', '-static-libstdc++'] + + def configureTarget(self, cxx, sdk): + self.configureCommon(cxx) + binary = cxx.Library(self.plugin_name) + cxx = binary.compiler + + cxx.cxxincludes += [ + os.path.join(builder.sourcePath, 'src'), + os.path.join(mms_root, 'core'), + os.path.join(mms_root, 'core', 'sourcehook'), + os.path.join(sdk['path'], 'public'), + os.path.join(sdk['path'], 'public', 'tier0'), + os.path.join(sdk['path'], 'public', 'tier1'), + os.path.join(sdk['path'], 'public', 'entity2'), + os.path.join('/home/csgo/am/STFixes-metamod/vendor/funchook/include'), + os.path.join('/home/csgo/am/STFixes-metamod/protobuf/generated'), + ] + for manifest in SdkHelpers.sdk_manifests: + cxx.defines += ['SE_{}={}'.format(manifest['define'], manifest['code'])] + + SdkHelpers.configureCxx(builder, binary, sdk) + binary.sources += [ + os.path.join(sdk['path'], 'public', 'tier0', 'memoverride.cpp'), + os.path.join(sdk['path'], 'tier1', 'convar.cpp'), + 'src/plugin.cpp', + 'src/module.cpp', + 'src/gameconfig.cpp', + 'src/schema.cpp', + 'src/overrides.cpp', + 'src/detour.cpp', + ] + binary.compiler.postlink += [ + '/home/csgo/am/STFixes-metamod/vendor/funchook/lib/Release/libfunchook.a', + '/home/csgo/am/STFixes-metamod/vendor/funchook/lib/Release/libdistorm.a', + ] + nodes = builder.Add(binary) + self.binaries += [nodes] + +SendProxy = SendProxyConfig() +SendProxy.configure() + +BuildScripts = ['PackageScript'] +builder.Build(BuildScripts, {'MMSPlugin': SendProxy}) diff --git a/PackageScript b/PackageScript new file mode 100644 index 0000000..2680554 --- /dev/null +++ b/PackageScript @@ -0,0 +1,23 @@ +# vim: set ts=2 sw=2 tw=99 et ft=python: +import os + +builder.SetBuildFolder('package') + +metamod_folder = builder.AddFolder(os.path.join('addons', 'metamod')) +bin_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'bin', 'linuxsteamrt64')) +gamedata_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'gamedata')) +config_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'configs')) + +for task in MMSPlugin.binaries: + vdf_path = os.path.join(builder.buildPath, MMSPlugin.plugin_name + '.vdf') + with open(vdf_path, 'w') as fp: + fp.write('"Metamod Plugin"\n{\n') + fp.write('\t"alias"\t"{}"\n'.format(MMSPlugin.plugin_alias)) + fp.write('\t"file"\t"addons/{}/bin/linuxsteamrt64/{}"\n'.format(MMSPlugin.plugin_name, MMSPlugin.plugin_name)) + fp.write('}\n') + builder.AddCopy(task.binary, bin_folder) + builder.AddCopy(vdf_path, metamod_folder) + +builder.AddCopy(os.path.join('gamedata', 'sendproxy.games.txt'), gamedata_folder) +builder.AddCopy(os.path.join('configs', 'sendproxy.cfg'), config_folder) + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9239961 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# SendProxy for CS2 + +SendProxy is an experimental Counter-Strike 2 Metamod addon plus CounterStrikeSharp interop layer for per-recipient networked field spoofing. + +The goal is similar to Source 1 `SendProxy`: let plugin code change the value serialized to one viewer without changing the real server entity state. For example, player A can see an entity field as one value while player B sees another value. + +## Current Architecture + +The Metamod addon is intentionally small: + +- Hooks `CServerSideClient::SendSnapshot` to identify the recipient slot for the current snapshot. +- Hooks `CNetworkGameServer::PackEntity`. +- Looks up exact rules by packed entity index. +- Temporarily writes raw bytes to the entity field. +- Calls the original entity packer. +- Restores the original bytes immediately. + +There is no value parser or high-level rule language in Metamod. CounterStrikeSharp owns typed values, selectors, policy, and rule lifecycle. + +Native rule shape: + +```text +recipient slot + entity index + schema field path = raw bytes +``` + +The native side resolves `className + fieldPath` to a schema offset when a rule is added. The pack hot path does not parse strings, walk entities, or query schema. + +## Native ABI + +The addon exports a small C ABI from `sendproxy.so`: + +```cpp +extern "C" int SendProxy_SetOverride( + int recipientSlot, + int entityIndex, + const char* className, + const char* fieldPath, + const void* value, + int valueSize); + +extern "C" bool SendProxy_RemoveOverride(int ruleId); +extern "C" void SendProxy_ClearOverrides(); +extern "C" bool SendProxy_MarkDirty(int entityIndex, const char* className, const char* fieldPath); +``` + +`recipientSlot` may be `-1` for all recipients, or `0..63` for one viewer. + +`SendProxy_SetOverride` returns a rule ID. Keep this ID in managed code and pass it to `SendProxy_RemoveOverride` when the spoof is no longer wanted. + +Disabled native rules are compacted on every map start. + +## CounterStrikeSharp Wrapper + +The C# wrapper is in: + +```text +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 the normal addons layout: + +```csharp +string addonsDirectory = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "..")); +string libraryPath = Path.Combine(addonsDirectory, "sendproxy", "bin", "linuxsteamrt64", "sendproxy.so"); +var sendProxy = new SendProxyNative(libraryPath); + +int ruleId = sendProxy.SetInt32( + recipientSlot: viewer.Slot, + entityIndex: (int)targetPawn.Index, + className: "CBaseEntity", + fieldPath: "m_iHealth", + value: 42); + +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 source: + +```text +examples/CounterStrikeSharp/SendProxyWeaponVisibility/ +``` + +It demonstrates: + +- Spoofing other players' pawn health to `42` per viewer while leaving the viewer's own health real. +- Hiding weapon entities owned by other players with spoofed `CBaseEntity::m_fEffects`. +- Coloring dropped AK-47s red for Terrorist viewers. +- Coloring dropped M4A1-S blue for CT viewers. + +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 + +Prerequisites: + +- AMBuild 2.2+ +- Metamod:Source +- HL2SDK for CS2 +- `funchook` and generated protobuf headers as referenced by `AMBuildScript` + +The defaults in `configure.py` match this workspace: + +```text +--hl2sdk-root /home/csgo/am/hl2sdk-root +--mms_path /home/csgo/am/metamod-source +``` + +Configure and build: + +```bash +mkdir -p build +cd build +python3 ../configure.py --enable-debug +ambuild +``` + +The packaged addon is written to: + +```text +build/package/addons/sendproxy/ +build/package/addons/metamod/sendproxy.vdf +``` + +## Installing + +Copy the package into the CS2 server's `game/csgo/addons` tree: + +```bash +cp -a build/package/addons/sendproxy /home/csgo/serverfiles/game/csgo/addons/ +cp -a build/package/addons/metamod/sendproxy.vdf /home/csgo/serverfiles/game/csgo/addons/metamod/ +``` + +In this workspace, `/home/csgo/codex/sendproxy/serverfiles` is a bind-mounted target equivalent to `/home/csgo/serverfiles`. + +Verify the native ABI exports: + +```bash +nm -D /home/csgo/serverfiles/game/csgo/addons/sendproxy/bin/linuxsteamrt64/sendproxy.so \ + | rg 'SendProxy_(SetOverride|RemoveOverride|ClearOverrides|MarkDirty)' +``` + +## Building the CSS Example + +This container currently has the CounterStrikeSharp .NET runtime but not a .NET SDK, so the C# sample cannot be compiled here as-is. + +On a machine with the .NET SDK: + +```bash +cd examples/CounterStrikeSharp/SendProxyWeaponVisibility +dotnet build -c Release +``` + +Install the built CSS plugin under: + +```text +game/csgo/addons/counterstrikesharp/plugins/SendProxyWeaponVisibility/ +``` + +Make sure `SendProxyNative.cs` is included in your CSS project or copied into your plugin source. + +The example plugin expects the Metamod addon at: + +```text +game/csgo/addons/sendproxy/bin/linuxsteamrt64/sendproxy.so +``` + +## Notes and Limits + +- Only fields serialized through the hooked `PackEntity` path are affected. +- The real server entity state is restored immediately after packing. +- The value byte layout must match the server schema field layout. +- The native addon validates that `valueSize` is not larger than the resolved schema field size when schema size is available. +- Rule updates should be event-driven where possible. Avoid removing and recreating many rules every tick. +- If per-entity rule lists become large, the next native optimization is indexing by `(entityIndex, recipientSlot)` instead of only `entityIndex`. diff --git a/configs/sendproxy.cfg b/configs/sendproxy.cfg new file mode 100644 index 0000000..4dd96c5 --- /dev/null +++ b/configs/sendproxy.cfg @@ -0,0 +1,5 @@ +// Examples: +// sp_set all weapon_ak47 CBasePlayerWeapon m_iClip1 int32 1 +// sp_weapon_test weapon_ak47 7 +sp_enable 1 +sp_trace 0 diff --git a/configure.py b/configure.py new file mode 100644 index 0000000..0cfa142 --- /dev/null +++ b/configure.py @@ -0,0 +1,18 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et: +import sys +try: + from ambuild2 import run +except: + sys.stderr.write('AMBuild 2.2+ is required.\n') + sys.exit(1) + +parser = run.BuildParser(sourcePath=sys.path[0], api='2.2') +parser.options.add_argument('--hl2sdk-root', type=str, dest='hl2sdk_root', default='/home/csgo/am/hl2sdk-root') +parser.options.add_argument('--hl2sdk-manifests', type=str, dest='hl2sdk_manifests', default='hl2sdk-manifests/') +parser.options.add_argument('--mms_path', type=str, dest='mms_path', default='/home/csgo/am/metamod-source') +parser.options.add_argument('--enable-debug', action='store_const', const='1', dest='debug') +parser.options.add_argument('--enable-optimize', action='store_const', const='1', dest='opt') +parser.options.add_argument('-s', '--sdks', default='cs2', dest='sdks') +parser.options.add_argument('--targets', type=str, dest='targets', default='x86_64') +parser.Configure() + diff --git a/examples/CounterStrikeSharp/SendProxyNative.cs b/examples/CounterStrikeSharp/SendProxyNative.cs new file mode 100644 index 0000000..2c85808 --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyNative.cs @@ -0,0 +1,102 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.InteropServices; + +namespace SendProxyInterop; + +public sealed class SendProxyNative : IDisposable +{ + private readonly IntPtr _library; + private readonly SetOverrideDelegate _setOverride; + private readonly RemoveOverrideDelegate _removeOverride; + private readonly ClearOverridesDelegate _clearOverrides; + private readonly MarkDirtyDelegate _markDirty; + + public SendProxyNative(string libraryPath) + { + _library = NativeLibrary.Load(libraryPath); + _setOverride = GetExport("SendProxy_SetOverride"); + _removeOverride = GetExport("SendProxy_RemoveOverride"); + _clearOverrides = GetExport("SendProxy_ClearOverrides"); + _markDirty = GetExport("SendProxy_MarkDirty"); + } + + public int SetBytes(int recipientSlot, int entityIndex, string className, string fieldPath, ReadOnlySpan value) + { + byte[] bytes = value.ToArray(); + return _setOverride(recipientSlot, entityIndex, className, fieldPath, bytes, bytes.Length); + } + + public int SetBool(int recipientSlot, int entityIndex, string className, string fieldPath, bool value) + { + Span bytes = stackalloc byte[1]; + bytes[0] = value ? (byte)1 : (byte)0; + return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes); + } + + public int SetUInt32(int recipientSlot, int entityIndex, string className, string fieldPath, uint value) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32LittleEndian(bytes, value); + return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes); + } + + public int SetInt32(int recipientSlot, int entityIndex, string className, string fieldPath, int value) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(bytes, value); + return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes); + } + + public int SetFloat(int recipientSlot, int entityIndex, string className, string fieldPath, float value) + { + Span bytes = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(bytes, BitConverter.SingleToInt32Bits(value)); + return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes); + } + + public int SetColor(int recipientSlot, int entityIndex, string className, string fieldPath, byte r, byte g, byte b, byte a) + { + Span bytes = stackalloc byte[] { r, g, b, a }; + return SetBytes(recipientSlot, entityIndex, className, fieldPath, bytes); + } + + public bool RemoveOverride(int ruleId) + { + return _removeOverride(ruleId); + } + + public void ClearOverrides() + { + _clearOverrides(); + } + + public bool MarkDirty(int entityIndex, string className, string fieldPath) + { + return _markDirty(entityIndex, className, fieldPath); + } + + public void Dispose() + { + NativeLibrary.Free(_library); + } + + private T GetExport(string name) where T : Delegate + { + return Marshal.GetDelegateForFunctionPointer(NativeLibrary.GetExport(_library, name)); + } + + [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)] + [return: MarshalAs(UnmanagedType.I1)] + private delegate bool RemoveOverrideDelegate(int ruleId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void ClearOverridesDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + [return: MarshalAs(UnmanagedType.I1)] + private delegate bool MarkDirtyDelegate(int entityIndex, string className, string fieldPath); +} diff --git a/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibility.csproj b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibility.csproj new file mode 100644 index 0000000..006a797 --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibility.csproj @@ -0,0 +1,13 @@ + + + net10.0 + enable + enable + true + + + + + + + diff --git a/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs new file mode 100644 index 0000000..c667bfc --- /dev/null +++ b/examples/CounterStrikeSharp/SendProxyWeaponVisibility/SendProxyWeaponVisibilityPlugin.cs @@ -0,0 +1,246 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Timers; +using CounterStrikeSharp.API.Modules.Utils; +using SendProxyInterop; +using Timer = CounterStrikeSharp.API.Modules.Timers.Timer; + +namespace SendProxyWeaponVisibility; + +public sealed class SendProxyWeaponVisibilityPlugin : BasePlugin +{ + private const int TeamT = 2; + private const int TeamCt = 3; + private const uint EfNoDraw = 0x20; + private const int SpoofedHealth = 42; + + private readonly Dictionary _activeRules = new(); + private SendProxyNative? _sendProxy; + private Timer? _syncTimer; + + public override string ModuleName => "SendProxy Weapon Visibility"; + public override string ModuleVersion => "0.1.0"; + public override string ModuleAuthor => "OpenAI Codex"; + + public override void Load(bool hotReload) + { + _sendProxy = new SendProxyNative(GetSendProxyLibraryPath()); + _syncTimer = AddTimer(0.25f, SyncRules, TimerFlags.REPEAT); + RegisterListener(_ => Server.NextFrame(SyncRules)); + RegisterListener(_ => Server.NextFrame(SyncRules)); + SyncRules(); + } + + public override void Unload(bool hotReload) + { + _syncTimer?.Kill(); + ClearNativeRules(); + _sendProxy?.Dispose(); + _sendProxy = null; + } + + private void SyncRules() + { + if (_sendProxy == null) + return; + + var desired = new Dictionary(); + var viewers = Utilities.GetPlayers() + .Where(player => player.IsValid && !player.IsBot && !player.IsHLTV) + .ToArray(); + + AddHealthRules(desired, viewers); + + foreach (var weapon in EnumerateWeapons()) + { + var owner = GetOwner(weapon); + bool dropped = owner == null; + int weaponIndex = (int)weapon.Index; + string designerName = weapon.DesignerName; + + foreach (var viewer in viewers) + { + int recipientSlot = viewer.Slot; + + if (owner != null && IsOtherPlayerWeapon(viewer, owner)) + { + AddDesiredRule( + desired, + recipientSlot, + weaponIndex, + "CBaseEntity", + "m_fEffects", + RuleValue.UInt32(EfNoDraw)); + continue; + } + + if (!dropped) + continue; + + int team = (int)viewer.Team; + if (team == TeamT && IsAk47(designerName)) + { + AddDesiredRule( + desired, + recipientSlot, + weaponIndex, + "CBaseModelEntity", + "m_clrRender", + RuleValue.Color(255, 0, 0, 255)); + } + else if (team == TeamCt && IsM4A1S(designerName)) + { + AddDesiredRule( + desired, + recipientSlot, + weaponIndex, + "CBaseModelEntity", + "m_clrRender", + RuleValue.Color(0, 96, 255, 255)); + } + } + } + + 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 = _sendProxy.SetBytes( + key.RecipientSlot, + key.EntityIndex, + key.ClassName, + key.FieldPath, + value.Bytes); + + if (ruleId != 0) + _activeRules[key] = ruleId; + } + } + + private static void AddHealthRules(Dictionary desired, IReadOnlyCollection viewers) + { + foreach (var viewer in viewers) + { + var viewerPawn = viewer.PlayerPawn.Value; + int viewerPawnIndex = viewerPawn != null && viewerPawn.IsValid ? (int)viewerPawn.Index : -1; + + foreach (var target in viewers) + { + var targetPawn = target.PlayerPawn.Value; + if (targetPawn == null || !targetPawn.IsValid) + continue; + + int targetPawnIndex = (int)targetPawn.Index; + if (targetPawnIndex == viewerPawnIndex) + continue; + + AddDesiredRule( + desired, + viewer.Slot, + targetPawnIndex, + "CBaseEntity", + "m_iHealth", + RuleValue.Int32(SpoofedHealth)); + } + } + } + + private static void AddDesiredRule( + Dictionary desired, + int recipientSlot, + int entityIndex, + string className, + string fieldPath, + RuleValue value) + { + desired[new RuleKey(recipientSlot, entityIndex, className, fieldPath)] = value; + } + + private IEnumerable EnumerateWeapons() + { + foreach (var entity in Utilities.GetAllEntities()) + { + if (!entity.IsValid || !entity.DesignerName.StartsWith("weapon_", StringComparison.Ordinal)) + continue; + + CBasePlayerWeapon weapon = entity.As(); + if (weapon.IsValid) + yield return weapon; + } + } + + private static CBaseEntity? GetOwner(CBasePlayerWeapon weapon) + { + var owner = weapon.OwnerEntity.Value; + return owner != null && owner.IsValid ? owner : null; + } + + private static bool IsOtherPlayerWeapon(CCSPlayerController viewer, CBaseEntity owner) + { + var ownPawn = viewer.PlayerPawn.Value; + if (ownPawn == null || !ownPawn.IsValid) + return owner.DesignerName.Contains("player", StringComparison.Ordinal); + + return owner.Index != ownPawn.Index && owner.DesignerName.Contains("player", StringComparison.Ordinal); + } + + private static bool IsAk47(string designerName) + { + return designerName.Equals("weapon_ak47", StringComparison.Ordinal); + } + + private static bool IsM4A1S(string designerName) + { + return designerName.Equals("weapon_m4a1_silencer", StringComparison.Ordinal); + } + + private void ClearNativeRules() + { + if (_sendProxy == null) + return; + + foreach (int ruleId in _activeRules.Values) + _sendProxy.RemoveOverride(ruleId); + _activeRules.Clear(); + } + + private string GetSendProxyLibraryPath() + { + string addonsDirectory = Path.GetFullPath(Path.Combine(ModuleDirectory, "..", "..", "..")); + string libraryPath = Path.Combine(addonsDirectory, "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) + { + public static RuleValue Int32(int value) + { + byte[] bytes = new byte[4]; + BitConverter.TryWriteBytes(bytes, value); + return new RuleValue(bytes); + } + + public static RuleValue UInt32(uint value) + { + byte[] bytes = new byte[4]; + BitConverter.TryWriteBytes(bytes, value); + return new RuleValue(bytes); + } + + public static RuleValue Color(byte r, byte g, byte b, byte a) + { + return new RuleValue(new[] { r, g, b, a }); + } + } +} diff --git a/gamedata/sendproxy.games.txt b/gamedata/sendproxy.games.txt new file mode 100644 index 0000000..75d0af4 --- /dev/null +++ b/gamedata/sendproxy.games.txt @@ -0,0 +1,43 @@ +"Games" +{ + "csgo" + { + "Signatures" + { + "CServerSideClient_SendSnapshot" + { + "library" "engine" + "linux" "55 48 89 e5 41 57 41 56 41 55 41 54 49 89 f4 53 48 89 fb 48 83 ec 18 c6 46 4c 00 80 bf f9 09 00 00 00" + } + "CNetworkGameServer_PackEntities_Normal" + { + "library" "engine" + "linux" "55 48 89 e5 41 57 41 56 48 8d 85 60 db ff ff 41 89 d6 ba 80 00 00 00 41 55 41 54 4c 8d 65 c0 53 48 8d 9d c0 db ff ff 48 81 ec 70 26 00 00" + } + "CNetworkGameServer_PackEntity" + { + "library" "engine" + "linux" "55 48 89 e5 41 57 41 56 41 55 4d 89 c5 41 54 53 89 d3 81 c2 ff 03 00 00 48 81 ec 48 63 00 00" + } + } + "Offsets" + { + "CNetworkGameServer_ClientList" + { + "linux" "80" + } + "GameEntitySystem" + { + "linux" "80" + } + "CServerSideClient_Slot" + { + "linux" "224" + } + "CServerSideClient_DeltaTick" + { + "linux" "348" + } + } + } +} diff --git a/hl2sdk-manifests/SdkHelpers.ambuild b/hl2sdk-manifests/SdkHelpers.ambuild new file mode 100644 index 0000000..7bde20c --- /dev/null +++ b/hl2sdk-manifests/SdkHelpers.ambuild @@ -0,0 +1,292 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: +import json +import os + +def get_this_file_path(): + return get_this_file_path.__code__.co_filename + +class SdkLibBuilder(object): + def __init__(self, sdk, name, cxx): + self.targets = [cxx.clone()] + self.libs = [] + self.sdk = sdk + self.name = name + def ConfigureLibrary(self, project, compiler, context, prefix = ''): + name = prefix + project.name + binary = project.Configure(compiler, '{0}_{1}'.format(self.sdk['name'], name), '{0} - {1} - {2}'.format(self.sdk['name'], self.name, compiler.target.arch)) + binary.compiler.cxxincludes += [ + os.path.join(context.currentSourcePath) + ] + SdkHelpers.configureCxx(context, binary, self.sdk) + return binary + + +class SdkTarget(object): + def __init__(self, context, sdk, cxx): + self.sdk = sdk + self.cxx = cxx + # Find protoc + self.protoc = None + rel_protoc_path = sdk[cxx.target.platform].get('protoc_path', None) + if rel_protoc_path: + protoc_path = os.path.join(sdk['path'], rel_protoc_path) + self.protoc = context.DetectProtoc(path = protoc_path) + for path in sdk['include_paths']: + self.protoc.includes += [os.path.join(sdk['path'], path)] + # Find tier1 + tier1 = SdkTarget.findLibrary(context, sdk, cxx, 'tier1') + # Find mathlib + mathlib = SdkTarget.findLibrary(context, sdk, cxx, 'mathlib') + + # Only append the libs at the end, so they aren't found during the configureCxx step + if not 'tier1' in sdk : + sdk['tier1'] = {} + if not 'mathlib' in sdk : + sdk['mathlib'] = {} + sdk['tier1'][cxx.target.arch] = tier1 + sdk['mathlib'][cxx.target.arch] = mathlib + + + @staticmethod + def findLibrary(context, sdk, cxx, name): + # Mock libs are currently unhandled (different ambuild scripts) + if sdk['name'] == 'mock': + return None + ambuilder = os.path.join(sdk['path'], name, 'AMBuilder') + if os.path.exists(ambuilder): + ambuilder = os.path.abspath(ambuilder) + libbuilders = SdkLibBuilder(sdk, name, cxx) + oldSource = context.cm.sourcePath + context.cm.sourcePath = os.path.dirname(ambuilder) + libcontext = context.Build('AMBuilder', { 'HL2SDK': libbuilders }) + context.cm.sourcePath = oldSource + if len(libbuilders.libs) != 1: + raise Exception('No lib found for {0}'.format(name)) + return libbuilders.libs[0] + return None + +class SdkHelpers(object): + def __init__(self): + self.sdks = {} + self.sdk_manifests = [] + self.sdk_filter = None + self.find_sdk_path = None + self.sdk_targets = [] + + # find_sdk_path must be set to use. + def findSdks(self, builder, cxx_list, sdk_list): + not_found = [] + sdk_remaining = set(sdk_list) + for sdk_name, sdk in SdkHelpers.getSdks(builder): + self.sdk_manifests.append(sdk) + # Skip SDKs that weren't specified or are not supported. + if not self.shouldFindSdk(sdk, sdk_list): + continue + # Skip SDKs that won't build on any targets. + if not SdkHelpers.sdkHasBuildTargets(sdk, cxx_list): + continue + + sdk_path = self.find_sdk_path(sdk_name) + if sdk_path is None: + if SdkHelpers.shouldRequireSdk(sdk_name, sdk_list): + raise Exception('Could not find a valid path for {0}'.format(sdk_name)) + not_found.append(sdk_name) + continue + + sdk_remaining.discard(sdk_name) + + sdk['path'] = sdk_path + self.sdks[sdk_name] = sdk + + for cxx in cxx_list: + if SdkHelpers.shouldBuildSdk(sdk, cxx): + self.sdk_targets += [SdkTarget(builder, sdk, cxx)] + + if 'present' in sdk_list: + for sdk in not_found: + print('Warning: hl2sdk-{} was not found, and will not be included in build.'.format(sdk)) + elif len(sdk_remaining) and 'all' not in sdk_list: + for sdk in sdk_remaining: + print('Error: hl2sdk-{} was not found.'.format(sdk)) + raise Exception('Missing hl2sdks: {}'.format(','.join(sdk_remaining))) + + if not len(self.sdk_targets) and len(sdk_list): + raise Exception('No buildable SDKs were found, nothing to build.') + + @staticmethod + def shouldRequireSdk(sdk_name, sdk_list): + if 'all' in sdk_list: + return sdk_name != 'mock' + if sdk_name in sdk_list: + return True + return 'present' not in sdk_list + + def shouldFindSdk(self, sdk, sdk_list): + # Remove SDKs that the project doesn't support. + if self.sdk_filter and not self.sdk_filter(sdk): + return False + if 'all' in sdk_list or 'present' in sdk_list: + return True + return sdk['name'] in sdk_list + + @staticmethod + def sdkHasBuildTargets(sdk, cxx_list): + for cxx in cxx_list: + if SdkHelpers.shouldBuildSdk(sdk, cxx): + return True + return False + + @staticmethod + def shouldBuildSdk(sdk, cxx): + if cxx.target.platform in sdk['platforms']: + if cxx.target.arch in sdk['platforms'][cxx.target.platform]: + return True + return False + + @staticmethod + def addLists(sdk, list_name, cxx): + result = SdkHelpers.getLists(sdk, list_name, cxx) + cxx_list = getattr(cxx, list_name) + cxx_list.extend(result) + + @staticmethod + def getLists(sdk, list_name, cxx): + result = SdkHelpers.getListsImpl(sdk, list_name, cxx) + if Project in sdk: + result += SdkHelpers.getListsImpl(sdk[Project], list_name, cxx) + return result + + @staticmethod + def getListsImpl(info, list_name, cxx): + result = [] + if cxx.target.platform in info: + platform_info = info[cxx.target.platform] + result += platform_info.get(list_name, []) + if cxx.target.arch in platform_info: + arch_info = platform_info[cxx.target.arch] + result += arch_info.get(list_name, []) + return result + + @staticmethod + def getSdks(builder): + try: + # Requires new enough ambuild version for __file__ to be defined + sdk_manifest_dir = os.path.join(os.path.dirname(__file__), 'manifests') + except NameError: + sdk_manifest_dir = os.path.join(os.path.dirname(get_this_file_path()), 'manifests') + + out = [] + for sdk_manifest in os.listdir(sdk_manifest_dir): + sdk_name, _ = os.path.splitext(sdk_manifest) + sdk_manifest_path = os.path.join(sdk_manifest_dir, sdk_manifest) + with open(sdk_manifest_path, 'rt') as fp: + sdk = json.load(fp) + builder.AddConfigureFile(sdk_manifest_path) + out.append((sdk_name, sdk)) + return out + + @staticmethod + def addLibrary(context, binary, sdk, name): + ambuilder = os.path.join(sdk['path'], name, 'AMBuilder') + if os.path.exists(ambuilder): + builder = SdkLibBuilder(sdk['name'], binary.compiler) + context.Build([ambuilder], { 'HL2SDK': builder }) + if len(builder.libs) != 1: + raise Exception('No lib found for {0}'.format(name)) + task = builder.libs[0] + binary.compiler.postlink += [os.path.join(context.buildPath, task.binary.path)] + binary.compiler.linkdeps += [task.binary] + + @staticmethod + def configureCxx(context, binary, sdk): + cxx = binary.compiler + + # Includes/defines. + cxx.defines += ['SOURCE_ENGINE={}'.format(sdk['code'])] + cxx.defines += ['GAME_DLL', 'RAD_TELEMETRY_DISABLED'] + + if sdk['name'] in ['sdk2013', 'bms', 'pvkii', 'tf2', 'css', 'dods', 'hl2dm', 'l4d2'] and cxx.like('gcc'): + # The 2013 SDK already has these in public/tier0/basetypes.h + rm_defines = [ + 'stricmp=strcasecmp', '_stricmp=strcasecmp', + '_snprintf=snprintf', '_vsnprintf=vsnprintf,' + ] + for rm_define in rm_defines: + if rm_define in cxx.defines: + try: + cxx.defines.remove(rm_define) + except ValueError: + pass + + if cxx.family == 'msvc': + cxx.defines += ['COMPILER_MSVC'] + if cxx.target.arch == 'x86': + cxx.defines += ['COMPILER_MSVC32', 'WIN32'] + elif cxx.target.arch == 'x86_64': + cxx.defines += ['COMPILER_MSVC64', 'WIN32', 'WIN64'] + + if cxx.version >= 1900: + cxx.linkflags += ['legacy_stdio_definitions.lib'] + else: + cxx.defines += ['COMPILER_GCC', '_LINUX', 'LINUX', 'POSIX', 'GNUC'] + + if cxx.target.arch == 'x86_64': + cxx.defines += ['X64BITS', 'PLATFORM_64BITS'] + + SdkHelpers.addLists(sdk, 'defines', cxx) + SdkHelpers.addLists(sdk, 'linkflags', cxx) + + for path in sdk['include_paths']: + cxx.cxxincludes += [os.path.join(sdk['path'], path)] + + # Link steps. + for lib in SdkHelpers.getLists(sdk, 'libs', cxx): + cxx.linkflags += [os.path.join(sdk['path'], lib)] + for lib in SdkHelpers.getLists(sdk, 'postlink_libs', cxx): + cxx.postlink += [os.path.join(sdk['path'], lib)] + + dynamic_libs = SdkHelpers.getLists(sdk, 'dynamic_libs', cxx) + for library in dynamic_libs: + file_name = os.path.split(library)[1] + source_path = os.path.join(sdk['path'], library) + output_path = os.path.join(binary.localFolder, file_name) + + context.AddFolder(binary.localFolder) + output = context.AddSymlink(source_path, output_path) + + cxx.weaklinkdeps += [output] + cxx.linkflags[0:0] = [file_name] + + # cflags + for cflag in SdkHelpers.getLists(sdk, 'cflags', cxx): + cxx.cflags += [cflag] + + # cxxflags + for cxxflag in SdkHelpers.getLists(sdk, 'cxxflags', cxx): + cxx.cxxflags += [cxxflag] + + if cxx.target.platform == 'linux': + cxx.linkflags[0:0] = ['-lm'] + + if cxx.target.platform == 'linux': + if sdk[cxx.target.platform]['uses_system_cxxlib']: + try: + cxx.linkflags.remove('-static-libstdc++') + except ValueError: + pass + cxx.linkflags += ['-lstdc++'] + + if 'tier1' in sdk and cxx.target.arch in sdk['tier1']: + task = sdk['tier1'][cxx.target.arch] + if task != None: + cxx.postlink += [os.path.join(context.buildPath, task.binary.path)] + cxx.linkdeps += [task.binary] + + if 'mathlib' in sdk and cxx.target.arch in sdk['mathlib']: + task = sdk['mathlib'][cxx.target.arch] + if task != None: + cxx.postlink += [os.path.join(context.buildPath, task.binary.path)] + cxx.linkdeps += [task.binary] + + +rvalue = SdkHelpers() diff --git a/hl2sdk-manifests/manifests/cs2.json b/hl2sdk-manifests/manifests/cs2.json new file mode 100644 index 0000000..7f80dd5 --- /dev/null +++ b/hl2sdk-manifests/manifests/cs2.json @@ -0,0 +1,57 @@ +{ + "name": "cs2", + "env_var": "HL2SDKCS2", + "extension": "2.cs2", + "code": 25, + "define": "CS2", + "platforms": { + "windows": [ + "x86_64" + ], + "linux": [ + "x86_64" + ] + }, + "source2": true, + "include_paths": [ + "thirdparty/protobuf-3.21.8/src", + "public", + "public/engine", + "public/mathlib", + "public/tier0", + "public/tier1", + "public/entity2", + "public/game/server", + "game/shared", + "game/server", + "common" + ], + "linux": { + "x86_64": { + "postlink_libs": [ + "lib/linux64/mathlib.a", + "lib/linux64/interfaces.a", + "lib/linux64/release/libprotobuf.a" + ], + "dynamic_libs": [ + "lib/linux64/libtier0.so" + ] + }, + "defines": [ + "_GLIBCXX_USE_CXX11_ABI=0" + ], + "uses_system_cxxlib": false, + "protoc_path": "devtools/bin/linux/protoc" + }, + "windows": { + "x86_64": { + "libs": [ + "lib/public/win64/2015/libprotobuf.lib", + "lib/public/win64/mathlib.lib", + "lib/public/win64/tier0.lib", + "lib/public/win64/interfaces.lib" + ] + }, + "protoc_path": "devtools/bin/protoc.exe" + } +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..49fe495 --- /dev/null +++ b/src/common.h @@ -0,0 +1,12 @@ +#pragma once + +#include "platform.h" + +#define ROOTBIN "/bin/linuxsteamrt64/" +#define GAMEBIN "/csgo/bin/linuxsteamrt64/" +#define MODULE_PREFIX "lib" +#define MODULE_EXT ".so" + +void SPMessage(const char *msg, ...); +void SPWarning(const char *msg, ...); + diff --git a/src/detour.cpp b/src/detour.cpp new file mode 100644 index 0000000..c8ebaac --- /dev/null +++ b/src/detour.cpp @@ -0,0 +1,69 @@ +#include "detour.h" + +#include "common.h" +#include "gameconfig.h" +#include "overrides.h" +#include "playerslot.h" + +extern CGameConfig* g_gameConfig; +extern bool g_enabled; +extern bool g_armed; + +using SendSnapshotFn = void (*)(void*, void*); +static SendSnapshotFn g_sendSnapshot = nullptr; +static funchook_t* g_hook = nullptr; + +static CPlayerSlot SlotFromClient(void* client) +{ + int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_Slot") : -1; + if (offset < 0) + return CPlayerSlot(-1); + return *reinterpret_cast(reinterpret_cast(client) + offset); +} + +static void Detour_SendSnapshot(void* client, void* snapshot) +{ + if (!g_enabled || !g_armed || !client || g_overrides.Empty()) + { + g_sendSnapshot(client, snapshot); + return; + } + + CPlayerSlot slot = SlotFromClient(client); + auto applied = g_overrides.ApplyForRecipient(slot); + g_sendSnapshot(client, snapshot); + g_overrides.Restore(applied); +} + +bool InitSnapshotDetour(CGameConfig* config) +{ + void* target = config->ResolveSignature("CServerSideClient_SendSnapshot"); + if (!target) + { + SPWarning("CServerSideClient_SendSnapshot signature missing; snapshot hook disabled\n"); + return true; + } + + g_sendSnapshot = reinterpret_cast(target); + g_hook = funchook_create(); + if (!g_hook) + return false; + + if (funchook_prepare(g_hook, reinterpret_cast(&g_sendSnapshot), reinterpret_cast(Detour_SendSnapshot)) != 0) + return false; + if (funchook_install(g_hook, 0) != 0) + return false; + + SPMessage("detoured CServerSideClient_SendSnapshot at %p\n", target); + return true; +} + +void ShutdownSnapshotDetour() +{ + if (!g_hook) + return; + funchook_uninstall(g_hook, 0); + funchook_destroy(g_hook); + g_hook = nullptr; + g_sendSnapshot = nullptr; +} diff --git a/src/detour.h b/src/detour.h new file mode 100644 index 0000000..7d192a8 --- /dev/null +++ b/src/detour.h @@ -0,0 +1,9 @@ +#pragma once + +#include "funchook.h" + +class CGameConfig; + +bool InitSnapshotDetour(CGameConfig* config); +void ShutdownSnapshotDetour(); + diff --git a/src/gameconfig.cpp b/src/gameconfig.cpp new file mode 100644 index 0000000..025b218 --- /dev/null +++ b/src/gameconfig.cpp @@ -0,0 +1,137 @@ +#include "gameconfig.h" + +#include "common.h" +#include "module.h" + +#include +#include + +CGameConfig::CGameConfig(std::string gameDir, std::string path) + : m_gameDir(std::move(gameDir)), m_path(std::move(path)), m_kv(new KeyValues("Games")) +{ +} + +CGameConfig::~CGameConfig() +{ + delete m_kv; +} + +bool CGameConfig::Init(IFileSystem* fs, char* error, size_t maxlen) +{ + if (!m_kv->LoadFromFile(fs, m_path.c_str(), "GAME")) + { + snprintf(error, maxlen, "failed to load gamedata"); + return false; + } + + KeyValues* game = m_kv->FindKey(m_gameDir.c_str(), false); + if (!game) + { + snprintf(error, maxlen, "missing game section %s", m_gameDir.c_str()); + return false; + } + + if (KeyValues* sigs = game->FindKey("Signatures", false)) + { + FOR_EACH_SUBKEY(sigs, it) + { + m_libraries[it->GetName()] = it->GetString("library", ""); + m_signatures[it->GetName()] = it->GetString("linux", ""); + } + } + + if (KeyValues* offsets = game->FindKey("Offsets", false)) + { + FOR_EACH_SUBKEY(offsets, it) + { + m_offsets[it->GetName()] = it->GetInt("linux", -1); + } + } + + return true; +} + +const char* CGameConfig::GetSignature(const char* name) const +{ + auto it = m_signatures.find(name); + return it == m_signatures.end() ? nullptr : it->second.c_str(); +} + +const char* CGameConfig::GetLibrary(const char* name) const +{ + auto it = m_libraries.find(name); + return it == m_libraries.end() ? nullptr : it->second.c_str(); +} + +int CGameConfig::GetOffset(const char* name) const +{ + auto it = m_offsets.find(name); + return it == m_offsets.end() ? -1 : it->second; +} + +bool CGameConfig::HexToBytes(const char* src, std::vector& out) +{ + if (!src || !*src) + return false; + + for (const char* p = src; *p;) + { + while (*p == ' ' || *p == '\t') + ++p; + if (!*p) + break; + if (p[0] == '\\' && p[1] == 'x') + p += 2; + if (p[0] == '?' && p[1] == '?') + { + out.push_back(0x2A); + p += 2; + continue; + } + unsigned value = 0; + if (sscanf(p, "%2x", &value) != 1) + return false; + out.push_back(static_cast(value)); + p += 2; + } + return !out.empty(); +} + +void* CGameConfig::ResolveSignature(const char* name) const +{ + const char* sig = GetSignature(name); + const char* library = GetLibrary(name); + if (!sig || !*sig || !library || !*library) + return nullptr; + + CModule* module = nullptr; + if (!strcmp(library, "engine")) + module = modules::engine; + else if (!strcmp(library, "server")) + module = modules::server; + else if (!strcmp(library, "schemasystem")) + module = modules::schemasystem; + + if (!module) + return nullptr; + + if (sig[0] == '@') + return dlsym(module->GetHandle(), sig + 1); + + std::vector bytes; + if (!HexToBytes(sig, bytes)) + return nullptr; + + int error = SIG_NOT_FOUND; + void* addr = module->FindSignature(bytes.data(), bytes.size(), error); + if (error == SIG_FOUND_MULTIPLE) + SPWarning("signature %s matched multiple locations; using first\n", name); + return addr; +} + +std::string CGameConfig::DirectoryName(const char* path) +{ + std::string s(path ? path : ""); + size_t pos = s.find_last_of("/\\"); + return pos == std::string::npos ? s : s.substr(pos + 1); +} diff --git a/src/gameconfig.h b/src/gameconfig.h new file mode 100644 index 0000000..1e78c04 --- /dev/null +++ b/src/gameconfig.h @@ -0,0 +1,35 @@ +#pragma once + +#include "KeyValues.h" + +#include +#include +#include +#include + +class CGameConfig +{ +public: + CGameConfig(std::string gameDir, std::string path); + ~CGameConfig(); + + bool Init(IFileSystem* fs, char* error, size_t maxlen); + const char* GetSignature(const char* name) const; + const char* GetLibrary(const char* name) const; + int GetOffset(const char* name) const; + void* ResolveSignature(const char* name) const; + const std::string& GetPath() const { return m_path; } + + static std::string DirectoryName(const char* path); + +private: + static bool HexToBytes(const char* src, std::vector& out); + + std::string m_gameDir; + std::string m_path; + KeyValues* m_kv {}; + std::unordered_map m_signatures; + std::unordered_map m_libraries; + std::unordered_map m_offsets; +}; + diff --git a/src/module.cpp b/src/module.cpp new file mode 100644 index 0000000..71465af --- /dev/null +++ b/src/module.cpp @@ -0,0 +1,119 @@ +#include "module.h" + +#include "dbg.h" +#include "filesystem.h" +#include "strtools.h" + +#include +#include +#include +#include +#include +#include + +namespace modules +{ + CModule* engine = nullptr; + CModule* server = nullptr; + CModule* schemasystem = nullptr; +} + +static bool GetModuleInformation(void* handle, void** base, size_t* length, std::vector
& sections) +{ + link_map* lmap = nullptr; + if (dlinfo(handle, RTLD_DI_LINKMAP, &lmap) != 0 || !lmap) + return false; + + int fd = open(lmap->l_name, O_RDONLY); + if (fd == -1) + return false; + + struct stat st {}; + if (fstat(fd, &st) != 0) + { + close(fd); + return false; + } + + void* map = mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); + close(fd); + if (map == MAP_FAILED) + return false; + + auto* ehdr = static_cast(map); + auto* shdrs = reinterpret_cast(reinterpret_cast(ehdr) + ehdr->e_shoff); + const char* strTab = reinterpret_cast(reinterpret_cast(ehdr) + shdrs[ehdr->e_shstrndx].sh_offset); + + for (int i = 0; i < ehdr->e_phnum; ++i) + { + auto* phdr = reinterpret_cast(reinterpret_cast(ehdr) + ehdr->e_phoff + i * ehdr->e_phentsize); + if (phdr->p_type == PT_LOAD && (phdr->p_flags & PF_X)) + { + *base = reinterpret_cast(lmap->l_addr + phdr->p_vaddr); + *length = phdr->p_filesz; + break; + } + } + + for (int i = 0; i < ehdr->e_shnum; ++i) + { + auto* shdr = reinterpret_cast(reinterpret_cast(shdrs) + i * ehdr->e_shentsize); + if (*(strTab + shdr->sh_name) == '\0') + continue; + sections.push_back({strTab + shdr->sh_name, reinterpret_cast(lmap->l_addr + shdr->sh_addr), shdr->sh_size}); + } + + munmap(map, st.st_size); + return *base && *length; +} + +CModule::CModule(const char* relativePath, const char* module) : m_name(module) +{ + char path[MAX_PATH]; + V_snprintf(path, sizeof(path), "%s%s%s%s%s", Plat_GetGameDirectory(), relativePath, MODULE_PREFIX, module, MODULE_EXT); + + m_handle = dlopen(path, RTLD_NOW | RTLD_NOLOAD); + if (!m_handle) + m_handle = dlopen(path, RTLD_NOW); + if (!m_handle) + Error("[SendProxy] Could not open %s: %s\n", path, dlerror()); + + if (!GetModuleInformation(m_handle, &m_base, &m_size, m_sections)) + Error("[SendProxy] Could not inspect %s\n", path); + + SPMessage("module %s base=%p size=%zu\n", module, m_base, m_size); +} + +void* CModule::FindSignature(const uint8_t* pattern, size_t length, int& error) const +{ + error = SIG_NOT_FOUND; + void* found = nullptr; + auto* memory = static_cast(m_base); + + for (size_t i = 0; i + length < m_size; ++i) + { + size_t matched = 0; + while (matched < length && (pattern[matched] == 0x2A || memory[i + matched] == pattern[matched])) + ++matched; + if (matched != length) + continue; + if (found) + { + error = SIG_FOUND_MULTIPLE; + return found; + } + found = memory + i; + error = SIG_OK; + } + + return found; +} + +void* CModule::FindInterface(const char* name) const +{ + auto fn = reinterpret_cast(dlsym(m_handle, "CreateInterface")); + if (!fn) + return nullptr; + return fn(name, nullptr); +} + diff --git a/src/module.h b/src/module.h new file mode 100644 index 0000000..72f51ae --- /dev/null +++ b/src/module.h @@ -0,0 +1,51 @@ +#pragma once + +#include "common.h" +#include "interface.h" + +#include +#include +#include +#include +#include +#include + +struct Section +{ + std::string name; + void* base; + size_t size; +}; + +enum SigError +{ + SIG_OK, + SIG_NOT_FOUND, + SIG_FOUND_MULTIPLE, +}; + +class CModule +{ +public: + CModule(const char* relativePath, const char* module); + ~CModule() = default; + + void* FindSignature(const uint8_t* pattern, size_t length, int& error) const; + void* FindInterface(const char* name) const; + void* GetHandle() const { return m_handle; } + +private: + void* m_handle {}; + void* m_base {}; + size_t m_size {}; + std::vector
m_sections; + std::string m_name; +}; + +namespace modules +{ + extern CModule* engine; + extern CModule* server; + extern CModule* schemasystem; +} + diff --git a/src/overrides.cpp b/src/overrides.cpp new file mode 100644 index 0000000..76cab80 --- /dev/null +++ b/src/overrides.cpp @@ -0,0 +1,463 @@ +#include "overrides.h" + +#include "common.h" +#include "schema.h" +#include "entity2/entitysystem.h" +#include "entityhandle.h" + +#include +#include + +extern CGameEntitySystem* g_entitySystem; +extern CGameEntitySystem* GameEntitySystem(); + +OverrideManager g_overrides; + +static bool ParseType(const char* s, FieldType& out) +{ + if (!V_stricmp(s, "bool")) { out = FieldType::Bool; return true; } + if (!V_stricmp(s, "int8")) { out = FieldType::Int8; return true; } + if (!V_stricmp(s, "uint8")) { out = FieldType::UInt8; return true; } + if (!V_stricmp(s, "int16")) { out = FieldType::Int16; return true; } + if (!V_stricmp(s, "uint16")) { out = FieldType::UInt16; return true; } + if (!V_stricmp(s, "int") || !V_stricmp(s, "int32")) { out = FieldType::Int32; return true; } + if (!V_stricmp(s, "uint") || !V_stricmp(s, "uint32")) { out = FieldType::UInt32; return true; } + if (!V_stricmp(s, "int64")) { out = FieldType::Int64; return true; } + if (!V_stricmp(s, "uint64")) { out = FieldType::UInt64; return true; } + if (!V_stricmp(s, "float")) { out = FieldType::Float; return true; } + return false; +} + +static uint64_t ParseValue(FieldType type, const char* value) +{ + if (type == FieldType::Float) + { + float f = static_cast(atof(value)); + uint32_t bits; + memcpy(&bits, &f, sizeof(bits)); + return bits; + } + if (type == FieldType::Bool) + return (!V_stricmp(value, "true") || atoi(value) != 0) ? 1 : 0; + return strtoull(value, nullptr, 0); +} + +static uint64_t ReadBits(uint8_t* address, FieldType type) +{ + switch (type) + { + case FieldType::Bool: return *reinterpret_cast(address) ? 1 : 0; + case FieldType::Int8: return static_cast(*reinterpret_cast(address)); + case FieldType::UInt8: return *reinterpret_cast(address); + case FieldType::Int16: return static_cast(*reinterpret_cast(address)); + case FieldType::UInt16: return *reinterpret_cast(address); + case FieldType::Int32: return static_cast(*reinterpret_cast(address)); + case FieldType::UInt32: return *reinterpret_cast(address); + case FieldType::Int64: return static_cast(*reinterpret_cast(address)); + case FieldType::UInt64: return *reinterpret_cast(address); + case FieldType::Float: { + uint32_t bits; + memcpy(&bits, address, sizeof(bits)); + return bits; + } + } + return 0; +} + +static void WriteBits(uint8_t* address, FieldType type, uint64_t bits) +{ + switch (type) + { + case FieldType::Bool: *reinterpret_cast(address) = bits != 0; break; + case FieldType::Int8: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::UInt8: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::Int16: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::UInt16: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::Int32: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::UInt32: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::Int64: *reinterpret_cast(address) = static_cast(bits); break; + case FieldType::UInt64: *reinterpret_cast(address) = bits; break; + case FieldType::Float: { + uint32_t narrowed = static_cast(bits); + memcpy(address, &narrowed, sizeof(narrowed)); + break; + } + } +} + +static const char* TypeName(FieldType type) +{ + switch (type) + { + case FieldType::Bool: return "bool"; + case FieldType::Int8: return "int8"; + case FieldType::UInt8: return "uint8"; + case FieldType::Int16: return "int16"; + case FieldType::UInt16: return "uint16"; + case FieldType::Int32: return "int32"; + case FieldType::UInt32: return "uint32"; + case FieldType::Int64: return "int64"; + case FieldType::UInt64: return "uint64"; + case FieldType::Float: return "float"; + } + return "unknown"; +} + +static bool DesignerContains(CEntityIdentity* identity, const char* needle) +{ + const char* designer = identity ? identity->m_designerName.String() : nullptr; + return designer && V_stristr(designer, needle); +} + +static int EntityIndex(CEntityIdentity* identity) +{ + return identity ? identity->m_EHandle.GetEntryIndex() : -1; +} + +static CEntityIdentity* FindIdentityByIndex(int index) +{ + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return nullptr; + + int visited = 0; + for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + if (EntityIndex(identity) == index) + return identity; + } + return nullptr; +} + +static CEntityIdentity* FindControllerBySlot(CPlayerSlot slot) +{ + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return nullptr; + + auto slotField = schema::FindField("CBasePlayerController", "m_nSplitScreenSlot"); + if (!slotField.found) + return nullptr; + + int visited = 0; + for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + if (!identity->m_pInstance || !DesignerContains(identity, "controller")) + continue; + uintptr_t entity = reinterpret_cast(identity->m_pInstance); + int controllerSlot = *reinterpret_cast(entity + slotField.offset); + if (controllerSlot == slot.Get()) + return identity; + } + return nullptr; +} + +static uint8_t* FieldAddress(uintptr_t entity, const char* className, const char* fieldName) +{ + auto field = schema::FindField(className, fieldName); + if (!field.found) + return nullptr; + return reinterpret_cast(entity + field.offset); +} + +static bool RequiredField(const char* className, const char* fieldName, std::string& error) +{ + auto field = schema::FindField(className, fieldName); + if (!field.found) + { + error = std::string("schema field not found: ") + className + "::" + fieldName; + return false; + } + return true; +} + +static bool ValidatePath(const OverrideRule& rule, std::string& error) +{ + for (const auto& segment : rule.path) + { + auto field = schema::FindField(segment.className.c_str(), segment.fieldName.c_str()); + if (!field.found) + { + error = "schema field not found"; + return false; + } + } + return true; +} + +void OverrideManager::Clear() +{ + m_rules.clear(); +} + +bool OverrideManager::AddFromTokens(int argc, const char** argv, std::string& error) +{ + if (argc < 7) + { + error = "usage: sp_set "; + return false; + } + + OverrideRule rule; + rule.id = m_nextId++; + rule.recipientSlot = !V_stricmp(argv[1], "all") ? -1 : atoi(argv[1]); + rule.target = argv[2]; + rule.className = argv[3]; + rule.fieldName = argv[4]; + rule.path.push_back({rule.className, rule.fieldName}); + if (!ParseType(argv[5], rule.type)) + { + error = "unknown type"; + return false; + } + rule.bits = ParseValue(rule.type, argv[6]); + + if (!ValidatePath(rule, error)) + return false; + + m_rules.push_back(rule); + SPMessage("rule #%d: recipient=%d target=%s %s::%s type=%s\n", rule.id, rule.recipientSlot, rule.target.c_str(), rule.className.c_str(), rule.fieldName.c_str(), TypeName(rule.type)); + return true; +} + +bool OverrideManager::AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error) +{ + OverrideRule rule; + rule.id = m_nextId++; + rule.recipientSlot = recipientSlot; + rule.target = target; + rule.className = "CEconEntity"; + rule.fieldName = "m_AttributeManager.m_Item.m_iItemDefinitionIndex"; + rule.path.push_back({"CEconEntity", "m_AttributeManager"}); + rule.path.push_back({"CAttributeContainer", "m_Item"}); + rule.path.push_back({"CEconItemView", "m_iItemDefinitionIndex"}); + rule.type = FieldType::UInt16; + rule.bits = itemDefinitionIndex; + + if (!ValidatePath(rule, error)) + return false; + + m_rules.push_back(rule); + SPMessage("rule #%d: recipient=%d target=%s weapon item_definition_index=%u\n", rule.id, rule.recipientSlot, rule.target.c_str(), itemDefinitionIndex); + return true; +} + +bool OverrideManager::SetHealthMoneyTest(bool enabled, std::string& error) +{ + if (enabled) + { + if (!RequiredField("CCSPlayerController", "m_pInGameMoneyServices", error) || + !RequiredField("CCSPlayerController_InGameMoneyServices", "m_iAccount", error) || + !RequiredField("CBasePlayerController", "m_hPawn", error) || + !RequiredField("CBasePlayerController", "m_nSplitScreenSlot", error) || + !RequiredField("CBaseEntity", "m_iHealth", error) || + !RequiredField("CBaseEntity", "m_iTeamNum", error)) + { + return false; + } + } + + m_healthMoneyTest = enabled; + SPMessage("health_money_test=%d\n", m_healthMoneyTest ? 1 : 0); + return true; +} + +bool OverrideManager::SetHealth42Test(bool enabled, std::string& error) +{ + if (enabled && !RequiredField("CBaseEntity", "m_iHealth", error)) + return false; + + m_health42Test = enabled; + SPMessage("health_42_test=%d\n", m_health42Test ? 1 : 0); + return true; +} + +void OverrideManager::Dump() const +{ + SPMessage("%zu static rules loaded; %zu dynamic rules loaded; trace=%d\n", m_rules.size(), DynamicRuleCount(), m_trace ? 1 : 0); + if (m_healthMoneyTest) + SPMessage("dynamic: teammate CBaseEntity::m_iHealth = recipient CCSPlayerController_InGameMoneyServices::m_iAccount\n"); + if (m_health42Test) + SPMessage("dynamic: all player CBaseEntity::m_iHealth = 42\n"); + for (const auto& rule : m_rules) + SPMessage("#%d recipient=%d target=%s field=%s::%s type=%s\n", rule.id, rule.recipientSlot, rule.target.c_str(), rule.className.c_str(), rule.fieldName.c_str(), TypeName(rule.type)); +} + +uintptr_t OverrideManager::FindFirstEntityForTarget(const OverrideRule& rule) const +{ + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return 0; + + if (!V_stricmp(rule.target.c_str(), "entity")) + return 0; + + int visited = 0; + for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + auto* entity = identity->m_pInstance; + if (!entity) + continue; + const char* designer = identity->m_designerName.String(); + if (!designer) + continue; + if (!V_stricmp(rule.target.c_str(), "all") || V_stristr(designer, rule.target.c_str())) + return reinterpret_cast(entity); + } + return 0; +} + +uint8_t* OverrideManager::ResolveAddress(uintptr_t entity, const OverrideRule& rule) const +{ + uintptr_t address = entity; + for (const auto& segment : rule.path) + { + auto field = schema::FindField(segment.className.c_str(), segment.fieldName.c_str()); + if (!field.found) + return nullptr; + address += field.offset; + } + return reinterpret_cast(address); +} + +void OverrideManager::ApplyHealthMoneyTest(CPlayerSlot slot, std::vector& applied) +{ + if (!m_healthMoneyTest) + return; + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return; + + auto* recipientIdentity = FindControllerBySlot(slot); + if (!recipientIdentity || !DesignerContains(recipientIdentity, "controller") || !recipientIdentity->m_pInstance) + { + if (m_trace) + SPMessage("health_money_test recipient=%d no controller\n", slot.Get()); + return; + } + + uintptr_t recipient = reinterpret_cast(recipientIdentity->m_pInstance); + auto* moneyServicesAddress = FieldAddress(recipient, "CCSPlayerController", "m_pInGameMoneyServices"); + auto* teamAddress = FieldAddress(recipient, "CBaseEntity", "m_iTeamNum"); + auto* pawnHandleAddress = FieldAddress(recipient, "CBasePlayerController", "m_hPawn"); + if (!moneyServicesAddress || !teamAddress || !pawnHandleAddress) + { + if (m_trace) + SPMessage("health_money_test recipient=%d missing addresses\n", slot.Get()); + return; + } + + uintptr_t moneyServices = *reinterpret_cast(moneyServicesAddress); + if (!moneyServices) + { + if (m_trace) + SPMessage("health_money_test recipient=%d no money services\n", slot.Get()); + return; + } + auto* moneyAddress = FieldAddress(moneyServices, "CCSPlayerController_InGameMoneyServices", "m_iAccount"); + if (!moneyAddress) + return; + + int money = static_cast(ReadBits(moneyAddress, FieldType::Int32)); + int team = static_cast(*teamAddress); + if (team < 2) + { + if (m_trace) + SPMessage("health_money_test recipient=%d invalid team=%d\n", slot.Get(), team); + return; + } + + int ownPawnIndex = reinterpret_cast(pawnHandleAddress)->GetEntryIndex(); + int changed = 0; + int visited = 0; + for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + if (!identity->m_pInstance || EntityIndex(identity) == ownPawnIndex) + continue; + if (!DesignerContains(identity, "player") || DesignerContains(identity, "controller")) + continue; + + uintptr_t entity = reinterpret_cast(identity->m_pInstance); + auto* targetTeamAddress = FieldAddress(entity, "CBaseEntity", "m_iTeamNum"); + auto* healthAddress = FieldAddress(entity, "CBaseEntity", "m_iHealth"); + if (!targetTeamAddress || !healthAddress) + continue; + if (static_cast(*targetTeamAddress) != team) + continue; + + AppliedOverride saved {healthAddress, FieldType::Int32, ReadBits(healthAddress, FieldType::Int32)}; + WriteBits(healthAddress, FieldType::Int32, static_cast(money)); + applied.push_back(saved); + ++changed; + } + + if (m_trace) + SPMessage("health_money_test recipient=%d money=%d changed=%d\n", slot.Get(), money, changed); +} + +void OverrideManager::ApplyHealth42Test(CPlayerSlot slot, std::vector& applied) +{ + if (!m_health42Test) + return; + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return; + + int changed = 0; + int visited = 0; + for (auto* identity = g_entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + if (!identity->m_pInstance) + continue; + if (!DesignerContains(identity, "player") || DesignerContains(identity, "controller")) + continue; + + uintptr_t entity = reinterpret_cast(identity->m_pInstance); + auto* healthAddress = FieldAddress(entity, "CBaseEntity", "m_iHealth"); + if (!healthAddress) + continue; + + AppliedOverride saved {healthAddress, FieldType::Int32, ReadBits(healthAddress, FieldType::Int32)}; + WriteBits(healthAddress, FieldType::Int32, 42); + applied.push_back(saved); + ++changed; + } + + if (m_trace) + SPMessage("health_42_test recipient=%d changed=%d\n", slot.Get(), changed); +} + +std::vector OverrideManager::ApplyForRecipient(CPlayerSlot slot) +{ + std::vector applied; + ApplyHealthMoneyTest(slot, applied); + ApplyHealth42Test(slot, applied); + for (const auto& rule : m_rules) + { + if (!rule.enabled || (rule.recipientSlot != -1 && rule.recipientSlot != slot.Get())) + continue; + uintptr_t entity = FindFirstEntityForTarget(rule); + if (!entity) + continue; + auto* address = ResolveAddress(entity, rule); + if (!address) + continue; + AppliedOverride saved {address, rule.type, ReadBits(address, rule.type)}; + WriteBits(address, rule.type, rule.bits); + applied.push_back(saved); + if (m_trace) + SPMessage("applied #%d for recipient %d at %p\n", rule.id, slot.Get(), address); + } + return applied; +} + +void OverrideManager::Restore(const std::vector& applied) +{ + for (auto it = applied.rbegin(); it != applied.rend(); ++it) + WriteBits(it->address, it->type, it->originalBits); +} diff --git a/src/overrides.h b/src/overrides.h new file mode 100644 index 0000000..adc0bce --- /dev/null +++ b/src/overrides.h @@ -0,0 +1,81 @@ +#pragma once + +#include "playerslot.h" + +#include +#include +#include + +enum class FieldType +{ + Bool, + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Int64, + UInt64, + Float, +}; + +struct FieldPathSegment +{ + std::string className; + std::string fieldName; +}; + +struct OverrideRule +{ + int id {}; + int recipientSlot {-1}; + std::string target; + std::string className; + std::string fieldName; + std::vector path; + FieldType type {FieldType::Int32}; + uint64_t bits {}; + bool enabled {true}; +}; + +struct AppliedOverride +{ + uint8_t* address {}; + FieldType type {}; + uint64_t originalBits {}; +}; + +class OverrideManager +{ +public: + void Clear(); + bool AddFromTokens(int argc, const char** argv, std::string& error); + bool AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error); + bool SetHealthMoneyTest(bool enabled, std::string& error); + bool SetHealth42Test(bool enabled, std::string& error); + void Dump() const; + void SetTrace(bool value) { m_trace = value; } + bool Trace() const { return m_trace; } + size_t RuleCount() const { return m_rules.size(); } + size_t DynamicRuleCount() const { return (m_healthMoneyTest ? 1 : 0) + (m_health42Test ? 1 : 0); } + bool Empty() const { return m_rules.empty() && !m_healthMoneyTest && !m_health42Test; } + + std::vector ApplyForRecipient(CPlayerSlot slot); + void Restore(const std::vector& applied); + +private: + bool EntityMatches(uintptr_t entity, const OverrideRule& rule) const; + uintptr_t FindFirstEntityForTarget(const OverrideRule& rule) const; + uint8_t* ResolveAddress(uintptr_t entity, const OverrideRule& rule) const; + void ApplyHealthMoneyTest(CPlayerSlot slot, std::vector& applied); + void ApplyHealth42Test(CPlayerSlot slot, std::vector& applied); + + std::vector m_rules; + int m_nextId {1}; + bool m_trace {false}; + bool m_healthMoneyTest {false}; + bool m_health42Test {false}; +}; + +extern OverrideManager g_overrides; diff --git a/src/plugin.cpp b/src/plugin.cpp new file mode 100644 index 0000000..760db44 --- /dev/null +++ b/src/plugin.cpp @@ -0,0 +1,264 @@ +#include "plugin.h" + +#include "common.h" +#include "detour.h" +#include "gameconfig.h" +#include "icvar.h" +#include "iserver.h" +#include "module.h" +#include "overrides.h" +#include "schema.h" +#include "schemasystem/schemasystem.h" +#include "interfaces/interfaces.h" +#include "entity2/entitysystem.h" + +#include "tier0/memdbgon.h" + +SendProxyPlugin g_SendProxy; +IVEngineServer2* g_engine = nullptr; +INetworkServerService* g_networkServerService = nullptr; +IServerGameClients* g_gameClients = nullptr; +ISchemaSystem* g_schemaSystem = nullptr; +IFileSystem* g_fileSystem = nullptr; +IGameResourceService* g_gameResourceService = nullptr; +CGameConfig* g_gameConfig = nullptr; +CGameEntitySystem* g_entitySystem = nullptr; +bool g_enabled = true; +bool g_armed = false; + +void SPMessage(const char* msg, ...) +{ + va_list args; + va_start(args, msg); + char buf[1024]; + V_vsnprintf(buf, sizeof(buf), msg, args); + ConColorMsg(Color(80, 200, 255, 255), "[SendProxy] %s", buf); + va_end(args); +} + +void SPWarning(const char* msg, ...) +{ + va_list args; + va_start(args, msg); + char buf[1024]; + V_vsnprintf(buf, sizeof(buf), msg, args); + Warning("[SendProxy] %s", buf); + va_end(args); +} + +CGameEntitySystem* GameEntitySystem() +{ + if (g_entitySystem) + return g_entitySystem; + if (!g_gameResourceService || !g_gameConfig) + return nullptr; + int offset = g_gameConfig->GetOffset("GameEntitySystem"); + if (offset < 0) + return nullptr; + g_entitySystem = *reinterpret_cast(reinterpret_cast(g_gameResourceService) + offset); + return g_entitySystem; +} + +CON_COMMAND_F(sp_enable, "Enable SendProxy overrides", FCVAR_NONE) +{ + if (args.ArgC() >= 2) + g_enabled = atoi(args[1]) != 0; + SPMessage("enabled=%d\n", g_enabled ? 1 : 0); +} + +CON_COMMAND_F(sp_arm, "Arm SendProxy override application after the map is fully loaded", FCVAR_NONE) +{ + if (args.ArgC() >= 2) + g_armed = atoi(args[1]) != 0; + SPMessage("armed=%d\n", g_armed ? 1 : 0); +} + +CON_COMMAND_F(sp_trace, "Enable SendProxy tracing", FCVAR_NONE) +{ + if (args.ArgC() >= 2) + g_overrides.SetTrace(atoi(args[1]) != 0); + SPMessage("trace=%d\n", g_overrides.Trace() ? 1 : 0); +} + +CON_COMMAND_F(sp_dump, "Dump SendProxy state", FCVAR_NONE) +{ + SPMessage("enabled=%d armed=%d entitySystem=%p\n", g_enabled ? 1 : 0, g_armed ? 1 : 0, GameEntitySystem()); + g_overrides.Dump(); +} + +CON_COMMAND_F(sp_clear, "Clear SendProxy rules", FCVAR_NONE) +{ + g_overrides.Clear(); + SPMessage("rules cleared\n"); +} + +CON_COMMAND_F(sp_set, "sp_set ", FCVAR_NONE) +{ + const char* argv[8] {}; + for (int i = 0; i < args.ArgC() && i < 8; ++i) + argv[i] = args[i]; + std::string error; + if (!g_overrides.AddFromTokens(args.ArgC(), argv, error)) + SPWarning("%s\n", error.c_str()); +} + +CON_COMMAND_F(sp_weapon_test, "sp_weapon_test ", FCVAR_NONE) +{ + if (args.ArgC() < 3) + { + SPWarning("usage: sp_weapon_test \n"); + return; + } + std::string error; + if (!g_overrides.AddWeaponItemDefRule(-1, args[1], static_cast(atoi(args[2])), error)) + SPWarning("%s\n", error.c_str()); +} + +static void SetHealthMoneyCommand(const CCommand& args) +{ + if (args.ArgC() < 2) + { + SPWarning("usage: sp_health_money <0|1>\n"); + return; + } + + bool enable = atoi(args[1]) != 0; + std::string error; + if (!g_overrides.SetHealthMoneyTest(enable, error)) + { + SPWarning("%s\n", error.c_str()); + return; + } + + if (enable) + { + g_enabled = true; + g_armed = true; + SPMessage("enabled=1 armed=1\n"); + } + else if (g_overrides.Empty()) + { + g_armed = false; + SPMessage("armed=0\n"); + } +} + +CON_COMMAND_F(sp_health_money, "sp_health_money <0|1>", FCVAR_NONE) +{ + SetHealthMoneyCommand(args); +} + +CON_COMMAND_F(sp_health_money_test, "sp_health_money_test <0|1>", FCVAR_NONE) +{ + SetHealthMoneyCommand(args); +} + +CON_COMMAND_F(sp_health_42, "sp_health_42 <0|1>", FCVAR_NONE) +{ + if (args.ArgC() < 2) + { + SPWarning("usage: sp_health_42 <0|1>\n"); + return; + } + + bool enable = atoi(args[1]) != 0; + std::string error; + if (!g_overrides.SetHealth42Test(enable, error)) + { + SPWarning("%s\n", error.c_str()); + return; + } + + if (enable) + { + g_enabled = true; + g_armed = true; + SPMessage("enabled=1 armed=1\n"); + } + else if (g_overrides.Empty()) + { + g_armed = false; + SPMessage("armed=0\n"); + } +} + +CON_COMMAND_F(sp_schema_fields, "sp_schema_fields [substring]", FCVAR_NONE) +{ + if (args.ArgC() < 2) + { + SPWarning("usage: sp_schema_fields [substring]\n"); + return; + } + schema::DumpFields(args[1], args.ArgC() >= 3 ? args[2] : ""); +} + +PLUGIN_EXPOSE(SendProxyPlugin, g_SendProxy); + +bool SendProxyPlugin::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late) +{ + PLUGIN_SAVEVARS(); + + ICvar* icvar = nullptr; + GET_V_IFACE_CURRENT(GetEngineFactory, g_engine, IVEngineServer2, SOURCE2ENGINETOSERVER_INTERFACE_VERSION); + GET_V_IFACE_ANY(GetEngineFactory, g_networkServerService, INetworkServerService, NETWORKSERVERSERVICE_INTERFACE_VERSION); + GET_V_IFACE_ANY(GetServerFactory, g_gameClients, IServerGameClients, SOURCE2GAMECLIENTS_INTERFACE_VERSION); + GET_V_IFACE_CURRENT(GetEngineFactory, g_schemaSystem, ISchemaSystem, SCHEMASYSTEM_INTERFACE_VERSION); + GET_V_IFACE_CURRENT(GetEngineFactory, g_gameResourceService, IGameResourceService, GAMERESOURCESERVICESERVER_INTERFACE_VERSION); + GET_V_IFACE_ANY(GetFileSystemFactory, g_fileSystem, IFileSystem, FILESYSTEM_INTERFACE_VERSION); + GET_V_IFACE_CURRENT(GetEngineFactory, icvar, ICvar, CVAR_INTERFACE_VERSION); + + modules::engine = new CModule(ROOTBIN, "engine2"); + modules::server = new CModule(GAMEBIN, "server"); + modules::schemasystem = new CModule(ROOTBIN, "schemasystem"); + + CBufferStringGrowable<256> gameDir; + g_engine->GetGameDir(gameDir); + std::string gameName = CGameConfig::DirectoryName(gameDir.Get()); + g_gameConfig = new CGameConfig(gameName, "addons/sendproxy/gamedata/sendproxy.games.txt"); + char confError[255] {}; + if (!g_gameConfig->Init(g_fileSystem, confError, sizeof(confError))) + { + V_snprintf(error, maxlen, "%s", confError); + return false; + } + + schema::Init(g_schemaSystem); + if (!InitSnapshotDetour(g_gameConfig)) + { + V_snprintf(error, maxlen, "failed to initialize snapshot detour"); + return false; + } + + g_SMAPI->AddListener(this, this); + g_pCVar = icvar; + META_CONVAR_REGISTER(FCVAR_RELEASE | FCVAR_CLIENT_CAN_EXECUTE | FCVAR_GAMEDLL); + + g_engine->ServerCommand("execifexists addons/sendproxy/configs/sendproxy.cfg\n"); + SPMessage("loaded\n"); + return true; +} + +bool SendProxyPlugin::Unload(char*, size_t) +{ + ShutdownSnapshotDetour(); + ConVar_Unregister(); + delete g_gameConfig; + delete modules::engine; + delete modules::server; + delete modules::schemasystem; + g_gameConfig = nullptr; + modules::engine = modules::server = modules::schemasystem = nullptr; + return true; +} + +void SendProxyPlugin::OnLevelInit(char const* map, char const*, char const*, char const*, bool, bool) +{ + g_armed = false; + SPMessage("level init %s\n", map ? map : ""); +} + +void SendProxyPlugin::OnLevelShutdown() +{ + g_armed = false; + g_entitySystem = nullptr; +} diff --git a/src/plugin.h b/src/plugin.h new file mode 100644 index 0000000..c78cec6 --- /dev/null +++ b/src/plugin.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +class SendProxyPlugin : public ISmmPlugin, public IMetamodListener +{ +public: + bool Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late) override; + bool Unload(char* error, size_t maxlen) override; + bool Pause(char*, size_t) override { return true; } + bool Unpause(char*, size_t) override { return true; } + void AllPluginsLoaded() override {} + void OnLevelInit(char const* map, char const*, char const*, char const*, bool, bool) override; + void OnLevelShutdown() override; + + const char* GetAuthor() override { return "OpenAI Codex"; } + const char* GetName() override { return "SendProxy"; } + const char* GetDescription() override { return "Per-recipient network field override experiments for CS2"; } + const char* GetURL() override { return ""; } + const char* GetLicense() override { return "MIT"; } + const char* GetVersion() override { return "0.1.0"; } + const char* GetDate() override { return __DATE__; } + const char* GetLogTag() override { return "SendProxy"; } +}; + +extern SendProxyPlugin g_SendProxy; +PLUGIN_GLOBALVARS(); + diff --git a/src/schema.cpp b/src/schema.cpp new file mode 100644 index 0000000..b887520 --- /dev/null +++ b/src/schema.cpp @@ -0,0 +1,94 @@ +#include "schema.h" + +#include "common.h" +#include "schemasystem/schemasystem.h" + +#include + +static ISchemaSystem* g_schema = nullptr; +static std::map g_cache; + +static bool IsNetworked(const SchemaClassFieldData_t& field) +{ + for (int i = 0; i < field.m_nStaticMetadataCount; ++i) + { + if (!V_strcmp(field.m_pStaticMetadata[i].m_pszName, "MNetworkEnable")) + return true; + } + return false; +} + +void schema::Init(ISchemaSystem* system) +{ + g_schema = system; + g_cache.clear(); +} + +SchemaField schema::FindField(const char* className, const char* fieldName) +{ + std::string key = std::string(className) + "::" + fieldName; + auto cached = g_cache.find(key); + if (cached != g_cache.end()) + return cached->second; + + SchemaField result {}; + auto* typeScope = g_schema ? g_schema->FindTypeScopeForModule("libserver.so") : nullptr; + if (!typeScope) + { + SPWarning("schema type scope for libserver.so is unavailable\n"); + g_cache[key] = result; + return result; + } + + SchemaClassInfoData_t* info = typeScope->FindDeclaredClass(className).Get(); + if (!info) + { + SPWarning("schema class %s not found\n", className); + g_cache[key] = result; + return result; + } + + 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.offset = field.m_nSingleInheritanceOffset; + result.networked = IsNetworked(field); + result.found = true; + g_cache[key] = result; + return result; + } + + SPWarning("schema field %s::%s not found\n", className, fieldName); + g_cache[key] = result; + return result; +} + +void schema::DumpFields(const char* className, const char* contains) +{ + auto* typeScope = g_schema ? g_schema->FindTypeScopeForModule("libserver.so") : nullptr; + if (!typeScope) + { + SPWarning("schema type scope for libserver.so is unavailable\n"); + return; + } + + SchemaClassInfoData_t* info = typeScope->FindDeclaredClass(className).Get(); + if (!info) + { + SPWarning("schema class %s not found\n", className); + return; + } + + int printed = 0; + for (int i = 0; i < info->m_nFieldCount; ++i) + { + const auto& field = info->m_pFields[i]; + if (contains && contains[0] && !V_stristr(field.m_pszName, contains)) + continue; + SPMessage("%s::%s offset=%d networked=%d\n", className, field.m_pszName, field.m_nSingleInheritanceOffset, IsNetworked(field) ? 1 : 0); + ++printed; + } + SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : ""); +} diff --git a/src/schema.h b/src/schema.h new file mode 100644 index 0000000..04f97e2 --- /dev/null +++ b/src/schema.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +class ISchemaSystem; + +struct SchemaField +{ + int32_t offset {}; + bool networked {}; + bool found {}; +}; + +namespace schema +{ + void Init(ISchemaSystem* system); + SchemaField FindField(const char* className, const char* fieldName); + void DumpFields(const char* className, const char* contains); +}