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

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