From b0e8588bef5951c10eaa8054ccabc1be704cb440 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Mon, 10 Aug 2026 14:22:03 +0200 Subject: [PATCH] remake - v2 --- src/detour.cpp | 72 +++++----- src/overrides.cpp | 354 +++++++++++++++++++++++++++++++++++++++++++++- src/overrides.h | 35 ++++- src/plugin.cpp | 94 ++++++++++++ src/schema.cpp | 15 +- src/schema.h | 2 + 6 files changed, 533 insertions(+), 39 deletions(-) diff --git a/src/detour.cpp b/src/detour.cpp index ba25dde..82b3833 100644 --- a/src/detour.cpp +++ b/src/detour.cpp @@ -36,6 +36,7 @@ static uint64_t g_packChanged = 0; static uint64_t g_packEntityCalls = 0; static uint64_t g_packEntitySpoofs = 0; static uint64_t g_packEntityScopedWrites = 0; +static uint64_t g_packEntityGenericSpoofs = 0; static uint64_t g_packEntitySelfSkips = 0; static uint64_t g_packEntityNoRecipient = 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; - if (!g_enabled || !g_armed || !entityData) - { - g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); - return; - } - - CEntityIdentity* identity = PlayerIdentityFromInstance(entityData); - if (!identity) + if (!g_enabled || !g_armed || !entityData || !g_overrides.HasPackedOverrides()) { g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); return; } CPlayerSlot recipient = g_currentRecipient; + auto applied = g_overrides.ApplyForPackedEntity(recipient, entityIndex, entityData); + g_packEntityGenericSpoofs += applied.size(); + + bool spoofedHealth = false; + int savedHealth = 0; + int* healthAddress = nullptr; int spoofHealth = 42; - if (!g_overrides.TryGetHealth42Value(recipient, spoofHealth)) + if (g_overrides.TryGetHealth42Value(recipient, spoofHealth)) { - g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); - return; + CEntityIdentity* identity = PlayerIdentityFromInstance(entityData); + if (identity) + { + if (recipient.Get() < 0) + ++g_packEntityNoRecipient; + else if (OwnerSlotForPawn(identity) == recipient.Get()) + { + ++g_packEntitySelfSkips; + } + else + { + auto healthField = schema::FindField("CBaseEntity", "m_iHealth"); + if (healthField.found && healthField.offset >= 0) + { + healthAddress = reinterpret_cast(reinterpret_cast(entityData) + healthField.offset); + savedHealth = *healthAddress; + *healthAddress = spoofHealth; + spoofedHealth = true; + ++g_packEntitySpoofs; + ++g_packEntityScopedWrites; + } + } + } } - if (recipient.Get() < 0) - ++g_packEntityNoRecipient; - else if (OwnerSlotForPawn(identity) == recipient.Get()) - { - ++g_packEntitySelfSkips; - g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); - return; - } - - 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); - return; - } - - auto* healthAddress = reinterpret_cast(reinterpret_cast(entityData) + healthField.offset); - int savedHealth = *healthAddress; - *healthAddress = spoofHealth; - - ++g_packEntitySpoofs; - ++g_packEntityScopedWrites; g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); - *healthAddress = savedHealth; + if (spoofedHealth) + *healthAddress = savedHealth; + g_overrides.Restore(applied); } static bool InitPackEntityDetour(CGameConfig* config) @@ -303,7 +304,7 @@ void ShutdownSnapshotDetour() 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(g_snapshotCalls), static_cast(g_snapshotApplied), static_cast(g_snapshotChanged), @@ -312,6 +313,7 @@ void DumpDetourStats() static_cast(g_packChanged), static_cast(g_packEntityCalls), static_cast(g_packEntitySpoofs), + static_cast(g_packEntityGenericSpoofs), static_cast(g_packEntityScopedWrites), static_cast(g_packEntitySelfSkips), static_cast(g_packEntityNoRecipient), diff --git a/src/overrides.cpp b/src/overrides.cpp index 9374143..b23e05a 100644 --- a/src/overrides.cpp +++ b/src/overrides.cpp @@ -8,6 +8,7 @@ #include #include +#include extern CGameEntitySystem* g_entitySystem; 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, "uint64")) { out = FieldType::UInt64; 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; } @@ -61,6 +66,9 @@ static uint64_t ReadBits(uint8_t* address, FieldType type) memcpy(&bits, address, sizeof(bits)); return bits; } + case FieldType::Bytes: + case FieldType::StringPtr: + return 0; } return 0; } @@ -83,6 +91,9 @@ static void WriteBits(uint8_t* address, FieldType type, uint64_t bits) memcpy(address, &narrowed, sizeof(narrowed)); 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::UInt64: return "uint64"; case FieldType::Float: return "float"; + case FieldType::Bytes: return "bytes"; + case FieldType::StringPtr: return "string"; } return "unknown"; } +static std::vector SplitString(const char* value, char separator) +{ + std::vector 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(value); + return true; +} + +static bool ParseBytes(const char* value, std::vector& 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(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 +static void AppendValueBytes(std::vector& out, T value) +{ + out.resize(sizeof(T)); + memcpy(out.data(), &value, sizeof(T)); +} + static bool DesignerContains(CEntityIdentity* identity, const char* needle) { 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() { + for (const auto& rule : m_compiledRules) + { + if (rule.enabled) + MarkEntityFieldDirtyByOffset(rule.entityIndex, rule.offset); + } m_rules.clear(); + m_compiledRules.clear(); + m_compiledRulesByEntity.clear(); } 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; } +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& 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(strtol(value, nullptr, 0))); return true; + case FieldType::UInt8: AppendValueBytes(out, static_cast(strtoul(value, nullptr, 0))); return true; + case FieldType::Int16: AppendValueBytes(out, static_cast(strtol(value, nullptr, 0))); return true; + case FieldType::UInt16: AppendValueBytes(out, static_cast(strtoul(value, nullptr, 0))); return true; + case FieldType::Int32: AppendValueBytes(out, static_cast(strtol(value, nullptr, 0))); return true; + case FieldType::UInt32: AppendValueBytes(out, static_cast(strtoul(value, nullptr, 0))); return true; + case FieldType::Int64: AppendValueBytes(out, static_cast(strtoll(value, nullptr, 0))); return true; + case FieldType::UInt64: AppendValueBytes(out, static_cast(strtoull(value, nullptr, 0))); return true; + case FieldType::Float: AppendValueBytes(out, static_cast(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(out.size()) > fieldSize) + { + error = "bytes value is larger than schema field"; + return false; + } + return true; + } + case FieldType::StringPtr: { + if (fieldSize > 0 && fieldSize != static_cast(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(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(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) { OverrideRule rule; @@ -345,6 +653,12 @@ void OverrideManager::Dump() const } 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)); + 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 @@ -545,8 +859,46 @@ std::vector OverrideManager::ApplyForRecipient(CPlayerSlot slot return applied; } +std::vector OverrideManager::ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData) +{ + std::vector 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(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& applied) { for (auto it = applied.rbegin(); it != applied.rend(); ++it) - WriteBits(it->address, it->type, it->originalBits); + { + if (!it->originalBytes.empty()) + memcpy(it->address, it->originalBytes.data(), it->originalBytes.size()); + else + WriteBits(it->address, it->type, it->originalBits); + } } diff --git a/src/overrides.h b/src/overrides.h index 7046acd..fc1fdc7 100644 --- a/src/overrides.h +++ b/src/overrides.h @@ -5,6 +5,7 @@ #include #include #include +#include #include enum class FieldType @@ -19,6 +20,8 @@ enum class FieldType Int64, UInt64, Float, + Bytes, + StringPtr, }; struct FieldPathSegment @@ -45,6 +48,22 @@ struct AppliedOverride uint8_t* address {}; FieldType type {}; uint64_t originalBits {}; + std::vector 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 value; + std::string stringValue; + bool enabled {true}; }; class OverrideManager @@ -52,6 +71,10 @@ class OverrideManager public: void Clear(); 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 SetHealthMoneyTest(bool enabled, std::string& error); bool SetHealth42Test(bool enabled, std::string& error); @@ -60,13 +83,15 @@ public: 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 RuleCount() const { return m_rules.size() + m_compiledRules.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; } + bool Empty() const { return m_rules.empty() && m_compiledRules.empty() && !m_healthMoneyTest && !m_health42Test; } bool Health42Test() const { return m_health42Test; } bool TryGetHealth42Value(CPlayerSlot recipient, int& value) const; + bool HasPackedOverrides() const { return !m_compiledRules.empty() || m_health42Test; } std::vector ApplyForRecipient(CPlayerSlot slot); + std::vector ApplyForPackedEntity(CPlayerSlot recipient, int entityIndex, void* entityData); std::vector ApplyGlobalHealth42(); int MarkPlayerHealthDirty(); void Restore(const std::vector& applied); @@ -76,8 +101,14 @@ private: uintptr_t FindFirstEntityForTarget(const OverrideRule& rule) const; uint8_t* ResolveAddress(uintptr_t entity, const OverrideRule& rule) const; void ApplyHealthMoneyTest(CPlayerSlot slot, std::vector& 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& out, std::string& stringValue, std::string& error) const; + void RebuildCompiledIndex(); + bool MarkEntityFieldDirtyByOffset(int entityIndex, int32_t offset); std::vector m_rules; + std::vector m_compiledRules; + std::unordered_map> m_compiledRulesByEntity; int m_nextId {1}; bool m_trace {false}; bool m_healthMoneyTest {false}; diff --git a/src/plugin.cpp b/src/plugin.cpp index d178973..85b645c 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -178,6 +178,63 @@ CON_COMMAND_F(sp_clear, "Clear SendProxy rules", FCVAR_NONE) SPMessage("rules cleared\n"); } +CON_COMMAND_F(sp_set_exact, "sp_set_exact ", FCVAR_NONE) +{ + if (args.ArgC() < 7) + { + SPWarning("usage: sp_set_exact \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 ", FCVAR_NONE) +{ + if (args.ArgC() < 2) + { + SPWarning("usage: sp_unset \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 ", FCVAR_NONE) +{ + if (args.ArgC() < 4) + { + SPWarning("usage: sp_dirty \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 ", FCVAR_NONE) { const char* argv[8] {}; @@ -188,6 +245,43 @@ CON_COMMAND_F(sp_set, "sp_set ", FCVAR_NONE) { if (args.ArgC() < 3) diff --git a/src/schema.cpp b/src/schema.cpp index b887520..bfb74d6 100644 --- a/src/schema.cpp +++ b/src/schema.cpp @@ -55,6 +55,14 @@ SchemaField schema::FindField(const char* className, const char* fieldName) continue; result.offset = field.m_nSingleInheritanceOffset; 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; g_cache[key] = result; return result; @@ -87,7 +95,12 @@ void schema::DumpFields(const char* className, const char* contains) 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); + 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; } SPMessage("%s fields printed=%d filter=%s\n", className, printed, contains && contains[0] ? contains : ""); diff --git a/src/schema.h b/src/schema.h index 04f97e2..5c652ea 100644 --- a/src/schema.h +++ b/src/schema.h @@ -8,8 +8,10 @@ class ISchemaSystem; struct SchemaField { int32_t offset {}; + int32_t size {}; bool networked {}; bool found {}; + std::string typeName; }; namespace schema