remake - v2
This commit is contained in:
@@ -36,6 +36,7 @@ static uint64_t g_packChanged = 0;
|
|||||||
static uint64_t g_packEntityCalls = 0;
|
static uint64_t g_packEntityCalls = 0;
|
||||||
static uint64_t g_packEntitySpoofs = 0;
|
static uint64_t g_packEntitySpoofs = 0;
|
||||||
static uint64_t g_packEntityScopedWrites = 0;
|
static uint64_t g_packEntityScopedWrites = 0;
|
||||||
|
static uint64_t g_packEntityGenericSpoofs = 0;
|
||||||
static uint64_t g_packEntitySelfSkips = 0;
|
static uint64_t g_packEntitySelfSkips = 0;
|
||||||
static uint64_t g_packEntityNoRecipient = 0;
|
static uint64_t g_packEntityNoRecipient = 0;
|
||||||
static uint64_t g_fullUpdateWrites = 0;
|
static uint64_t g_fullUpdateWrites = 0;
|
||||||
@@ -162,51 +163,51 @@ static void Detour_PackEntity(void* self, void* arg1, int entityIndex, void* ent
|
|||||||
{
|
{
|
||||||
++g_packEntityCalls;
|
++g_packEntityCalls;
|
||||||
|
|
||||||
if (!g_enabled || !g_armed || !entityData)
|
if (!g_enabled || !g_armed || !entityData || !g_overrides.HasPackedOverrides())
|
||||||
{
|
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CEntityIdentity* identity = PlayerIdentityFromInstance(entityData);
|
|
||||||
if (!identity)
|
|
||||||
{
|
{
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CPlayerSlot recipient = g_currentRecipient;
|
CPlayerSlot recipient = g_currentRecipient;
|
||||||
int spoofHealth = 42;
|
auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData);
|
||||||
if (!g_overrides.TryGetHealth42Value(recipient, spoofHealth))
|
g_packEntityGenericSpoofs += applied.size();
|
||||||
{
|
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
bool spoofedHealth = false;
|
||||||
|
int savedHealth = 0;
|
||||||
|
int* healthAddress = nullptr;
|
||||||
|
int spoofHealth = 42;
|
||||||
|
if (g_overrides.TryGetHealth42Value(recipient, spoofHealth))
|
||||||
|
{
|
||||||
|
CEntityIdentity* identity = PlayerIdentityFromInstance(entityData);
|
||||||
|
if (identity)
|
||||||
|
{
|
||||||
if (recipient.Get() < 0)
|
if (recipient.Get() < 0)
|
||||||
++g_packEntityNoRecipient;
|
++g_packEntityNoRecipient;
|
||||||
else if (OwnerSlotForPawn(identity) == recipient.Get())
|
else if (OwnerSlotForPawn(identity) == recipient.Get())
|
||||||
{
|
{
|
||||||
++g_packEntitySelfSkips;
|
++g_packEntitySelfSkips;
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
auto healthField = schema::FindField("CBaseEntity", "m_iHealth");
|
|
||||||
if (!healthField.found || healthField.offset < 0)
|
|
||||||
{
|
{
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
auto healthField = schema::FindField("CBaseEntity", "m_iHealth");
|
||||||
return;
|
if (healthField.found && healthField.offset >= 0)
|
||||||
}
|
{
|
||||||
|
healthAddress = reinterpret_cast<int*>(reinterpret_cast<uint8_t*>(entityData) + healthField.offset);
|
||||||
auto* healthAddress = reinterpret_cast<int*>(reinterpret_cast<uint8_t*>(entityData) + healthField.offset);
|
savedHealth = *healthAddress;
|
||||||
int savedHealth = *healthAddress;
|
|
||||||
*healthAddress = spoofHealth;
|
*healthAddress = spoofHealth;
|
||||||
|
spoofedHealth = true;
|
||||||
++g_packEntitySpoofs;
|
++g_packEntitySpoofs;
|
||||||
++g_packEntityScopedWrites;
|
++g_packEntityScopedWrites;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||||
|
if (spoofedHealth)
|
||||||
*healthAddress = savedHealth;
|
*healthAddress = savedHealth;
|
||||||
|
g_overrides.Restore(applied);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool InitPackEntityDetour(CGameConfig* config)
|
static bool InitPackEntityDetour(CGameConfig* config)
|
||||||
@@ -303,7 +304,7 @@ void ShutdownSnapshotDetour()
|
|||||||
|
|
||||||
void DumpDetourStats()
|
void DumpDetourStats()
|
||||||
{
|
{
|
||||||
SPMessage("hooks: snapshot calls=%llu applied=%llu changed=%llu; pack calls=%llu applied=%llu changed=%llu; pack_entity calls=%llu spoofs=%llu scoped_writes=%llu self_skips=%llu no_recipient=%llu; fullupdate writes=%llu budget=%d\n",
|
SPMessage("hooks: snapshot calls=%llu applied=%llu changed=%llu; pack calls=%llu applied=%llu changed=%llu; pack_entity calls=%llu health_spoofs=%llu generic_spoofs=%llu scoped_writes=%llu self_skips=%llu no_recipient=%llu; fullupdate writes=%llu budget=%d\n",
|
||||||
static_cast<unsigned long long>(g_snapshotCalls),
|
static_cast<unsigned long long>(g_snapshotCalls),
|
||||||
static_cast<unsigned long long>(g_snapshotApplied),
|
static_cast<unsigned long long>(g_snapshotApplied),
|
||||||
static_cast<unsigned long long>(g_snapshotChanged),
|
static_cast<unsigned long long>(g_snapshotChanged),
|
||||||
@@ -312,6 +313,7 @@ void DumpDetourStats()
|
|||||||
static_cast<unsigned long long>(g_packChanged),
|
static_cast<unsigned long long>(g_packChanged),
|
||||||
static_cast<unsigned long long>(g_packEntityCalls),
|
static_cast<unsigned long long>(g_packEntityCalls),
|
||||||
static_cast<unsigned long long>(g_packEntitySpoofs),
|
static_cast<unsigned long long>(g_packEntitySpoofs),
|
||||||
|
static_cast<unsigned long long>(g_packEntityGenericSpoofs),
|
||||||
static_cast<unsigned long long>(g_packEntityScopedWrites),
|
static_cast<unsigned long long>(g_packEntityScopedWrites),
|
||||||
static_cast<unsigned long long>(g_packEntitySelfSkips),
|
static_cast<unsigned long long>(g_packEntitySelfSkips),
|
||||||
static_cast<unsigned long long>(g_packEntityNoRecipient),
|
static_cast<unsigned long long>(g_packEntityNoRecipient),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
extern CGameEntitySystem* g_entitySystem;
|
extern CGameEntitySystem* g_entitySystem;
|
||||||
extern CGameEntitySystem* GameEntitySystem();
|
extern CGameEntitySystem* GameEntitySystem();
|
||||||
@@ -26,6 +27,10 @@ static bool ParseType(const char* s, FieldType& out)
|
|||||||
if (!V_stricmp(s, "int64")) { out = FieldType::Int64; 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, "uint64")) { out = FieldType::UInt64; return true; }
|
||||||
if (!V_stricmp(s, "float")) { out = FieldType::Float; return true; }
|
if (!V_stricmp(s, "float")) { out = FieldType::Float; return true; }
|
||||||
|
if (!V_stricmp(s, "handle") || !V_stricmp(s, "ehandle")) { out = FieldType::UInt32; return true; }
|
||||||
|
if (!V_stricmp(s, "color") || !V_stricmp(s, "rgba")) { out = FieldType::Bytes; return true; }
|
||||||
|
if (!V_stricmp(s, "bytes") || !V_stricmp(s, "raw")) { out = FieldType::Bytes; return true; }
|
||||||
|
if (!V_stricmp(s, "string") || !V_stricmp(s, "charptr")) { out = FieldType::StringPtr; return true; }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +66,9 @@ static uint64_t ReadBits(uint8_t* address, FieldType type)
|
|||||||
memcpy(&bits, address, sizeof(bits));
|
memcpy(&bits, address, sizeof(bits));
|
||||||
return bits;
|
return bits;
|
||||||
}
|
}
|
||||||
|
case FieldType::Bytes:
|
||||||
|
case FieldType::StringPtr:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -83,6 +91,9 @@ static void WriteBits(uint8_t* address, FieldType type, uint64_t bits)
|
|||||||
memcpy(address, &narrowed, sizeof(narrowed));
|
memcpy(address, &narrowed, sizeof(narrowed));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case FieldType::Bytes:
|
||||||
|
case FieldType::StringPtr:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,10 +111,87 @@ static const char* TypeName(FieldType type)
|
|||||||
case FieldType::Int64: return "int64";
|
case FieldType::Int64: return "int64";
|
||||||
case FieldType::UInt64: return "uint64";
|
case FieldType::UInt64: return "uint64";
|
||||||
case FieldType::Float: return "float";
|
case FieldType::Float: return "float";
|
||||||
|
case FieldType::Bytes: return "bytes";
|
||||||
|
case FieldType::StringPtr: return "string";
|
||||||
}
|
}
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> SplitString(const char* value, char separator)
|
||||||
|
{
|
||||||
|
std::vector<std::string> out;
|
||||||
|
if (!value)
|
||||||
|
return out;
|
||||||
|
|
||||||
|
const char* start = value;
|
||||||
|
for (const char* p = value; ; ++p)
|
||||||
|
{
|
||||||
|
if (*p != separator && *p != '\0')
|
||||||
|
continue;
|
||||||
|
out.emplace_back(start, p - start);
|
||||||
|
if (*p == '\0')
|
||||||
|
break;
|
||||||
|
start = p + 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseByteToken(const std::string& token, uint8_t& out)
|
||||||
|
{
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long value = strtoul(token.c_str(), &end, 0);
|
||||||
|
if (!end || *end || value > 0xFF)
|
||||||
|
return false;
|
||||||
|
out = static_cast<uint8_t>(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseBytes(const char* value, std::vector<uint8_t>& out)
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
if (!value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!V_strnicmp(value, "0x", 2))
|
||||||
|
{
|
||||||
|
const char* hex = value + 2;
|
||||||
|
size_t len = strlen(hex);
|
||||||
|
if ((len % 2) != 0)
|
||||||
|
return false;
|
||||||
|
out.reserve(len / 2);
|
||||||
|
for (size_t i = 0; i < len; i += 2)
|
||||||
|
{
|
||||||
|
char token[3] {hex[i], hex[i + 1], '\0'};
|
||||||
|
char* end = nullptr;
|
||||||
|
unsigned long byte = strtoul(token, &end, 16);
|
||||||
|
if (!end || *end || byte > 0xFF)
|
||||||
|
return false;
|
||||||
|
out.push_back(static_cast<uint8_t>(byte));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parts = SplitString(value, ',');
|
||||||
|
if (parts.empty())
|
||||||
|
return false;
|
||||||
|
out.reserve(parts.size());
|
||||||
|
for (const auto& part : parts)
|
||||||
|
{
|
||||||
|
uint8_t byte = 0;
|
||||||
|
if (!ParseByteToken(part, byte))
|
||||||
|
return false;
|
||||||
|
out.push_back(byte);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void AppendValueBytes(std::vector<uint8_t>& out, T value)
|
||||||
|
{
|
||||||
|
out.resize(sizeof(T));
|
||||||
|
memcpy(out.data(), &value, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
static bool DesignerContains(CEntityIdentity* identity, const char* needle)
|
static bool DesignerContains(CEntityIdentity* identity, const char* needle)
|
||||||
{
|
{
|
||||||
const char* designer = identity ? identity->m_designerName.String() : nullptr;
|
const char* designer = identity ? identity->m_designerName.String() : nullptr;
|
||||||
@@ -196,7 +284,14 @@ static bool ValidatePath(const OverrideRule& rule, std::string& error)
|
|||||||
|
|
||||||
void OverrideManager::Clear()
|
void OverrideManager::Clear()
|
||||||
{
|
{
|
||||||
|
for (const auto& rule : m_compiledRules)
|
||||||
|
{
|
||||||
|
if (rule.enabled)
|
||||||
|
MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset);
|
||||||
|
}
|
||||||
m_rules.clear();
|
m_rules.clear();
|
||||||
|
m_compiledRules.clear();
|
||||||
|
m_compiledRulesByEntity.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OverrideManager::AddFromTokens(int argc, const char** argv, std::string& error)
|
bool OverrideManager::AddFromTokens(int argc, const char** argv, std::string& error)
|
||||||
@@ -229,6 +324,219 @@ bool OverrideManager::AddFromTokens(int argc, const char** argv, std::string& er
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::string& typeName, std::string& error) const
|
||||||
|
{
|
||||||
|
if (!className || !className[0] || !fieldPath || !fieldPath[0])
|
||||||
|
{
|
||||||
|
error = "class and field path are required";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parts = SplitString(fieldPath, '.');
|
||||||
|
if (parts.empty())
|
||||||
|
{
|
||||||
|
error = "field path is empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = 0;
|
||||||
|
size = 0;
|
||||||
|
std::string currentClass = className;
|
||||||
|
for (size_t i = 0; i < parts.size(); ++i)
|
||||||
|
{
|
||||||
|
auto field = schema::FindField(currentClass.c_str(), parts[i].c_str());
|
||||||
|
if (!field.found)
|
||||||
|
{
|
||||||
|
error = "schema field not found: " + currentClass + "::" + parts[i];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += field.offset;
|
||||||
|
size = field.size;
|
||||||
|
typeName = field.typeName;
|
||||||
|
if (i + 1 < parts.size())
|
||||||
|
{
|
||||||
|
if (field.typeName.empty())
|
||||||
|
{
|
||||||
|
error = "intermediate field has no schema type name";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentClass = field.typeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::CompileValue(FieldType type, int32_t fieldSize, const char* value, std::vector<uint8_t>& out, std::string& stringValue, std::string& error) const
|
||||||
|
{
|
||||||
|
out.clear();
|
||||||
|
stringValue.clear();
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case FieldType::Bool: {
|
||||||
|
bool b = !V_stricmp(value, "true") || atoi(value) != 0;
|
||||||
|
AppendValueBytes(out, b);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case FieldType::Int8: AppendValueBytes(out, static_cast<int8_t>(strtol(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::UInt8: AppendValueBytes(out, static_cast<uint8_t>(strtoul(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::Int16: AppendValueBytes(out, static_cast<int16_t>(strtol(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::UInt16: AppendValueBytes(out, static_cast<uint16_t>(strtoul(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::Int32: AppendValueBytes(out, static_cast<int32_t>(strtol(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::UInt32: AppendValueBytes(out, static_cast<uint32_t>(strtoul(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::Int64: AppendValueBytes(out, static_cast<int64_t>(strtoll(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::UInt64: AppendValueBytes(out, static_cast<uint64_t>(strtoull(value, nullptr, 0))); return true;
|
||||||
|
case FieldType::Float: AppendValueBytes(out, static_cast<float>(atof(value))); return true;
|
||||||
|
case FieldType::Bytes: {
|
||||||
|
if (!ParseBytes(value, out))
|
||||||
|
{
|
||||||
|
error = "bytes value must be 0xAABBCC or comma-separated byte values";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (fieldSize > 0 && static_cast<int32_t>(out.size()) > fieldSize)
|
||||||
|
{
|
||||||
|
error = "bytes value is larger than schema field";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case FieldType::StringPtr: {
|
||||||
|
if (fieldSize > 0 && fieldSize != static_cast<int32_t>(sizeof(const char*)))
|
||||||
|
{
|
||||||
|
error = "string currently supports pointer-sized char* fields only; use bytes for fixed buffers";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
stringValue = value ? value : "";
|
||||||
|
const char* pointer = stringValue.c_str();
|
||||||
|
out.resize(sizeof(pointer));
|
||||||
|
memcpy(out.data(), &pointer, sizeof(pointer));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error = "unsupported type";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OverrideManager::RebuildCompiledIndex()
|
||||||
|
{
|
||||||
|
m_compiledRulesByEntity.clear();
|
||||||
|
for (size_t i = 0; i < m_compiledRules.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_compiledRules[i].type == FieldType::StringPtr)
|
||||||
|
{
|
||||||
|
const char* pointer = m_compiledRules[i].stringValue.c_str();
|
||||||
|
m_compiledRules[i].value.resize(sizeof(pointer));
|
||||||
|
memcpy(m_compiledRules[i].value.data(), &pointer, sizeof(pointer));
|
||||||
|
}
|
||||||
|
if (m_compiledRules[i].enabled)
|
||||||
|
m_compiledRulesByEntity[m_compiledRules[i].entityIndex].push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset)
|
||||||
|
{
|
||||||
|
auto* identity = FindIdentityByIndex(entityIndex);
|
||||||
|
if (!identity || !identity->m_pInstance)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
NetworkStateChangedData data(static_cast<uint32>(offset));
|
||||||
|
identity->m_pInstance->NetworkStateChanged(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int OverrideManager::AddCompiledRule(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const char* type, const char* value, std::string& error)
|
||||||
|
{
|
||||||
|
if (recipientSlot < -1 || recipientSlot >= 64)
|
||||||
|
{
|
||||||
|
error = "recipient slot must be all/-1 or 0..63";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (entityIndex < 0)
|
||||||
|
{
|
||||||
|
error = "entity index must be >= 0";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldType fieldType {};
|
||||||
|
if (!ParseType(type, fieldType))
|
||||||
|
{
|
||||||
|
error = "unknown type";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompiledOverrideRule rule;
|
||||||
|
rule.id = m_nextId++;
|
||||||
|
rule.recipientSlot = recipientSlot;
|
||||||
|
rule.entityIndex = entityIndex;
|
||||||
|
rule.className = className ? className : "";
|
||||||
|
rule.fieldPath = fieldPath ? fieldPath : "";
|
||||||
|
std::string schemaType;
|
||||||
|
if (!ResolveFieldPath(rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size, schemaType, error))
|
||||||
|
return 0;
|
||||||
|
rule.type = fieldType;
|
||||||
|
if (!CompileValue(fieldType, rule.size, value, rule.value, rule.stringValue, error))
|
||||||
|
return 0;
|
||||||
|
if (rule.size > 0 && fieldType != FieldType::Bytes && fieldType != FieldType::StringPtr && static_cast<int32_t>(rule.value.size()) > rule.size)
|
||||||
|
{
|
||||||
|
error = "value type is larger than schema field";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_compiledRules.push_back(rule);
|
||||||
|
RebuildCompiledIndex();
|
||||||
|
MarkEntityFieldDirtyByOffset(entityIndex, rule.offset);
|
||||||
|
SPMessage("compiled rule #%d: recipient=%d entity=%d %s::%s offset=%d size=%d schema_type=%s value_type=%s\n",
|
||||||
|
rule.id, rule.recipientSlot, rule.entityIndex, rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size,
|
||||||
|
schemaType.c_str(), TypeName(rule.type));
|
||||||
|
return rule.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::RemoveRule(int id)
|
||||||
|
{
|
||||||
|
for (auto& rule : m_compiledRules)
|
||||||
|
{
|
||||||
|
if (rule.id != id)
|
||||||
|
continue;
|
||||||
|
rule.enabled = false;
|
||||||
|
MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset);
|
||||||
|
RebuildCompiledIndex();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& rule : m_rules)
|
||||||
|
{
|
||||||
|
if (rule.id != id)
|
||||||
|
continue;
|
||||||
|
rule.enabled = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::MarkRuleDirty(int id)
|
||||||
|
{
|
||||||
|
for (const auto& rule : m_compiledRules)
|
||||||
|
{
|
||||||
|
if (rule.id == id && rule.enabled)
|
||||||
|
return MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OverrideManager::MarkEntityFieldDirty(int entityIndex, const char* className, const char* fieldPath, std::string& error)
|
||||||
|
{
|
||||||
|
int32_t offset = 0;
|
||||||
|
int32_t size = 0;
|
||||||
|
std::string typeName;
|
||||||
|
if (!ResolveFieldPath(className, fieldPath, offset, size, typeName, error))
|
||||||
|
return false;
|
||||||
|
return MarkEntityFieldDirtyByOffset(entityIndex, offset);
|
||||||
|
}
|
||||||
|
|
||||||
bool OverrideManager::AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error)
|
bool OverrideManager::AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error)
|
||||||
{
|
{
|
||||||
OverrideRule rule;
|
OverrideRule rule;
|
||||||
@@ -345,6 +653,12 @@ void OverrideManager::Dump() const
|
|||||||
}
|
}
|
||||||
for (const auto& rule : m_rules)
|
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));
|
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));
|
||||||
|
for (const auto& rule : m_compiledRules)
|
||||||
|
{
|
||||||
|
if (rule.enabled)
|
||||||
|
SPMessage("#%d recipient=%d entity=%d field=%s::%s offset=%d size=%d type=%s\n",
|
||||||
|
rule.id, rule.recipientSlot, rule.entityIndex, rule.className.c_str(), rule.fieldPath.c_str(), rule.offset, rule.size, TypeName(rule.type));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uintptr_t OverrideManager::FindFirstEntityForTarget(const OverrideRule& rule) const
|
uintptr_t OverrideManager::FindFirstEntityForTarget(const OverrideRule& rule) const
|
||||||
@@ -545,8 +859,46 @@ std::vector<AppliedOverride> OverrideManager::ApplyForRecipient(CPlayerSlot slot
|
|||||||
return applied;
|
return applied;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<AppliedOverride> OverrideManager::ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData)
|
||||||
|
{
|
||||||
|
std::vector<AppliedOverride> applied;
|
||||||
|
if (!entityData || m_compiledRulesByEntity.empty())
|
||||||
|
return applied;
|
||||||
|
|
||||||
|
auto found = m_compiledRulesByEntity.find(entityIndex);
|
||||||
|
if (found == m_compiledRulesByEntity.end())
|
||||||
|
return applied;
|
||||||
|
|
||||||
|
for (size_t ruleIndex : found->second)
|
||||||
|
{
|
||||||
|
const auto& rule = m_compiledRules[ruleIndex];
|
||||||
|
if (!rule.enabled || (rule.recipientSlot != -1 && rule.recipientSlot != recipient.Get()))
|
||||||
|
continue;
|
||||||
|
if (rule.value.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto* address = reinterpret_cast<uint8_t*>(entityData) + rule.offset;
|
||||||
|
AppliedOverride saved {};
|
||||||
|
saved.address = address;
|
||||||
|
saved.type = rule.type;
|
||||||
|
saved.originalBytes.resize(rule.value.size());
|
||||||
|
memcpy(saved.originalBytes.data(), address, saved.originalBytes.size());
|
||||||
|
memcpy(address, rule.value.data(), rule.value.size());
|
||||||
|
applied.push_back(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_trace && !applied.empty())
|
||||||
|
SPMessage("packed entity overrides recipient=%d entity=%d changed=%zu\n", recipient.Get(), entityIndex, applied.size());
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
void OverrideManager::Restore(const std::vector<AppliedOverride>& applied)
|
void OverrideManager::Restore(const std::vector<AppliedOverride>& applied)
|
||||||
{
|
{
|
||||||
for (auto it = applied.rbegin(); it != applied.rend(); ++it)
|
for (auto it = applied.rbegin(); it != applied.rend(); ++it)
|
||||||
|
{
|
||||||
|
if (!it->originalBytes.empty())
|
||||||
|
memcpy(it->address, it->originalBytes.data(), it->originalBytes.size());
|
||||||
|
else
|
||||||
WriteBits(it->address, it->type, it->originalBits);
|
WriteBits(it->address, it->type, it->originalBits);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
enum class FieldType
|
enum class FieldType
|
||||||
@@ -19,6 +20,8 @@ enum class FieldType
|
|||||||
Int64,
|
Int64,
|
||||||
UInt64,
|
UInt64,
|
||||||
Float,
|
Float,
|
||||||
|
Bytes,
|
||||||
|
StringPtr,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FieldPathSegment
|
struct FieldPathSegment
|
||||||
@@ -45,6 +48,22 @@ struct AppliedOverride
|
|||||||
uint8_t* address {};
|
uint8_t* address {};
|
||||||
FieldType type {};
|
FieldType type {};
|
||||||
uint64_t originalBits {};
|
uint64_t originalBits {};
|
||||||
|
std::vector<uint8_t> originalBytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CompiledOverrideRule
|
||||||
|
{
|
||||||
|
int id {};
|
||||||
|
int recipientSlot {-1};
|
||||||
|
int entityIndex {-1};
|
||||||
|
std::string className;
|
||||||
|
std::string fieldPath;
|
||||||
|
int32_t offset {};
|
||||||
|
int32_t size {};
|
||||||
|
FieldType type {FieldType::Int32};
|
||||||
|
std::vector<uint8_t> value;
|
||||||
|
std::string stringValue;
|
||||||
|
bool enabled {true};
|
||||||
};
|
};
|
||||||
|
|
||||||
class OverrideManager
|
class OverrideManager
|
||||||
@@ -52,6 +71,10 @@ class OverrideManager
|
|||||||
public:
|
public:
|
||||||
void Clear();
|
void Clear();
|
||||||
bool AddFromTokens(int argc, const char** argv, std::string& error);
|
bool AddFromTokens(int argc, const char** argv, std::string& error);
|
||||||
|
int AddCompiledRule(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const char* type, const char* value, std::string& error);
|
||||||
|
bool RemoveRule(int id);
|
||||||
|
bool MarkRuleDirty(int id);
|
||||||
|
bool MarkEntityFieldDirty(int entityIndex, const char* className, const char* fieldPath, std::string& error);
|
||||||
bool AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error);
|
bool AddWeaponItemDefRule(int recipientSlot, const char* target, uint16_t itemDefinitionIndex, std::string& error);
|
||||||
bool SetHealthMoneyTest(bool enabled, std::string& error);
|
bool SetHealthMoneyTest(bool enabled, std::string& error);
|
||||||
bool SetHealth42Test(bool enabled, std::string& error);
|
bool SetHealth42Test(bool enabled, std::string& error);
|
||||||
@@ -60,13 +83,15 @@ public:
|
|||||||
void Dump() const;
|
void Dump() const;
|
||||||
void SetTrace(bool value) { m_trace = value; }
|
void SetTrace(bool value) { m_trace = value; }
|
||||||
bool Trace() const { return m_trace; }
|
bool Trace() const { return m_trace; }
|
||||||
size_t RuleCount() const { return m_rules.size(); }
|
size_t RuleCount() const { return m_rules.size() + m_compiledRules.size(); }
|
||||||
size_t DynamicRuleCount() const { return (m_healthMoneyTest ? 1 : 0) + (m_health42Test ? 1 : 0); }
|
size_t DynamicRuleCount() const { return (m_healthMoneyTest ? 1 : 0) + (m_health42Test ? 1 : 0); }
|
||||||
bool Empty() const { return m_rules.empty() && !m_healthMoneyTest && !m_health42Test; }
|
bool Empty() const { return m_rules.empty() && m_compiledRules.empty() && !m_healthMoneyTest && !m_health42Test; }
|
||||||
bool Health42Test() const { return m_health42Test; }
|
bool Health42Test() const { return m_health42Test; }
|
||||||
bool TryGetHealth42Value(CPlayerSlot recipient, int& value) const;
|
bool TryGetHealth42Value(CPlayerSlot recipient, int& value) const;
|
||||||
|
bool HasPackedOverrides() const { return !m_compiledRules.empty() || m_health42Test; }
|
||||||
|
|
||||||
std::vector<AppliedOverride> ApplyForRecipient(CPlayerSlot slot);
|
std::vector<AppliedOverride> ApplyForRecipient(CPlayerSlot slot);
|
||||||
|
std::vector<AppliedOverride> ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData);
|
||||||
std::vector<AppliedOverride> ApplyGlobalHealth42();
|
std::vector<AppliedOverride> ApplyGlobalHealth42();
|
||||||
int MarkPlayerHealthDirty();
|
int MarkPlayerHealthDirty();
|
||||||
void Restore(const std::vector<AppliedOverride>& applied);
|
void Restore(const std::vector<AppliedOverride>& applied);
|
||||||
@@ -76,8 +101,14 @@ private:
|
|||||||
uintptr_t FindFirstEntityForTarget(const OverrideRule& rule) const;
|
uintptr_t FindFirstEntityForTarget(const OverrideRule& rule) const;
|
||||||
uint8_t* ResolveAddress(uintptr_t entity, const OverrideRule& rule) const;
|
uint8_t* ResolveAddress(uintptr_t entity, const OverrideRule& rule) const;
|
||||||
void ApplyHealthMoneyTest(CPlayerSlot slot, std::vector<AppliedOverride>& applied);
|
void ApplyHealthMoneyTest(CPlayerSlot slot, std::vector<AppliedOverride>& applied);
|
||||||
|
bool ResolveFieldPath(const char* className, const char* fieldPath, int32_t& offset, int32_t& size, std::string& typeName, std::string& error) const;
|
||||||
|
bool CompileValue(FieldType type, int32_t fieldSize, const char* value, std::vector<uint8_t>& out, std::string& stringValue, std::string& error) const;
|
||||||
|
void RebuildCompiledIndex();
|
||||||
|
bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset);
|
||||||
|
|
||||||
std::vector<OverrideRule> m_rules;
|
std::vector<OverrideRule> m_rules;
|
||||||
|
std::vector<CompiledOverrideRule> m_compiledRules;
|
||||||
|
std::unordered_map<int, std::vector<size_t>> m_compiledRulesByEntity;
|
||||||
int m_nextId {1};
|
int m_nextId {1};
|
||||||
bool m_trace {false};
|
bool m_trace {false};
|
||||||
bool m_healthMoneyTest {false};
|
bool m_healthMoneyTest {false};
|
||||||
|
|||||||
@@ -178,6 +178,63 @@ CON_COMMAND_F(sp_clear, "Clear SendProxy rules", FCVAR_NONE)
|
|||||||
SPMessage("rules cleared\n");
|
SPMessage("rules cleared\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CON_COMMAND_F(sp_set_exact, "sp_set_exact <recipient|all> <entity_index> <ClassName> <field.path> <type> <value>", FCVAR_NONE)
|
||||||
|
{
|
||||||
|
if (args.ArgC() < 7)
|
||||||
|
{
|
||||||
|
SPWarning("usage: sp_set_exact <recipient|all> <entity_index> <ClassName> <field.path> <type> <value>\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int recipientSlot = !V_stricmp(args[1], "all") ? -1 : atoi(args[1]);
|
||||||
|
int entityIndex = atoi(args[2]);
|
||||||
|
std::string error;
|
||||||
|
int id = g_overrides.AddCompiledRule(recipientSlot, entityIndex, args[3], args[4], args[5], args[6], error);
|
||||||
|
if (!id)
|
||||||
|
{
|
||||||
|
SPWarning("%s\n", error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_enabled = true;
|
||||||
|
g_armed = true;
|
||||||
|
SPMessage("enabled=1 armed=1 rule_id=%d\n", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
CON_COMMAND_F(sp_unset, "sp_unset <rule_id>", FCVAR_NONE)
|
||||||
|
{
|
||||||
|
if (args.ArgC() < 2)
|
||||||
|
{
|
||||||
|
SPWarning("usage: sp_unset <rule_id>\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = atoi(args[1]);
|
||||||
|
if (!g_overrides.RemoveRule(id))
|
||||||
|
{
|
||||||
|
SPWarning("rule %d not found\n", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SPMessage("rule %d removed\n", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
CON_COMMAND_F(sp_dirty, "sp_dirty <entity_index> <ClassName> <field.path>", FCVAR_NONE)
|
||||||
|
{
|
||||||
|
if (args.ArgC() < 4)
|
||||||
|
{
|
||||||
|
SPWarning("usage: sp_dirty <entity_index> <ClassName> <field.path>\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
if (!g_overrides.MarkEntityFieldDirty(atoi(args[1]), args[2], args[3], error))
|
||||||
|
{
|
||||||
|
SPWarning("%s\n", error.empty() ? "failed to mark field dirty" : error.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SPMessage("marked entity %d %s::%s dirty\n", atoi(args[1]), args[2], args[3]);
|
||||||
|
}
|
||||||
|
|
||||||
CON_COMMAND_F(sp_set, "sp_set <recipient|all> <entity|bot|all> <ClassName> <field> <type> <value>", FCVAR_NONE)
|
CON_COMMAND_F(sp_set, "sp_set <recipient|all> <entity|bot|all> <ClassName> <field> <type> <value>", FCVAR_NONE)
|
||||||
{
|
{
|
||||||
const char* argv[8] {};
|
const char* argv[8] {};
|
||||||
@@ -188,6 +245,43 @@ CON_COMMAND_F(sp_set, "sp_set <recipient|all> <entity|bot|all> <ClassName> <fiel
|
|||||||
SPWarning("%s\n", error.c_str());
|
SPWarning("%s\n", error.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default"))) int SendProxy_SetOverride(int recipientSlot, int entityIndex, const char* className, const char* fieldPath, const char* type, const char* value)
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
int id = g_overrides.AddCompiledRule(recipientSlot, entityIndex, className, fieldPath, type, value, error);
|
||||||
|
if (!id)
|
||||||
|
{
|
||||||
|
SPWarning("SendProxy_SetOverride failed: %s\n", error.c_str());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_enabled = true;
|
||||||
|
g_armed = true;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default"))) bool SendProxy_RemoveOverride(int ruleId)
|
||||||
|
{
|
||||||
|
return g_overrides.RemoveRule(ruleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default"))) void SendProxy_ClearOverrides()
|
||||||
|
{
|
||||||
|
g_overrides.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __attribute__((visibility("default"))) bool SendProxy_MarkDirty(int entityIndex, const char* className, const char* fieldPath)
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
if (!g_overrides.MarkEntityFieldDirty(entityIndex, className, fieldPath, error))
|
||||||
|
{
|
||||||
|
if (!error.empty())
|
||||||
|
SPWarning("SendProxy_MarkDirty failed: %s\n", error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
CON_COMMAND_F(sp_weapon_test, "sp_weapon_test <weapon_entity_substring|all> <item_definition_index>", FCVAR_NONE)
|
CON_COMMAND_F(sp_weapon_test, "sp_weapon_test <weapon_entity_substring|all> <item_definition_index>", FCVAR_NONE)
|
||||||
{
|
{
|
||||||
if (args.ArgC() < 3)
|
if (args.ArgC() < 3)
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ SchemaField schema::FindField(const char* className, const char* fieldName)
|
|||||||
continue;
|
continue;
|
||||||
result.offset = field.m_nSingleInheritanceOffset;
|
result.offset = field.m_nSingleInheritanceOffset;
|
||||||
result.networked = IsNetworked(field);
|
result.networked = IsNetworked(field);
|
||||||
|
if (field.m_pType)
|
||||||
|
{
|
||||||
|
int size = 0;
|
||||||
|
uint8 alignment = 0;
|
||||||
|
if (field.m_pType->GetSizeAndAlignment(size, alignment))
|
||||||
|
result.size = size;
|
||||||
|
result.typeName = field.m_pType->m_sTypeName.String();
|
||||||
|
}
|
||||||
result.found = true;
|
result.found = true;
|
||||||
g_cache[key] = result;
|
g_cache[key] = result;
|
||||||
return result;
|
return result;
|
||||||
@@ -87,7 +95,12 @@ void schema::DumpFields(const char* className, const char* contains)
|
|||||||
const auto& field = info->m_pFields[i];
|
const auto& field = info->m_pFields[i];
|
||||||
if (contains && contains[0] && !V_stristr(field.m_pszName, contains))
|
if (contains && contains[0] && !V_stristr(field.m_pszName, contains))
|
||||||
continue;
|
continue;
|
||||||
SPMessage("%s::%s offset=%d networked=%d\n", className, field.m_pszName, field.m_nSingleInheritanceOffset, IsNetworked(field) ? 1 : 0);
|
const char* typeName = field.m_pType ? field.m_pType->m_sTypeName.String() : "";
|
||||||
|
int size = 0;
|
||||||
|
uint8 alignment = 0;
|
||||||
|
if (field.m_pType)
|
||||||
|
field.m_pType->GetSizeAndAlignment(size, alignment);
|
||||||
|
SPMessage("%s::%s offset=%d size=%d type=%s networked=%d\n", className, field.m_pszName, field.m_nSingleInheritanceOffset, size, typeName ? typeName : "", IsNetworked(field) ? 1 : 0);
|
||||||
++printed;
|
++printed;
|
||||||
}
|
}
|
||||||
SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : "<none>");
|
SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : "<none>");
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ class ISchemaSystem;
|
|||||||
struct SchemaField
|
struct SchemaField
|
||||||
{
|
{
|
||||||
int32_t offset {};
|
int32_t offset {};
|
||||||
|
int32_t size {};
|
||||||
bool networked {};
|
bool networked {};
|
||||||
bool found {};
|
bool found {};
|
||||||
|
std::string typeName;
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace schema
|
namespace schema
|
||||||
|
|||||||
Reference in New Issue
Block a user