diff --git a/src/detour.cpp b/src/detour.cpp index c8ebaac..ba25dde 100644 --- a/src/detour.cpp +++ b/src/detour.cpp @@ -1,17 +1,46 @@ #include "detour.h" #include "common.h" +#include "entity2/entitysystem.h" +#include "entity2/entityinstance.h" +#include "entityhandle.h" #include "gameconfig.h" +#include "networksystem/inetworkserializer.h" #include "overrides.h" #include "playerslot.h" +#include "schema.h" + +#include extern CGameConfig* g_gameConfig; +extern CGameEntitySystem* g_entitySystem; extern bool g_enabled; extern bool g_armed; +extern CGameEntitySystem* GameEntitySystem(); using SendSnapshotFn = void (*)(void*, void*); +using PackEntitiesFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*); +using PackEntityFn = void (*)(void*, void*, int, void*, void*, void*, void*, void*, void*, void*); static SendSnapshotFn g_sendSnapshot = nullptr; -static funchook_t* g_hook = nullptr; +static PackEntitiesFn g_packEntities = nullptr; +static PackEntityFn g_packEntity = nullptr; +static funchook_t* g_snapshotHook = nullptr; +static funchook_t* g_packHook = nullptr; +static funchook_t* g_packEntityHook = nullptr; +static uint64_t g_snapshotCalls = 0; +static uint64_t g_snapshotApplied = 0; +static uint64_t g_snapshotChanged = 0; +static uint64_t g_packCalls = 0; +static uint64_t g_packApplied = 0; +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_packEntitySelfSkips = 0; +static uint64_t g_packEntityNoRecipient = 0; +static uint64_t g_fullUpdateWrites = 0; +static int g_fullUpdateBudget = 0; +static thread_local CPlayerSlot g_currentRecipient(-1); static CPlayerSlot SlotFromClient(void* client) { @@ -21,8 +50,36 @@ static CPlayerSlot SlotFromClient(void* client) return *reinterpret_cast(reinterpret_cast(client) + offset); } +struct ScopedRecipient +{ + explicit ScopedRecipient(CPlayerSlot slot) : previous(g_currentRecipient) + { + g_currentRecipient = slot; + } + + ~ScopedRecipient() + { + g_currentRecipient = previous; + } + + CPlayerSlot previous; +}; + static void Detour_SendSnapshot(void* client, void* snapshot) { + ++g_snapshotCalls; + ScopedRecipient recipient(client ? SlotFromClient(client) : CPlayerSlot(-1)); + if (client && g_fullUpdateBudget > 0) + { + int offset = g_gameConfig ? g_gameConfig->GetOffset("CServerSideClient_DeltaTick") : -1; + if (offset >= 0) + { + *reinterpret_cast(reinterpret_cast(client) + offset) = -1; + --g_fullUpdateBudget; + ++g_fullUpdateWrites; + } + } + if (!g_enabled || !g_armed || !client || g_overrides.Empty()) { g_sendSnapshot(client, snapshot); @@ -31,10 +88,150 @@ static void Detour_SendSnapshot(void* client, void* snapshot) CPlayerSlot slot = SlotFromClient(client); auto applied = g_overrides.ApplyForRecipient(slot); + if (!applied.empty()) + { + ++g_snapshotApplied; + g_snapshotChanged += applied.size(); + } g_sendSnapshot(client, snapshot); g_overrides.Restore(applied); } +static void Detour_PackEntities(void* self, void* arg1, int arg2, void* arg3, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8) +{ + ++g_packCalls; + g_packEntities(self, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); +} + +static CEntityIdentity* PlayerIdentityFromInstance(void* instance) +{ + if (!instance) + return nullptr; + + auto* entity = static_cast(instance); + CEntityIdentity* identity = entity->m_pEntity; + const char* designer = identity ? identity->m_designerName.String() : nullptr; + return designer && !V_stricmp(designer, "player") ? identity : nullptr; +} + +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 int OwnerSlotForPawn(CEntityIdentity* pawnIdentity) +{ + if (!pawnIdentity) + return -1; + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return -1; + + auto pawnField = schema::FindField("CBasePlayerController", "m_hPawn"); + auto slotField = schema::FindField("CBasePlayerController", "m_nSplitScreenSlot"); + if (!pawnField.found || !slotField.found) + return -1; + + int pawnIndex = EntityIndex(pawnIdentity); + 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; + + auto* entity = reinterpret_cast(identity->m_pInstance); + int controlledPawnIndex = reinterpret_cast(entity + pawnField.offset)->GetEntryIndex(); + if (controlledPawnIndex != pawnIndex) + continue; + + int slot = *reinterpret_cast(entity + slotField.offset); + return slot >= 0 && slot < 64 ? slot : -1; + } + + return -1; +} + +static void Detour_PackEntity(void* self, void* arg1, int entityIndex, void* entityData, void* arg4, void* arg5, void* arg6, void* arg7, void* arg8, void* arg9) +{ + ++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) + { + g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); + return; + } + + CPlayerSlot recipient = g_currentRecipient; + int spoofHealth = 42; + if (!g_overrides.TryGetHealth42Value(recipient, spoofHealth)) + { + g_packEntity(self, arg1, entityIndex, entityData, arg4, arg5, arg6, arg7, arg8, arg9); + return; + } + + 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; +} + +static bool InitPackEntityDetour(CGameConfig* config) +{ + void* packEntityTarget = config->ResolveSignature("CNetworkGameServer_PackEntity"); + if (!packEntityTarget) + { + SPWarning("CNetworkGameServer_PackEntity signature missing; scoped health hook disabled\n"); + return true; + } + + g_packEntity = reinterpret_cast(packEntityTarget); + g_packEntityHook = funchook_create(); + if (!g_packEntityHook) + return false; + + if (funchook_prepare(g_packEntityHook, reinterpret_cast(&g_packEntity), reinterpret_cast(Detour_PackEntity)) != 0) + return false; + if (funchook_install(g_packEntityHook, 0) != 0) + return false; + + SPMessage("detoured CNetworkGameServer_PackEntity at %p\n", packEntityTarget); + return true; +} + bool InitSnapshotDetour(CGameConfig* config) { void* target = config->ResolveSignature("CServerSideClient_SendSnapshot"); @@ -45,25 +242,85 @@ bool InitSnapshotDetour(CGameConfig* config) } g_sendSnapshot = reinterpret_cast(target); - g_hook = funchook_create(); - if (!g_hook) + g_snapshotHook = funchook_create(); + if (!g_snapshotHook) return false; - if (funchook_prepare(g_hook, reinterpret_cast(&g_sendSnapshot), reinterpret_cast(Detour_SendSnapshot)) != 0) + if (funchook_prepare(g_snapshotHook, reinterpret_cast(&g_sendSnapshot), reinterpret_cast(Detour_SendSnapshot)) != 0) return false; - if (funchook_install(g_hook, 0) != 0) + if (funchook_install(g_snapshotHook, 0) != 0) return false; SPMessage("detoured CServerSideClient_SendSnapshot at %p\n", target); + + void* packTarget = config->ResolveSignature("CNetworkGameServer_PackEntities_Normal"); + if (!packTarget) + { + SPWarning("CNetworkGameServer_PackEntities_Normal signature missing; pre-pack health_42 hook disabled\n"); + return true; + } + + g_packEntities = reinterpret_cast(packTarget); + g_packHook = funchook_create(); + if (!g_packHook) + return false; + + if (funchook_prepare(g_packHook, reinterpret_cast(&g_packEntities), reinterpret_cast(Detour_PackEntities)) != 0) + return false; + if (funchook_install(g_packHook, 0) != 0) + return false; + + SPMessage("detoured CNetworkGameServer_PackEntities_Normal at %p\n", packTarget); + if (!InitPackEntityDetour(config)) + return false; return true; } void ShutdownSnapshotDetour() { - if (!g_hook) - return; - funchook_uninstall(g_hook, 0); - funchook_destroy(g_hook); - g_hook = nullptr; + if (g_packEntityHook) + { + funchook_uninstall(g_packEntityHook, 0); + funchook_destroy(g_packEntityHook); + g_packEntityHook = nullptr; + } + if (g_packHook) + { + funchook_uninstall(g_packHook, 0); + funchook_destroy(g_packHook); + g_packHook = nullptr; + } + if (g_snapshotHook) + { + funchook_uninstall(g_snapshotHook, 0); + funchook_destroy(g_snapshotHook); + g_snapshotHook = nullptr; + } g_sendSnapshot = nullptr; + g_packEntities = nullptr; + g_packEntity = nullptr; +} + +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", + static_cast(g_snapshotCalls), + static_cast(g_snapshotApplied), + static_cast(g_snapshotChanged), + static_cast(g_packCalls), + static_cast(g_packApplied), + static_cast(g_packChanged), + static_cast(g_packEntityCalls), + static_cast(g_packEntitySpoofs), + static_cast(g_packEntityScopedWrites), + static_cast(g_packEntitySelfSkips), + static_cast(g_packEntityNoRecipient), + static_cast(g_fullUpdateWrites), + g_fullUpdateBudget); +} + +void RequestSnapshotFullUpdates(int budget) +{ + if (budget > g_fullUpdateBudget) + g_fullUpdateBudget = budget; } diff --git a/src/detour.h b/src/detour.h index 7d192a8..ad27104 100644 --- a/src/detour.h +++ b/src/detour.h @@ -6,4 +6,5 @@ class CGameConfig; bool InitSnapshotDetour(CGameConfig* config); void ShutdownSnapshotDetour(); - +void DumpDetourStats(); +void RequestSnapshotFullUpdates(int budget = 512); diff --git a/src/module.cpp b/src/module.cpp index 71465af..402e724 100644 --- a/src/module.cpp +++ b/src/module.cpp @@ -116,4 +116,3 @@ void* CModule::FindInterface(const char* name) const return nullptr; return fn(name, nullptr); } - diff --git a/src/module.h b/src/module.h index 72f51ae..5364141 100644 --- a/src/module.h +++ b/src/module.h @@ -48,4 +48,3 @@ namespace modules extern CModule* server; extern CModule* schemasystem; } - diff --git a/src/overrides.cpp b/src/overrides.cpp index 76cab80..9374143 100644 --- a/src/overrides.cpp +++ b/src/overrides.cpp @@ -3,6 +3,7 @@ #include "common.h" #include "schema.h" #include "entity2/entitysystem.h" +#include "entity2/entityinstance.h" #include "entityhandle.h" #include @@ -109,6 +110,12 @@ static bool DesignerContains(CEntityIdentity* identity, const char* needle) return designer && V_stristr(designer, needle); } +static bool DesignerEquals(CEntityIdentity* identity, const char* value) +{ + const char* designer = identity ? identity->m_designerName.String() : nullptr; + return designer && !V_stricmp(designer, value); +} + static int EntityIndex(CEntityIdentity* identity) { return identity ? identity->m_EHandle.GetEntryIndex() : -1; @@ -274,13 +281,68 @@ bool OverrideManager::SetHealth42Test(bool enabled, std::string& error) return true; } +bool OverrideManager::SetRecipientHealthValue(int recipientSlot, int value, std::string& error) +{ + if (recipientSlot < -1 || recipientSlot >= static_cast(m_recipientHealthSet.size())) + { + error = "recipient slot must be all/-1 or 0..63"; + return false; + } + + if (!RequiredField("CBaseEntity", "m_iHealth", error)) + return false; + + if (recipientSlot == -1) + { + m_defaultHealth42Value = value; + SPMessage("health_42 default recipient value=%d\n", value); + return true; + } + + m_recipientHealthSet[recipientSlot] = true; + m_recipientHealthValue[recipientSlot] = value; + SPMessage("health_42 recipient slot %d value=%d\n", recipientSlot, value); + return true; +} + +void OverrideManager::ClearRecipientHealthValues() +{ + m_defaultHealth42Value = 42; + m_recipientHealthSet.fill(false); + m_recipientHealthValue.fill(0); + SPMessage("health_42 recipient values cleared\n"); +} + +bool OverrideManager::TryGetHealth42Value(CPlayerSlot recipient, int& value) const +{ + if (!m_health42Test) + return false; + + int slot = recipient.Get(); + if (slot >= 0 && slot < static_cast(m_recipientHealthSet.size()) && m_recipientHealthSet[slot]) + { + value = m_recipientHealthValue[slot]; + return true; + } + + value = m_defaultHealth42Value; + 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"); + { + SPMessage("dynamic: all non-own player CBaseEntity::m_iHealth = per-recipient value, default=%d\n", m_defaultHealth42Value); + for (size_t i = 0; i < m_recipientHealthSet.size(); ++i) + { + if (m_recipientHealthSet[i]) + SPMessage("dynamic: recipient slot %zu health=%d\n", i, m_recipientHealthValue[i]); + } + } 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)); } @@ -399,44 +461,71 @@ void OverrideManager::ApplyHealthMoneyTest(CPlayerSlot slot, std::vector& applied) +std::vector OverrideManager::ApplyGlobalHealth42() { + std::vector applied; if (!m_health42Test) - return; + return applied; if (!g_entitySystem) g_entitySystem = GameEntitySystem(); if (!g_entitySystem) - return; + return applied; - int changed = 0; + auto healthField = schema::FindField("CBaseEntity", "m_iHealth"); + if (!healthField.found) + return applied; + + applied.reserve(16); 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")) + if (!DesignerEquals(identity, "player")) continue; - uintptr_t entity = reinterpret_cast(identity->m_pInstance); - auto* healthAddress = FieldAddress(entity, "CBaseEntity", "m_iHealth"); - if (!healthAddress) - continue; + auto* healthAddress = reinterpret_cast(identity->m_pInstance) + healthField.offset; - AppliedOverride saved {healthAddress, FieldType::Int32, ReadBits(healthAddress, FieldType::Int32)}; - WriteBits(healthAddress, FieldType::Int32, 42); + AppliedOverride saved {healthAddress, FieldType::Int32, static_cast(*reinterpret_cast(healthAddress))}; + *reinterpret_cast(healthAddress) = 42; applied.push_back(saved); + } + + return applied; +} + +int OverrideManager::MarkPlayerHealthDirty() +{ + if (!g_entitySystem) + g_entitySystem = GameEntitySystem(); + if (!g_entitySystem) + return 0; + + auto healthField = schema::FindField("CBaseEntity", "m_iHealth"); + if (!healthField.found) + return 0; + + 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 || !DesignerEquals(identity, "player")) + continue; + + NetworkStateChangedData data(static_cast(healthField.offset)); + identity->m_pInstance->NetworkStateChanged(data); ++changed; } if (m_trace) - SPMessage("health_42_test recipient=%d changed=%d\n", slot.Get(), changed); + SPMessage("marked player health dirty changed=%d\n", changed); + return changed; } std::vector OverrideManager::ApplyForRecipient(CPlayerSlot slot) { std::vector applied; ApplyHealthMoneyTest(slot, applied); - ApplyHealth42Test(slot, applied); for (const auto& rule : m_rules) { if (!rule.enabled || (rule.recipientSlot != -1 && rule.recipientSlot != slot.Get())) diff --git a/src/overrides.h b/src/overrides.h index adc0bce..7046acd 100644 --- a/src/overrides.h +++ b/src/overrides.h @@ -3,6 +3,7 @@ #include "playerslot.h" #include +#include #include #include @@ -54,14 +55,20 @@ public: 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); + bool SetRecipientHealthValue(int recipientSlot, int value, std::string& error); + void ClearRecipientHealthValues(); 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; } + bool Health42Test() const { return m_health42Test; } + bool TryGetHealth42Value(CPlayerSlot recipient, int& value) const; std::vector ApplyForRecipient(CPlayerSlot slot); + std::vector ApplyGlobalHealth42(); + int MarkPlayerHealthDirty(); void Restore(const std::vector& applied); private: @@ -69,13 +76,15 @@ 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); - void ApplyHealth42Test(CPlayerSlot slot, std::vector& applied); std::vector m_rules; int m_nextId {1}; bool m_trace {false}; bool m_healthMoneyTest {false}; bool m_health42Test {false}; + int m_defaultHealth42Value {42}; + std::array m_recipientHealthSet {}; + std::array m_recipientHealthValue {}; }; extern OverrideManager g_overrides; diff --git a/src/plugin.cpp b/src/plugin.cpp index 760db44..d178973 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -10,6 +10,7 @@ #include "schema.h" #include "schemasystem/schemasystem.h" #include "interfaces/interfaces.h" +#include "entity2/entityclass.h" #include "entity2/entitysystem.h" #include "tier0/memdbgon.h" @@ -26,6 +27,43 @@ CGameEntitySystem* g_entitySystem = nullptr; bool g_enabled = true; bool g_armed = false; +static int ForceFullUpdateForAllClients() +{ + if (!g_networkServerService || !g_gameConfig) + return 0; + + CNetworkGameServerBase* server = g_networkServerService->GetIGameServer(); + if (!server) + return 0; + + int listOffset = g_gameConfig->GetOffset("CNetworkGameServer_ClientList"); + int deltaTickOffset = g_gameConfig->GetOffset("CServerSideClient_DeltaTick"); + if (listOffset < 0 || deltaTickOffset < 0) + return 0; + + auto* list = reinterpret_cast(server) + listOffset; + int count = *reinterpret_cast(list); + if (count <= 0 || count > 256) + return 0; + + auto** clients = *reinterpret_cast(list + 8); + if (!clients) + return 0; + + int forced = 0; + for (int i = 0; i < count; ++i) + { + void* client = clients[i]; + if (!client) + continue; + + auto* deltaTick = reinterpret_cast(reinterpret_cast(client) + deltaTickOffset); + *deltaTick = -1; + ++forced; + } + return forced; +} + void SPMessage(const char* msg, ...) { va_list args; @@ -83,9 +121,57 @@ CON_COMMAND_F(sp_trace, "Enable SendProxy tracing", FCVAR_NONE) 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()); + DumpDetourStats(); g_overrides.Dump(); } +CON_COMMAND_F(sp_fullupdate, "Force full snapshot update for all connected clients", FCVAR_NONE) +{ + int forced = ForceFullUpdateForAllClients(); + RequestSnapshotFullUpdates(); + SPMessage("forced full update for %d clients; queued snapshot fallback\n", forced); +} + +CON_COMMAND_F(sp_entities, "sp_entities [substring] [limit]", FCVAR_NONE) +{ + CGameEntitySystem* entitySystem = GameEntitySystem(); + if (!entitySystem) + { + SPWarning("no entity system\n"); + return; + } + + const char* filter = args.ArgC() >= 2 ? args[1] : ""; + int limit = args.ArgC() >= 3 ? atoi(args[2]) : 80; + if (limit <= 0) + limit = 80; + + auto healthField = schema::FindField("CBaseEntity", "m_iHealth"); + int printed = 0; + int visited = 0; + for (auto* identity = entitySystem->m_EntityList.m_pFirstActiveEntity; identity && visited < 16384; identity = identity->m_pNext, ++visited) + { + const char* designer = identity->m_designerName.String(); + const char* classDesigner = identity->m_pClass ? identity->m_pClass->m_designerName.String() : ""; + if (filter && *filter && !V_stristr(designer ? designer : "", filter) && !V_stristr(classDesigner ? classDesigner : "", filter)) + continue; + + int health = -999999; + if (identity->m_pInstance && healthField.found) + health = *reinterpret_cast(reinterpret_cast(identity->m_pInstance) + healthField.offset); + + SPMessage("entity idx=%d inst=%p designer=%s class=%s health=%d\n", + identity->m_EHandle.GetEntryIndex(), + identity->m_pInstance, + designer ? designer : "", + classDesigner ? classDesigner : "", + health); + if (++printed >= limit) + break; + } + SPMessage("entities printed=%d visited=%d filter=%s\n", printed, visited, filter ? filter : ""); +} + CON_COMMAND_F(sp_clear, "Clear SendProxy rules", FCVAR_NONE) { g_overrides.Clear(); @@ -135,11 +221,13 @@ static void SetHealthMoneyCommand(const CCommand& args) g_enabled = true; g_armed = true; SPMessage("enabled=1 armed=1\n"); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); } else if (g_overrides.Empty()) { g_armed = false; SPMessage("armed=0\n"); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); } } @@ -174,14 +262,61 @@ CON_COMMAND_F(sp_health_42, "sp_health_42 <0|1>", FCVAR_NONE) g_enabled = true; g_armed = true; SPMessage("enabled=1 armed=1\n"); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); } else if (g_overrides.Empty()) { g_armed = false; SPMessage("armed=0\n"); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); } } +CON_COMMAND_F(sp_health_recipient, "sp_health_recipient [value]", FCVAR_NONE) +{ + if (args.ArgC() < 2) + { + SPWarning("usage: sp_health_recipient [value]\n"); + return; + } + + if (!V_stricmp(args[1], "clear")) + { + g_overrides.ClearRecipientHealthValues(); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); + return; + } + + if (args.ArgC() < 3) + { + SPWarning("usage: sp_health_recipient [value]\n"); + return; + } + + int recipientSlot = !V_stricmp(args[1], "all") ? -1 : atoi(args[1]); + int value = atoi(args[2]); + std::string error; + if (!g_overrides.SetRecipientHealthValue(recipientSlot, value, error)) + { + SPWarning("%s\n", error.c_str()); + return; + } + + if (!g_overrides.Health42Test()) + { + if (!g_overrides.SetHealth42Test(true, error)) + { + SPWarning("%s\n", error.c_str()); + return; + } + } + + g_enabled = true; + g_armed = true; + SPMessage("enabled=1 armed=1\n"); + SPMessage("marked health dirty for %d players\n", g_overrides.MarkPlayerHealthDirty()); +} + CON_COMMAND_F(sp_schema_fields, "sp_schema_fields [substring]", FCVAR_NONE) { if (args.ArgC() < 2)