initial commit -- v1 code

This commit is contained in:
2026-08-10 14:21:07 +02:00
commit 1ecb7f777e
25 changed files with 2472 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/

110
AMBuildScript Normal file
View File

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

23
PackageScript Normal file
View File

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

178
README.md Normal file
View File

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

5
configs/sendproxy.cfg Normal file
View File

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

18
configure.py Normal file
View File

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

View File

@@ -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<SetOverrideDelegate>("SendProxy_SetOverride");
_removeOverride = GetExport<RemoveOverrideDelegate>("SendProxy_RemoveOverride");
_clearOverrides = GetExport<ClearOverridesDelegate>("SendProxy_ClearOverrides");
_markDirty = GetExport<MarkDirtyDelegate>("SendProxy_MarkDirty");
}
public int SetBytes(int recipientSlot, int entityIndex, string className, string fieldPath, ReadOnlySpan<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<byte> 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<T>(string name) where T : Delegate
{
return Marshal.GetDelegateForFunctionPointer<T>(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);
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
<Compile Include="..\SendProxyNative.cs" Link="SendProxyNative.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,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<RuleKey, int> _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<Listeners.OnEntityCreated>(_ => Server.NextFrame(SyncRules));
RegisterListener<Listeners.OnEntityDeleted>(_ => 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<RuleKey, RuleValue>();
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<RuleKey, RuleValue> desired, IReadOnlyCollection<CCSPlayerController> 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<RuleKey, RuleValue> desired,
int recipientSlot,
int entityIndex,
string className,
string fieldPath,
RuleValue value)
{
desired[new RuleKey(recipientSlot, entityIndex, className, fieldPath)] = value;
}
private IEnumerable<CBasePlayerWeapon> EnumerateWeapons()
{
foreach (var entity in Utilities.GetAllEntities())
{
if (!entity.IsValid || !entity.DesignerName.StartsWith("weapon_", StringComparison.Ordinal))
continue;
CBasePlayerWeapon weapon = entity.As<CBasePlayerWeapon>();
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 });
}
}
}

View File

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

View File

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

View File

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

12
src/common.h Normal file
View File

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

69
src/detour.cpp Normal file
View File

