initial commit -- v1 code
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build/
|
||||
|
||||
110
AMBuildScript
Normal file
110
AMBuildScript
Normal 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
23
PackageScript
Normal 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
178
README.md
Normal 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
5
configs/sendproxy.cfg
Normal 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
18
configure.py
Normal 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()
|
||||
|
||||
102
examples/CounterStrikeSharp/SendProxyNative.cs
Normal file
102
examples/CounterStrikeSharp/SendProxyNative.cs
Normal 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);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CounterStrikeSharp.API" Version="1.0.369" />
|
||||
<Compile Include="..\SendProxyNative.cs" Link="SendProxyNative.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
43
gamedata/sendproxy.games.txt
Normal file
43
gamedata/sendproxy.games.txt
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
292
hl2sdk-manifests/SdkHelpers.ambuild
Normal file
292
hl2sdk-manifests/SdkHelpers.ambuild
Normal 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()
|
||||
57
hl2sdk-manifests/manifests/cs2.json
Normal file
57
hl2sdk-manifests/manifests/cs2.json
Normal 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
12
src/common.h
Normal 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
69
src/detour.cpp
Normal 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
9
src/detour.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "funchook.h"
|
||||
|
||||
class CGameConfig;
|
||||
|
||||
bool InitSnapshotDetour(CGameConfig* config);
|
||||
void ShutdownSnapshotDetour();
|
||||
|
||||
137
src/gameconfig.cpp
Normal file
137
src/gameconfig.cpp
Normal 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
35
src/gameconfig.h
Normal 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
119
src/module.cpp
Normal 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
51
src/module.h
Normal 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
463
src/overrides.cpp
Normal 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
81
src/overrides.h
Normal 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
264
src/plugin.cpp
Normal 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
29
src/plugin.h
Normal 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
94
src/schema.cpp
Normal 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
20
src/schema.h
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user