@@ -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<CPlayerSlot*>(reinterpret_cast<uint8_t*>(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<SendSnapshotFn>(target);
g_hook = funchook_create();
if (!g_hook)
return false;
if (funchook_prepare(g_hook, reinterpret_cast<void**>(&g_sendSnapshot), reinterpret_cast<void*>(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;
}

9
src/detour.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
#include "funchook.h"
class CGameConfig;
bool InitSnapshotDetour(CGameConfig* config);
void ShutdownSnapshotDetour();

137
src/gameconfig.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include "gameconfig.h"
#include "common.h"
#include "module.h"
#include <cstdio>
#include <cstring>
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<uint8_t>& 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<uint8_t>(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<uint8_t> 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);
}

35
src/gameconfig.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include "KeyValues.h"
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
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<uint8_t>& out);
std::string m_gameDir;
std::string m_path;
KeyValues* m_kv {};
std::unordered_map<std::string, std::string> m_signatures;
std::unordered_map<std::string, std::string> m_libraries;
std::unordered_map<std::string, int> m_offsets;
};

119
src/module.cpp Normal file
View File

@@ -0,0 +1,119 @@
#include "module.h"
#include "dbg.h"
#include "filesystem.h"
#include "strtools.h"
#include <elf.h>
#include <fcntl.h>
#include <link.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
namespace modules
{
CModule* engine = nullptr;
CModule* server = nullptr;
CModule* schemasystem = nullptr;
}
static bool GetModuleInformation(void* handle, void** base, size_t* length, std::vector<Section>& 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<ElfW(Ehdr)*>(map);
auto* shdrs = reinterpret_cast<ElfW(Shdr)*>(reinterpret_cast<uintptr_t>(ehdr) + ehdr->e_shoff);
const char* strTab = reinterpret_cast<const char*>(reinterpret_cast<uintptr_t>(ehdr) + shdrs[ehdr->e_shstrndx].sh_offset);
for (int i = 0; i < ehdr->e_phnum; ++i)
{
auto* phdr = reinterpret_cast<ElfW(Phdr)*>(reinterpret_cast<uintptr_t>(ehdr) + ehdr->e_phoff + i * ehdr->e_phentsize);
if (phdr->p_type == PT_LOAD && (phdr->p_flags & PF_X))
{
*base = reinterpret_cast<void*>(lmap->l_addr + phdr->p_vaddr);
*length = phdr->p_filesz;
break;
}
}
for (int i = 0; i < ehdr->e_shnum; ++i)
{
auto* shdr = reinterpret_cast<ElfW(Shdr)*>(reinterpret_cast<uintptr_t>(shdrs) + i * ehdr->e_shentsize);
if (*(strTab + shdr->sh_name) == '\0')
continue;
sections.push_back({strTab + shdr->sh_name, reinterpret_cast<void*>(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<uint8_t*>(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<CreateInterfaceFn>(dlsym(m_handle, "CreateInterface"));
if (!fn)
return nullptr;
return fn(name, nullptr);
}

51
src/module.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#include "common.h"
#include "interface.h"
#include <cstddef>
#include <cstdint>
#include <dlfcn.h>
#include <string>
#include <string_view>
#include <vector>
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<Section> m_sections;
std::string m_name;
};
namespace modules
{
extern CModule* engine;
extern CModule* server;
extern CModule* schemasystem;
}

463
src/overrides.cpp Normal file
View File

@@ -0,0 +1,463 @@
#include "overrides.h"
#include "common.h"
#include "schema.h"
#include "entity2/entitysystem.h"
#include "entityhandle.h"
#include <cstdlib>
#include <cstring>
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<float>(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<bool*>(address) ? 1 : 0;
case FieldType::Int8: return static_cast<uint8_t>(*reinterpret_cast<int8_t*>(address));
case FieldType::UInt8: return *reinterpret_cast<uint8_t*>(address);
case FieldType::Int16: return static_cast<uint16_t>(*reinterpret_cast<int16_t*>(address));
case FieldType::UInt16: return *reinterpret_cast<uint16_t*>(address);
case FieldType::Int32: return static_cast<uint32_t>(*reinterpret_cast<int32_t*>(address));
case FieldType::UInt32: return *reinterpret_cast<uint32_t*>(address);
case FieldType::Int64: return static_cast<uint64_t>(*reinterpret_cast<int64_t*>(address));
case FieldType::UInt64: return *reinterpret_cast<uint64_t*>(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<bool*>(address) = bits != 0; break;
case FieldType::Int8: *reinterpret_cast<int8_t*>(address) = static_cast<int8_t>(bits); break;
case FieldType::UInt8: *reinterpret_cast<uint8_t*>(address) = static_cast<uint8_t>(bits); break;
case FieldType::Int16: *reinterpret_cast<int16_t*>(address) = static_cast<int16_t>(bits); break;
case FieldType::UInt16: *reinterpret_cast<uint16_t*>(address) = static_cast<uint16_t>(bits); break;
case FieldType::Int32: *reinterpret_cast<int32_t*>(address) = static_cast<int32_t>(bits); break;
case FieldType::UInt32: *reinterpret_cast<uint32_t*>(address) = static_cast<uint32_t>(bits); break;
case FieldType::Int64: *reinterpret_cast<int64_t*>(address) = static_cast<int64_t>(bits); break;
case FieldType::UInt64: *reinterpret_cast<uint64_t*>(address) = bits; break;
case FieldType::Float: {
uint32_t narrowed = static_cast<uint32_t>(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<uintptr_t>(identity->m_pInstance);
int controllerSlot = *reinterpret_cast<int*>(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<uint8_t*>(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 <recipient|all> <entity|bot|all> <ClassName> <field> <type> <value>";
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<uintptr_t>(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<uint8_t*>(address);
}
void OverrideManager::ApplyHealthMoneyTest(CPlayerSlot slot, std::vector<AppliedOverride>& 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<uintptr_t>(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<uintptr_t*>(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<int>(ReadBits(moneyAddress, FieldType::Int32));
int team = static_cast<int>(*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<CEntityHandle*>(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<uintptr_t>(identity->m_pInstance);
auto* targetTeamAddress = FieldAddress(entity, "CBaseEntity", "m_iTeamNum");
auto* healthAddress = FieldAddress(entity, "CBaseEntity", "m_iHealth");
if (!targetTeamAddress || !healthAddress)
continue;
if (static_cast<int>(*targetTeamAddress) != team)
continue;
AppliedOverride saved {healthAddress, FieldType::Int32, ReadBits(healthAddress, FieldType::Int32)};
WriteBits(healthAddress, FieldType::Int32, static_cast<uint32_t>(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<AppliedOverride>& 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<uintptr_t>(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<AppliedOverride> OverrideManager::ApplyForRecipient(CPlayerSlot slot)
{
std::vector<AppliedOverride> 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<AppliedOverride>& applied)
{
for (auto it = applied.rbegin(); it != applied.rend(); ++it)
WriteBits(it->address, it->type, it->originalBits);
}

81
src/overrides.h Normal file
View File

@@ -0,0 +1,81 @@
#pragma once
#include "playerslot.h"
#include <cstdint>
#include <string>
#include <vector>
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<FieldPathSegment> 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<AppliedOverride> ApplyForRecipient(CPlayerSlot slot);
void Restore(const std::vector<AppliedOverride>& 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<AppliedOverride>& applied);
void ApplyHealth42Test(CPlayerSlot slot, std::vector<AppliedOverride>& applied);
std::vector<OverrideRule> m_rules;
int m_nextId {1};
bool m_trace {false};
bool m_healthMoneyTest {false};
bool m_health42Test {false};
};
extern OverrideManager g_overrides;

264
src/plugin.cpp Normal file
View File

@@ -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<CGameEntitySystem**>(reinterpret_cast<uint8_t*>(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 <recipient|all> <entity|bot|all> <ClassName> <field> <type> <value>", 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 <weapon_entity_substring|all> <item_definition_index>", FCVAR_NONE)
{
if (args.ArgC() < 3)
{
SPWarning("usage: sp_weapon_test <weapon_entity_substring|all> <item_definition_index>\n");
return;
}
std::string error;
if (!g_overrides.AddWeaponItemDefRule(-1, args[1], static_cast<uint16_t>(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 <ClassName> [substring]", FCVAR_NONE)
{
if (args.ArgC() < 2)
{
SPWarning("usage: sp_schema_fields <ClassName> [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;
}

29
src/plugin.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <ISmmPlugin.h>
#include <igameevents.h>
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();

94
src/schema.cpp Normal file
View File

@@ -0,0 +1,94 @@
#include "schema.h"
#include "common.h"
#include "schemasystem/schemasystem.h"
#include <map>
static ISchemaSystem* g_schema = nullptr;
static std::map<std::string, SchemaField> 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 : "<none>");
}

20
src/schema.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <string>
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);
}