From c5676340ad22d450e2cc3623cdf8c492478ed30b Mon Sep 17 00:00:00 2001 From: Vauff Date: Mon, 14 Sep 2026 00:42:20 -0400 Subject: [PATCH 01/11] Fix possible crash in FireEvent hook Seems like the game passes nullptr in some situations --- src/virtual_hooks.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/virtual_hooks.cpp b/src/virtual_hooks.cpp index af287782..50496e8e 100644 --- a/src/virtual_hooks.cpp +++ b/src/virtual_hooks.cpp @@ -770,7 +770,7 @@ KHook::Return Hook_LoadEventsFromFile(IGameEventManager2* pThis, const char KHook::Return Hook_FireEvent(IGameEventManager2* pThis, IGameEvent* pEvent, bool bDontBroadcast) { // Make player_connect obey cs2f_map_steamids_enable as well - if (!g_cvarEnableMapSteamIds.Get() && !V_stricmp(pEvent->GetName(), "player_connect")) + if (!g_cvarEnableMapSteamIds.Get() && pEvent && !V_stricmp(pEvent->GetName(), "player_connect")) { pEvent->SetString("networkid", ""); pEvent->SetUint64("xuid", 0); From 53409f3782c119e4f96d193a11a82eab120df5b8 Mon Sep 17 00:00:00 2001 From: Vauff Date: Mon, 14 Sep 2026 03:00:58 -0400 Subject: [PATCH 02/11] Fix FindUseEntity function prototype that crashed on Windows this has been wrong for a while but funchook didn't seem to care --- src/detours.cpp | 4 ++-- src/detours.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/detours.cpp b/src/detours.cpp index 29466c51..3417e731 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -716,13 +716,13 @@ KHook::Return Detour_CCSPlayerPawn_GetMaxSpeed(CCSPlayerPawn* pPawn) CConVar g_cvarPreventUsingPlayers("cs2f_prevent_using_players", FCVAR_NONE, "Whether to prevent +use from hitting players (0=can use players, 1=cannot use players)", false); bool g_bFindingUseEntity = false; -KHook::Return Detour_FindUseEntity(CCSPlayer_UseServices* pThis, float a2) +KHook::Return Detour_FindUseEntity(CCSPlayer_UseServices* pThis, float unk2, int64_t unk3) { g_bFindingUseEntity = true; return {KHook::Action::Ignore}; } -KHook::Return Detour_FindUseEntity_Post(CCSPlayer_UseServices* pThis, float a2) +KHook::Return Detour_FindUseEntity_Post(CCSPlayer_UseServices* pThis, float unk2, int64_t unk3) { g_bFindingUseEntity = false; return {KHook::Action::Ignore}; diff --git a/src/detours.h b/src/detours.h index 27d7245f..75a94f24 100644 --- a/src/detours.h +++ b/src/detours.h @@ -97,8 +97,8 @@ KHook::Return Detour_CGamePlayerEquip_InputTriggerForActivatedPlayer(CGame KHook::Return Detour_CTriggerGravity_GravityTouch(CTriggerGravity* pEntity, CBaseEntity* pOther); KHook::Return Detour_GetFreeClient(int64_t unk1, const __m128i* unk2, unsigned int unk3, int64_t unk4, char unk5, void* unk6); KHook::Return Detour_CCSPlayerPawn_GetMaxSpeed(CCSPlayerPawn*); -KHook::Return Detour_FindUseEntity(CCSPlayer_UseServices* pThis, float a2); -KHook::Return Detour_FindUseEntity_Post(CCSPlayer_UseServices* pThis, float a2); +KHook::Return Detour_FindUseEntity(CCSPlayer_UseServices* pThis, float unk2, int64_t unk3); +KHook::Return Detour_FindUseEntity_Post(CCSPlayer_UseServices* pThis, float unk2, int64_t unk3); KHook::Return Detour_TraceFunc(int64*, int*, float*, uint64); KHook::Return Detour_TraceShape(int64*, int64, int64, int64, CTraceFilter*, int64); KHook::Return Detour_CEntityIOOutput_FireOutputInternal(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay, void*, void*); From 0f7870b7e347d016db3c30bdc17a9a00c9815d0c Mon Sep 17 00:00:00 2001 From: xen Date: Wed, 16 Sep 2026 04:00:59 +0300 Subject: [PATCH 03/11] Add helper for virtual KHooks --- CS2Fixes.vcxproj | 2 +- CS2Fixes.vcxproj.filters | 2 +- src/cs2fixes.cpp | 4 +- src/detours.cpp | 72 ++++++------- src/utils/khook_helpers.h | 206 ++++++++++++++++++++++++++++++++++++++ src/utils/sighook.h | 147 --------------------------- src/virtual_hooks.cpp | 67 ++++--------- 7 files changed, 265 insertions(+), 235 deletions(-) create mode 100644 src/utils/khook_helpers.h delete mode 100644 src/utils/sighook.h diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj index c69eabfa..61c8a9b8 100644 --- a/CS2Fixes.vcxproj +++ b/CS2Fixes.vcxproj @@ -311,7 +311,7 @@ - + diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters index e24aad8d..5b04460d 100644 --- a/CS2Fixes.vcxproj.filters +++ b/CS2Fixes.vcxproj.filters @@ -448,7 +448,7 @@ Header Files\utils - + Header Files\utils diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index f31ad833..8e3c8185 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -27,7 +27,7 @@ #include "common.h" #include "ctimer.h" #include "cvarwhitelist.h" -#include "utils/sighook.h" +#include "khook_helpers.h" #include "discord.h" #include "entities.h" #include "entity/customhudlayout.h" @@ -128,7 +128,7 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool g_bRequiredInitLoaded = false; InitVirtualHooks(); - InitSigHooks(); + InitKHooks(); if (!InitPatches()) g_bRequiredInitLoaded = false; diff --git a/src/detours.cpp b/src/detours.cpp index 3417e731..2253b229 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -30,7 +30,7 @@ #include "customio.h" #include "cvarwhitelist.h" #include "detours.h" -#include "utils/sighook.h" +#include "khook_helpers.h" #include "entities.h" #include "entity/cbasemodelentity.h" #include "entity/cbeam.h" @@ -59,49 +59,49 @@ #include "tier0/memdbgon.h" -SIG_HOOK_FUNCTION(UTIL_SayTextFilter, Detour_UTIL_SayTextFilter, nullptr); -SIG_HOOK_FUNCTION(UTIL_SayText2Filter, Detour_UTIL_SayText2Filter, nullptr); -SIG_HOOK_MEMBER(TriggerPush_Touch, Detour_TriggerPush_Touch, nullptr); -SIG_HOOK_MEMBER(CBaseEntity_TakeDamageOld, Detour_CBaseEntity_TakeDamageOld, Detour_CBaseEntity_TakeDamageOld_Post); -SIG_HOOK_MEMBER(CCSPlayer_WeaponServices_CanUse, Detour_CCSPlayer_WeaponServices_CanUse, nullptr); -SIG_HOOK_MEMBER(CCSPlayer_WeaponServices_EquipWeapon, Detour_CCSPlayer_WeaponServices_EquipWeapon, nullptr); -SIG_HOOK_MEMBER(CEntityIdentity_AcceptInput, Detour_CEntityIdentity_AcceptInput, nullptr); -SIG_HOOK_MEMBER(CNavMesh_GetNearestNavArea, Detour_CNavMesh_GetNearestNavArea, nullptr); -SIG_HOOK_MEMBER(ProcessMovement, Detour_ProcessMovement, Detour_ProcessMovement_Post); -SIG_HOOK_MEMBER(ProcessUsercmds, Detour_ProcessUsercmds, nullptr); -SIG_HOOK_MEMBER(CGamePlayerEquip_InputTriggerForAllPlayers, Detour_CGamePlayerEquip_InputTriggerForAllPlayers, nullptr); -SIG_HOOK_MEMBER(CGamePlayerEquip_InputTriggerForActivatedPlayer, Detour_CGamePlayerEquip_InputTriggerForActivatedPlayer, nullptr); -SIG_HOOK_MEMBER(CTriggerGravity_GravityTouch, Detour_CTriggerGravity_GravityTouch, nullptr); -SIG_HOOK_FUNCTION(GetFreeClient, Detour_GetFreeClient, nullptr); +KHOOK_FUNCTION(UTIL_SayTextFilter, Detour_UTIL_SayTextFilter, nullptr); +KHOOK_FUNCTION(UTIL_SayText2Filter, Detour_UTIL_SayText2Filter, nullptr); +KHOOK_MEMBER(TriggerPush_Touch, Detour_TriggerPush_Touch, nullptr); +KHOOK_MEMBER(CBaseEntity_TakeDamageOld, Detour_CBaseEntity_TakeDamageOld, Detour_CBaseEntity_TakeDamageOld_Post); +KHOOK_MEMBER(CCSPlayer_WeaponServices_CanUse, Detour_CCSPlayer_WeaponServices_CanUse, nullptr); +KHOOK_MEMBER(CCSPlayer_WeaponServices_EquipWeapon, Detour_CCSPlayer_WeaponServices_EquipWeapon, nullptr); +KHOOK_MEMBER(CEntityIdentity_AcceptInput, Detour_CEntityIdentity_AcceptInput, nullptr); +KHOOK_MEMBER(CNavMesh_GetNearestNavArea, Detour_CNavMesh_GetNearestNavArea, nullptr); +KHOOK_MEMBER(ProcessMovement, Detour_ProcessMovement, Detour_ProcessMovement_Post); +KHOOK_MEMBER(ProcessUsercmds, Detour_ProcessUsercmds, nullptr); +KHOOK_MEMBER(CGamePlayerEquip_InputTriggerForAllPlayers, Detour_CGamePlayerEquip_InputTriggerForAllPlayers, nullptr); +KHOOK_MEMBER(CGamePlayerEquip_InputTriggerForActivatedPlayer, Detour_CGamePlayerEquip_InputTriggerForActivatedPlayer, nullptr); +KHOOK_MEMBER(CTriggerGravity_GravityTouch, Detour_CTriggerGravity_GravityTouch, nullptr); +KHOOK_FUNCTION(GetFreeClient, Detour_GetFreeClient, nullptr); #ifdef __linux__ // Inlined by MSVC as of 2025-07-28 CS2 update // TODO: Find some alternative that supports Windows -SIG_HOOK_MEMBER(CCSPlayerPawn_GetMaxSpeed, Detour_CCSPlayerPawn_GetMaxSpeed, nullptr); +KHOOK_MEMBER(CCSPlayerPawn_GetMaxSpeed, Detour_CCSPlayerPawn_GetMaxSpeed, nullptr); #endif -SIG_HOOK_MEMBER(FindUseEntity, Detour_FindUseEntity, Detour_FindUseEntity_Post); -SIG_HOOK_FUNCTION(TraceFunc, Detour_TraceFunc, nullptr); -SIG_HOOK_FUNCTION(TraceShape, Detour_TraceShape, nullptr); -SIG_HOOK_MEMBER(CBasePlayerPawn_GetEyePosition, Detour_CBasePlayerPawn_GetEyePosition, nullptr); -SIG_HOOK_MEMBER(CBasePlayerPawn_GetEyeAngles, Detour_CBasePlayerPawn_GetEyeAngles, nullptr); -SIG_HOOK_MEMBER(CBaseFilter_InputTestActivator, Detour_CBaseFilter_InputTestActivator, nullptr); -SIG_HOOK_FUNCTION(GameSystem_Think_CheckSteamBan, Detour_GameSystem_Think_CheckSteamBan, Detour_GameSystem_Think_CheckSteamBan_Post); -SIG_HOOK_MEMBER(CCSPlayer_ItemServices_CanAcquire, Detour_CCSPlayer_ItemServices_CanAcquire, nullptr); -SIG_HOOK_FUNCTION(CS_Script_SetModel, Detour_CS_Script_SetModel, Detour_CS_Script_SetModel_Post); -SIG_HOOK_MEMBER(CBaseModelEntity_SetModel, Detour_CBaseModelEntity_SetModel, nullptr); -SIG_HOOK_MEMBER(CCSGameRules_GoToIntermission, Detour_CCSGameRules_GoToIntermission, nullptr); -SIG_HOOK_MEMBER(SetBeamOrigin, Detour_SetBeamOrigin, nullptr); -SIG_HOOK_MEMBER(SetBeamEndPos, Detour_SetBeamEndPos, nullptr); -SIG_HOOK_FUNCTION(IsCommandWhitelisted, Detour_IsCommandWhitelisted, nullptr); - -std::vector& GetSigHookList() -{ - static std::vector s_vecSigHooks; +KHOOK_MEMBER(FindUseEntity, Detour_FindUseEntity, Detour_FindUseEntity_Post); +KHOOK_FUNCTION(TraceFunc, Detour_TraceFunc, nullptr); +KHOOK_FUNCTION(TraceShape, Detour_TraceShape, nullptr); +KHOOK_MEMBER(CBasePlayerPawn_GetEyePosition, Detour_CBasePlayerPawn_GetEyePosition, nullptr); +KHOOK_MEMBER(CBasePlayerPawn_GetEyeAngles, Detour_CBasePlayerPawn_GetEyeAngles, nullptr); +KHOOK_MEMBER(CBaseFilter_InputTestActivator, Detour_CBaseFilter_InputTestActivator, nullptr); +KHOOK_FUNCTION(GameSystem_Think_CheckSteamBan, Detour_GameSystem_Think_CheckSteamBan, Detour_GameSystem_Think_CheckSteamBan_Post); +KHOOK_MEMBER(CCSPlayer_ItemServices_CanAcquire, Detour_CCSPlayer_ItemServices_CanAcquire, nullptr); +KHOOK_FUNCTION(CS_Script_SetModel, Detour_CS_Script_SetModel, Detour_CS_Script_SetModel_Post); +KHOOK_MEMBER(CBaseModelEntity_SetModel, Detour_CBaseModelEntity_SetModel, nullptr); +KHOOK_MEMBER(CCSGameRules_GoToIntermission, Detour_CCSGameRules_GoToIntermission, nullptr); +KHOOK_MEMBER(SetBeamOrigin, Detour_SetBeamOrigin, nullptr); +KHOOK_MEMBER(SetBeamEndPos, Detour_SetBeamEndPos, nullptr); +KHOOK_FUNCTION(IsCommandWhitelisted, Detour_IsCommandWhitelisted, nullptr); + +std::vector& GetKHookList() +{ + static std::vector s_vecSigHooks; return s_vecSigHooks; } -void InitSigHooks() +void InitKHooks() { - for (auto hook : GetSigHookList()) + for (auto hook : GetKHookList()) hook->Configure(); } diff --git a/src/utils/khook_helpers.h b/src/utils/khook_helpers.h new file mode 100644 index 00000000..1d9e540a --- /dev/null +++ b/src/utils/khook_helpers.h @@ -0,0 +1,206 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2026 Source2ZE + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once +#include "../gameconfig.h" + +class CKHookBase +{ +public: + virtual void Configure() = 0; +}; + +extern std::vector& GetKHookList(); +void InitKHooks(); + +template +class CKHookFunction : public CKHookBase +{ +public: + using fnCallback = KHook::Return (*)(ARGS...); + + CKHookFunction(const char* pSignature, fnCallback cbPre, fnCallback cbPost) : + m_pSignatureName(pSignature), m_hook(cbPre, cbPost) + { + GetKHookList().push_back(this); + } + + virtual void Configure() override + { + auto address = g_GameConfig->ResolveSignature(m_pSignatureName); + + if (!address) + { + g_bRequiredInitLoaded = false; + return; + } + + m_hook.Configure(address); + + Message("Hooked %s at 0x%p\n", m_pSignatureName, address); + } + + RETURN CallOriginal(ARGS... args) + { + return m_hook.CallOriginal(args...); + } + +private: + KHook::Function m_hook; + const char* m_pSignatureName; +}; + +template +auto MakeKHookFunction(const char* pSignature, KHook::Return (*cbPre)(ARGS...), std::nullptr_t cbPost) +{ + return std::make_unique>(pSignature, cbPre, nullptr); +} + +template +auto MakeKHookFunction(const char* pSignature, std::nullptr_t cbPre, KHook::Return (*cbPost)(ARGS...)) +{ + return std::make_unique>(pSignature, nullptr, cbPost); +} + +template +auto MakeKHookFunction(const char* pSignature, KHook::Return (*cbPre)(ARGS...), KHook::Return (*cbPost)(ARGS...)) +{ + return std::make_unique>(pSignature, cbPre, cbPost); +} + +#define KHOOK_FUNCTION(name, pre, post) \ + static auto hook##name = MakeKHookFunction(#name, pre, post) + +template +class CKHookMember : public CKHookBase +{ +public: + using fnCallback = KHook::Return (*)(CLASS*, ARGS...); + + CKHookMember(const char* pSignature, fnCallback cbPre, fnCallback cbPost) : + m_pSignatureName(pSignature), m_hook(cbPre, cbPost) + { + GetKHookList().push_back(this); + } + + virtual void Configure() override + { + auto address = g_GameConfig->ResolveSignature(m_pSignatureName); + + if (!address) + { + g_bRequiredInitLoaded = false; + return; + } + + m_hook.Configure(address); + + Message("Hooked %s at 0x%p\n", m_pSignatureName, address); + } + + RETURN CallOriginal(CLASS* this_ptr, ARGS... args) + { + return m_hook.CallOriginal(this_ptr, args...); + } + +private: + KHook::Member m_hook; + const char* m_pSignatureName; +}; + +template +auto MakeKHookMember(const char* pSignature, KHook::Return (*cbPre)(CLASS*, ARGS...), std::nullptr_t cbPost) +{ + return std::make_unique>(pSignature, cbPre, nullptr); +} + +template +auto MakeKHookMember(const char* pSignature, std::nullptr_t cbPre, KHook::Return (*cbPost)(CLASS*, ARGS...)) +{ + return std::make_unique>(pSignature, nullptr, cbPost); +} + +template +auto MakeKHookMember(const char* pSignature, KHook::Return (*cbPre)(CLASS*, ARGS...), KHook::Return (*cbPost)(CLASS*, ARGS...)) +{ + return std::make_unique>(pSignature, cbPre, cbPost); +} + +#define KHOOK_MEMBER(name, pre, post) \ + static auto hook##name = MakeKHookMember(#name, pre, post) + +template +class CKHookVirtual : public CKHookBase +{ +public: + using fnCallback = KHook::Return (*)(CLASS*, ARGS...); + + CKHookVirtual(RETURN (CLASS::*pFunction)(ARGS...), CLASS*& pInstance, fnCallback cbPre, fnCallback cbPost) : + m_hook(pFunction, cbPre, cbPost), m_pInstance(pInstance) + { + GetKHookList().push_back(this); + } + + ~CKHookVirtual() + { + if (m_hook.IsActive()) + m_hook.Remove(m_pInstance); + } + + virtual void Configure() override + { + if (!m_pInstance) + { + g_bRequiredInitLoaded = false; + return; + } + + m_hook.Add(m_pInstance); + } + + RETURN CallOriginal(CLASS* this_ptr, ARGS... args) + { + return m_hook.CallOriginal(this_ptr, args...); + } + +private: + KHook::Virtual m_hook; + CLASS*& m_pInstance; +}; + +template +auto MakeKHookVirtual(RETURN (CLASS::*pFunction)(ARGS...), CLASS*& pInstance, KHook::Return (*cbPre)(CLASS*, ARGS...), std::nullptr_t cbPost) +{ + return std::make_unique>(pFunction, pInstance, cbPre, nullptr); +} + +template +auto MakeKHookVirtual(RETURN (CLASS::*pFunction)(ARGS...), CLASS*& pInstance, std::nullptr_t cbPre, KHook::Return (*cbPost)(CLASS*, ARGS...)) +{ + return std::make_unique>(pFunction, pInstance, nullptr, cbPost); +} + +template +auto MakeKHookVirtual(RETURN (CLASS::*pFunction)(ARGS...), CLASS*& pInstance, KHook::Return (*cbPre)(CLASS*, ARGS...), KHook::Return (*cbPost)(CLASS*, ARGS...)) +{ + return std::make_unique>(pFunction, pInstance, cbPre, cbPost); +} + +#define KHOOK_VIRTUAL(name, function, instance, pre, post) \ + static auto hook##name = MakeKHookVirtual(function, instance, pre, post) diff --git a/src/utils/sighook.h b/src/utils/sighook.h deleted file mode 100644 index 7adc7048..00000000 --- a/src/utils/sighook.h +++ /dev/null @@ -1,147 +0,0 @@ -/** - * ============================================================================= - * CS2Fixes - * Copyright (C) 2023-2026 Source2ZE - * ============================================================================= - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License, version 3.0, as published by the - * Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more - * details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ - -#pragma once -#include "../gameconfig.h" -#include "khook.hpp" - -class CSigHookBase -{ -public: - CSigHookBase(const char* pSignature) : - m_pSignatureName(pSignature) - { - } - - virtual void Configure() = 0; - -protected: - const char* m_pSignatureName; -}; - -extern std::vector& GetSigHookList(); -void InitSigHooks(); - -template -class CSigHookFunction : public CSigHookBase -{ -public: - using fnCallback = KHook::Return (*)(ARGS...); - - CSigHookFunction(const char* pSignature, fnCallback cbPre = nullptr, fnCallback cbPost = nullptr) : - CSigHookBase(pSignature), m_hook(cbPre, cbPost) - { - GetSigHookList().push_back(this); - } - - virtual void Configure() override - { - auto address = g_GameConfig->ResolveSignature(m_pSignatureName); - - if (!address) - g_bRequiredInitLoaded = false; - - m_hook.Configure(address); - - Message("Hooked %s at 0x%p\n", m_pSignatureName, address); - } - - RETURN CallOriginal(ARGS... args) - { - return m_hook.CallOriginal(args...); - } - -private: - KHook::Function m_hook; -}; - -template -auto MakeSigHookFunction(const char* pSignature, KHook::Return (*cbPre)(ARGS...), std::nullptr_t cbPost) -{ - return std::make_unique>(pSignature, cbPre, nullptr); -} - -template -auto MakeSigHookFunction(const char* pSignature, std::nullptr_t cbPre, KHook::Return (*cbPost)(ARGS...)) -{ - return std::make_unique>(pSignature, nullptr, cbPost); -} - -template -auto MakeSigHookFunction(const char* pSignature, KHook::Return (*cbPre)(ARGS...), KHook::Return (*cbPost)(ARGS...)) -{ - return std::make_unique>(pSignature, cbPre, cbPost); -} - -#define SIG_HOOK_FUNCTION(name, pre, post) \ - static auto hook##name = MakeSigHookFunction(#name, pre, post) - -template -class CSigHookMember : public CSigHookBase -{ -public: - using fnCallback = KHook::Return (*)(CLASS*, ARGS...); - - CSigHookMember(const char* pSignature, fnCallback cbPre = nullptr, fnCallback cbPost = nullptr) : - CSigHookBase(pSignature), m_hook(cbPre, cbPost) - { - GetSigHookList().push_back(this); - } - - virtual void Configure() override - { - auto address = g_GameConfig->ResolveSignature(m_pSignatureName); - - if (!address) - g_bRequiredInitLoaded = false; - - m_hook.Configure(address); - - Message("Hooked %s at 0x%p\n", m_pSignatureName, address); - } - - RETURN CallOriginal(CLASS* this_ptr, ARGS... args) - { - return m_hook.CallOriginal(this_ptr, args...); - } - -private: - KHook::Member m_hook; -}; - -template -auto MakeSigHookMember(const char* pSignature, KHook::Return (*cbPre)(CLASS*, ARGS...), std::nullptr_t cbPost) -{ - return std::make_unique>(pSignature, cbPre, nullptr); -} - -template -auto MakeSigHookMember(const char* pSignature, std::nullptr_t cbPre, KHook::Return (*cbPost)(CLASS*, ARGS...)) -{ - return std::make_unique>(pSignature, nullptr, cbPost); -} - -template -auto MakeSigHookMember(const char* pSignature, KHook::Return (*cbPre)(CLASS*, ARGS...), KHook::Return (*cbPost)(CLASS*, ARGS...)) -{ - return std::make_unique>(pSignature, cbPre, cbPost); -} - -#define SIG_HOOK_MEMBER(name, pre, post) \ - static auto hook##name = MakeSigHookMember(#name, pre, post) diff --git a/src/virtual_hooks.cpp b/src/virtual_hooks.cpp index 50496e8e..47d762e1 100644 --- a/src/virtual_hooks.cpp +++ b/src/virtual_hooks.cpp @@ -44,6 +44,7 @@ #include "map_votes.h" #include "mapmigrations.h" #include "module.h" +#include "khook_helpers.h" #include "networkstringtabledefs.h" #include "panoramavote.h" #include "playermanager.h" @@ -58,21 +59,22 @@ class GameSessionConfiguration_t {}; -KHook::Virtual gameFrameHook(&IServerGameDLL::GameFrame, nullptr, Hook_GameFrame_Post); -KHook::Virtual gameServerSteamAPIActivatedHook(&IServerGameDLL::GameServerSteamAPIActivated, Hook_GameServerSteamAPIActivated, nullptr); -KHook::Virtual applyGameSettingsHook(&IServerGameDLL::ApplyGameSettings, Hook_ApplyGameSettings, nullptr); -KHook::Virtual clientActiveHook(&IServerGameClients::ClientActive, nullptr, Hook_ClientActive_Post); -KHook::Virtual clientDisconnectHook(&IServerGameClients::ClientDisconnect, nullptr, Hook_ClientDisconnect_Post); -KHook::Virtual clientPutInServerHook(&IServerGameClients::ClientPutInServer, nullptr, Hook_ClientPutInServer_Post); -KHook::Virtual clientSettingsChangedHook(&IServerGameClients::ClientSettingsChanged, Hook_ClientSettingsChanged, nullptr); -KHook::Virtual onClientConnectedHook(&IServerGameClients::OnClientConnected, Hook_OnClientConnected, nullptr); -KHook::Virtual clientConnectHook(&IServerGameClients::ClientConnect, Hook_ClientConnect, nullptr); -KHook::Virtual clientCommandHook(&IServerGameClients::ClientCommand, Hook_ClientCommand, nullptr); -KHook::Virtual clientSvcUserMessageHook(&IServerGameClients::ClientSvcUserMessage, Hook_ClientSvcUserMessage, nullptr); -KHook::Virtual postEventAbstractHook(&IGameEventSystem::PostEventAbstract, Hook_PostEventAbstract, nullptr); -KHook::Virtual startupServerHook(&INetworkServerService::StartupServer, nullptr, Hook_StartupServer_Post); -KHook::Virtual&, CBitVec<16384>&, const Entity2Networkable_t**, const uint16*, int> checkTransmitHook(&ISource2GameEntities::CheckTransmit, nullptr, Hook_CheckTransmit_Post); -KHook::Virtual dispatchConCommandHook(&ICvar::DispatchConCommand, Hook_DispatchConCommand, nullptr); +KHOOK_VIRTUAL(GameFrame, &IServerGameDLL::GameFrame, g_pSource2Server, nullptr, Hook_GameFrame_Post); +KHOOK_VIRTUAL(GameServerSteamAPIActivated, &IServerGameDLL::GameServerSteamAPIActivated, g_pSource2Server, Hook_GameServerSteamAPIActivated, nullptr); +KHOOK_VIRTUAL(ApplyGameSettings, &IServerGameDLL::ApplyGameSettings, g_pSource2Server, Hook_ApplyGameSettings, nullptr); +KHOOK_VIRTUAL(ClientActive, &IServerGameClients::ClientActive, g_pSource2GameClients, nullptr, Hook_ClientActive_Post); +KHOOK_VIRTUAL(ClientDisconnect, &IServerGameClients::ClientDisconnect, g_pSource2GameClients, nullptr, Hook_ClientDisconnect_Post); +KHOOK_VIRTUAL(ClientPutInServer, &IServerGameClients::ClientPutInServer, g_pSource2GameClients, nullptr, Hook_ClientPutInServer_Post); +KHOOK_VIRTUAL(ClientSettingsChanged, &IServerGameClients::ClientSettingsChanged, g_pSource2GameClients, Hook_ClientSettingsChanged, nullptr); +KHOOK_VIRTUAL(OnClientConnected, &IServerGameClients::OnClientConnected, g_pSource2GameClients, Hook_OnClientConnected, nullptr); +KHOOK_VIRTUAL(ClientConnect, &IServerGameClients::ClientConnect, g_pSource2GameClients, Hook_ClientConnect, nullptr); +KHOOK_VIRTUAL(ClientCommand, &IServerGameClients::ClientCommand, g_pSource2GameClients, Hook_ClientCommand, nullptr); +KHOOK_VIRTUAL(ClientSvcUserMessage, &IServerGameClients::ClientSvcUserMessage, g_pSource2GameClients, Hook_ClientSvcUserMessage, nullptr); +KHOOK_VIRTUAL(PostEventAbstract, &IGameEventSystem::PostEventAbstract, g_gameEventSystem, Hook_PostEventAbstract, nullptr); +KHOOK_VIRTUAL(StartupServer, &INetworkServerService::StartupServer, g_pNetworkServerService, nullptr, Hook_StartupServer_Post); +KHOOK_VIRTUAL(CheckTransmit, &ISource2GameEntities::CheckTransmit, g_pSource2GameEntities, nullptr, Hook_CheckTransmit_Post); +KHOOK_VIRTUAL(DispatchConCommand, &ICvar::DispatchConCommand, g_pCVar, Hook_DispatchConCommand, nullptr); + KHook::Virtual createWorkshopMapGroupHook(0U, Hook_CreateWorkshopMapGroup, nullptr); KHook::Virtual loadEventsFromFileHook(&IGameEventManager2::LoadEventsFromFile, Hook_LoadEventsFromFile, nullptr); KHook::Virtual fireEventHook(&IGameEventManager2::FireEvent, Hook_FireEvent, nullptr); @@ -139,22 +141,6 @@ void SetupGlobalVirtualHook(KHook::Virtual& hook, CLASS* void InitVirtualHooks() { - gameFrameHook.Add(g_pSource2Server); - gameServerSteamAPIActivatedHook.Add(g_pSource2Server); - applyGameSettingsHook.Add(g_pSource2Server); - clientActiveHook.Add(g_pSource2GameClients); - clientDisconnectHook.Add(g_pSource2GameClients); - clientPutInServerHook.Add(g_pSource2GameClients); - clientSettingsChangedHook.Add(g_pSource2GameClients); - onClientConnectedHook.Add(g_pSource2GameClients); - clientConnectHook.Add(g_pSource2GameClients); - clientCommandHook.Add(g_pSource2GameClients); - clientSvcUserMessageHook.Add(g_pSource2GameClients); - postEventAbstractHook.Add(g_gameEventSystem); - startupServerHook.Add(g_pNetworkServerService); - checkTransmitHook.Add(g_pSource2GameEntities); - dispatchConCommandHook.Add(g_pCVar); - SetupVirtualHook(createWorkshopMapGroupHook, "IGameTypes_CreateWorkshopMapGroup", g_pGameTypes); SetupGlobalVirtualHook(loadEventsFromFileHook, g_pCGameEventManagerVTable, modules::server, "CGameEventManager"); @@ -174,21 +160,6 @@ void InitVirtualHooks() void RemoveVirtualHooks() { - gameFrameHook.Remove(g_pSource2Server); - gameServerSteamAPIActivatedHook.Remove(g_pSource2Server); - applyGameSettingsHook.Remove(g_pSource2Server); - clientActiveHook.Remove(g_pSource2GameClients); - clientDisconnectHook.Remove(g_pSource2GameClients); - clientPutInServerHook.Remove(g_pSource2GameClients); - clientSettingsChangedHook.Remove(g_pSource2GameClients); - onClientConnectedHook.Remove(g_pSource2GameClients); - clientConnectHook.Remove(g_pSource2GameClients); - clientCommandHook.Remove(g_pSource2GameClients); - clientSvcUserMessageHook.Remove(g_pSource2GameClients); - postEventAbstractHook.Remove(g_gameEventSystem); - startupServerHook.Remove(g_pNetworkServerService); - checkTransmitHook.Remove(g_pSource2GameEntities); - dispatchConCommandHook.Remove(g_pCVar); loadEventsFromFileHook.RemoveGlobal((IGameEventManager2*)&g_pCGameEventManagerVTable); fireEventHook.RemoveGlobal((IGameEventManager2*)&g_pCGameEventManagerVTable); spawnHook.RemoveGlobal((CEntitySystem*)&g_pCEntitySystemVTable); @@ -434,7 +405,7 @@ KHook::Return Hook_PostEventAbstract(IGameEventSystem* pThis, CSplitScreen uint64 clientMask = *(uint64*)clients & g_playerManager->GetSilenceSoundMask(); - postEventAbstractHook.CallOriginal(pThis, nSlot, bLocalOnly, nClientCount, &clientMask, pEvent, msg, nSize, bufType); + hookPostEventAbstract->CallOriginal(pThis, nSlot, bLocalOnly, nClientCount, &clientMask, pEvent, msg, nSize, bufType); msg->set_weapon_id(weapon_id); msg->set_sound_type(sound_type); @@ -701,7 +672,7 @@ KHook::Return Hook_DispatchConCommand(ICvar* pThis, ConCommandRef cmdHandl if (!bGagged && !bSilent && !bFlooding) { - dispatchConCommandHook.CallOriginal(pThis, cmdHandle, ctx, args); + hookDispatchConCommand->CallOriginal(pThis, cmdHandle, ctx, args); // Reset idle time if message is sent to chat if (g_cvarIdleKickTime.Get() > 0.0f) From 57198cfe02cb7720c603dfe0ed1ceb777cad65a0 Mon Sep 17 00:00:00 2001 From: xen Date: Wed, 16 Sep 2026 20:01:08 +0300 Subject: [PATCH 04/11] Add custom hud cleanup and disconnect callback --- src/commands.cpp | 7 +- src/cs2_sdk/entity/customhudlayout.cpp | 103 ++++++++++++++++++++----- src/cs2_sdk/entity/customhudlayout.h | 34 +++++++- src/cs2fixes.cpp | 2 +- 4 files changed, 123 insertions(+), 23 deletions(-) diff --git a/src/commands.cpp b/src/commands.cpp index 48bfdee1..020ae6a3 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -1183,7 +1183,12 @@ CON_COMMAND_CHAT(uitest, " () - Spawn UI panel") if (args.ArgC() > 2) hLayout->SetDialogVariableString("MyLabel", "CustomText", args[2], player); - hLayout->AddClickCallback([](CCSPlayerController* pController, CCSCustomHudLayout* pHud, std::string sButtonId) { + hLayout->SetDisconnectCallback([](CCSCustomHudLayout* pHud, int slot) { + CCSCustomHudLayout::DefaultOnDisconnect(pHud, slot); + Message("Custom disconnect callback called\n"); + }); + + hLayout->SetClickCallback([](CCSPlayerController* pController, CCSCustomHudLayout* pHud, std::string sButtonId) { ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s clicked %s on %s\n", pController->GetPlayerName().c_str(), sButtonId.c_str(), pHud->GetName()); if (sButtonId == "dismiss_button") diff --git a/src/cs2_sdk/entity/customhudlayout.cpp b/src/cs2_sdk/entity/customhudlayout.cpp index fc15346c..7e740d48 100644 --- a/src/cs2_sdk/entity/customhudlayout.cpp +++ b/src/cs2_sdk/entity/customhudlayout.cpp @@ -20,7 +20,7 @@ #include "customhudlayout.h" #include "entity.h" -std::unordered_map g_mapClickCallbacks; +std::unordered_map CCSCustomHudLayout::sm_mapCustomLayoutCallbacks; CCSCustomHudLayout* CCSCustomHudLayout::Create(std::string sLayout, std::string sTargetName) { @@ -37,34 +37,64 @@ CCSCustomHudLayout* CCSCustomHudLayout::Create(std::string sLayout, std::string pKeyValues->SetString("targetname", sTargetName.c_str()); pLayout->DispatchSpawn(pKeyValues); + pLayout->SetDisconnectCallback(&DefaultOnDisconnect); + return pLayout; } -void CCSCustomHudLayout::ClearClickCallbacks() +void CCSCustomHudLayout::ClearCallbacks() { - g_mapClickCallbacks.clear(); + sm_mapCustomLayoutCallbacks.clear(); } void CCSCustomHudLayout::OnClick(CCSPlayerController* pController, const std::string& sButtonId) { - if (auto it = g_mapClickCallbacks.find(GetHandle().ToInt()); it != g_mapClickCallbacks.end()) - it->second(pController, this, sButtonId); + if (auto it = sm_mapCustomLayoutCallbacks.find(GetHandle().ToInt()); it != sm_mapCustomLayoutCallbacks.end()) + it->second.m_OnClick(pController, this, sButtonId); +} + +void CCSCustomHudLayout::OnClientDisconnect(int slot) +{ + for (auto& info : sm_mapCustomLayoutCallbacks) + { + auto pLayout = CHandle(info.first).Get(); + + if (!pLayout) + { + sm_mapCustomLayoutCallbacks.erase(info.first); + continue; + } + + info.second.m_OnDisconnect(pLayout, slot); + } +} + +void CCSCustomHudLayout::DefaultOnDisconnect(CCSCustomHudLayout* pLayout, int slot) +{ + pLayout->ClearClasses(slot); + pLayout->ClearDialogVariables(slot); + pLayout->SetInputCaptureEnabled(false, slot); } void CCSCustomHudLayout::OnEntityDeleted() { - g_mapClickCallbacks.erase(GetHandle().ToInt()); + sm_mapCustomLayoutCallbacks.erase(GetHandle().ToInt()); } -CCSCustomHudLayoutState& CCSCustomHudLayout::GetLayoutState(CCSPlayerController* pController) +CCSCustomHudLayoutState& CCSCustomHudLayout::GetLayoutState(int nSlot) { - if (!pController) + if (nSlot < 0 || nSlot >= 64) return *m_globalLayoutState; - return *(CCSCustomHudLayoutState*)m_vecPlayerLayoutStates.GetManipulator()(SCHEMA_COLLECTION_MANIPULATOR_ACTION_GET_ELEMENT, m_vecPlayerLayoutStates, pController->GetPlayerSlot(), 0); + return *(CCSCustomHudLayoutState*)m_vecPlayerLayoutStates.GetManipulator()(SCHEMA_COLLECTION_MANIPULATOR_ACTION_GET_ELEMENT, m_vecPlayerLayoutStates, nSlot, 0); } -void CCSCustomHudLayout::SetHasClass(std::string sPanelId, std::string sClassName, bool bHasClass, CCSPlayerController* pController) +CCSCustomHudLayoutState& CCSCustomHudLayout::GetLayoutState(CCSPlayerController* pController) +{ + return pController ? GetLayoutState(pController->GetPlayerSlot()) : *m_globalLayoutState; +} + +void CCSCustomHudLayout::SetHasClass(std::string sPanelId, std::string sClassName, bool bHasClass, int nSlot) { auto panelIndex = m_vecPanelIds->Find(sPanelId.c_str()); @@ -76,7 +106,7 @@ void CCSCustomHudLayout::SetHasClass(std::string sPanelId, std::string sClassNam if (classIndex == -1) classIndex = m_vecClassNames->AddToTail(sClassName.c_str()); - auto& layoutState = GetLayoutState(pController); + auto& layoutState = GetLayoutState(nSlot); HUDPanelHasClass_t hasClass(panelIndex, classIndex, bHasClass); @@ -86,11 +116,14 @@ void CCSCustomHudLayout::SetHasClass(std::string sPanelId, std::string sClassNam layoutState.m_vecHasClasses->AddToTail(hasClass); else layoutState.m_vecHasClasses->Element(hasClassIndex).m_eClassStatus = hasClass.m_eClassStatus; +} - layoutState.m_vecHasClasses.NetworkStateChanged(); +void CCSCustomHudLayout::SetHasClass(std::string sPanelId, std::string sClassName, bool bHasClass, CCSPlayerController* pController) +{ + SetHasClass(sPanelId, sClassName, bHasClass, pController ? pController->GetPlayerSlot() : -1); } -void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, CCSPlayerController* pController) +void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, int nSlot) { auto panelIndex = m_vecPanelIds->Find(sPanelId.c_str()); @@ -102,7 +135,7 @@ void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::stri if (variableIndex == -1) variableIndex = m_vecDialogVariableNames->AddToTail(sVariableName.c_str()); - auto& layoutState = GetLayoutState(pController); + auto& layoutState = GetLayoutState(nSlot); HUDPanelDialogVariableString_t dialogVariable(panelIndex, variableIndex, sValue.c_str(), true); @@ -112,13 +145,42 @@ void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::stri layoutState.m_vecDialogVariableStrings->AddToTail(dialogVariable); else layoutState.m_vecDialogVariableStrings->Element(dialogVariableIndex).m_sValue = sValue.c_str(); +} - layoutState.m_vecDialogVariableStrings.NetworkStateChanged(); +void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, CCSPlayerController* pController) +{ + SetDialogVariableString(sPanelId, sVariableName, sValue, pController ? pController->GetPlayerSlot() : -1); +} + +void CCSCustomHudLayout::ClearClasses(int nSlot) +{ + auto& layoutState = GetLayoutState(nSlot); + static auto pfnManipulator = layoutState.m_vecHasClasses.GetManipulator(); + auto pVecHasClasses = layoutState.m_vecHasClasses(); + pfnManipulator(SCHEMA_COLLECTION_MANIPULATOR_ACTION_REMOVE_MULTIPLE, pVecHasClasses, 0, pVecHasClasses->Count()); +} + +void CCSCustomHudLayout::ClearDialogVariables(int nSlot) +{ + auto& layoutState = GetLayoutState(nSlot); + static auto pfnManipulator = layoutState.m_vecDialogVariableStrings.GetManipulator(); + auto pVecDialogVariableStrings = layoutState.m_vecDialogVariableStrings(); + pfnManipulator(SCHEMA_COLLECTION_MANIPULATOR_ACTION_REMOVE_MULTIPLE, pVecDialogVariableStrings, 0, pVecDialogVariableStrings->Count()); +} + +void CCSCustomHudLayout::SetInputCaptureEnabled(bool bEnable, int nSlot) +{ + GetLayoutState(nSlot).m_bInputCaptureEnabled = bEnable; } void CCSCustomHudLayout::SetInputCaptureEnabled(bool bEnable, CCSPlayerController* pController) { - GetLayoutState(pController).m_bInputCaptureEnabled = bEnable; + SetInputCaptureEnabled(bEnable, pController ? pController->GetPlayerSlot() : -1); +} + +bool CCSCustomHudLayout::IsInputCaptureEnabled(int nSlot) +{ + return GetLayoutState(nSlot).m_bInputCaptureEnabled; } bool CCSCustomHudLayout::IsInputCaptureEnabled(CCSPlayerController* pController) @@ -126,7 +188,12 @@ bool CCSCustomHudLayout::IsInputCaptureEnabled(CCSPlayerController* pController) return GetLayoutState(pController).m_bInputCaptureEnabled; } -void CCSCustomHudLayout::AddClickCallback(CustomHudClickCallback_t callback) +void CCSCustomHudLayout::SetClickCallback(CustomHudClickCallback_t callback) +{ + sm_mapCustomLayoutCallbacks[GetHandle().ToInt()].m_OnClick = callback; +} + +void CCSCustomHudLayout::SetDisconnectCallback(CustomHudDisconnectCallback_t callback) { - g_mapClickCallbacks.insert({GetHandle().ToInt(), callback}); + sm_mapCustomLayoutCallbacks[GetHandle().ToInt()].m_OnDisconnect = callback; } diff --git a/src/cs2_sdk/entity/customhudlayout.h b/src/cs2_sdk/entity/customhudlayout.h index 30f37c5b..1df3374d 100644 --- a/src/cs2_sdk/entity/customhudlayout.h +++ b/src/cs2_sdk/entity/customhudlayout.h @@ -25,6 +25,13 @@ class CCSCustomHudLayout; using CustomHudClickCallback_t = std::function; +using CustomHudDisconnectCallback_t = std::function; + +struct CustomHudLayoutCallbacks_t +{ + CustomHudClickCallback_t m_OnClick = nullptr; + CustomHudDisconnectCallback_t m_OnDisconnect = nullptr; +}; enum EHudPanelClassStatus_t : int { @@ -103,18 +110,39 @@ class CCSCustomHudLayout : public CBaseEntity SCHEMA_FIELD_POINTER(CUtlVector, m_vecDialogVariableNames); static CCSCustomHudLayout* Create(std::string sLayout, std::string sTargetName = ""); - static void ClearClickCallbacks(); + static void ClearCallbacks(); + CCSCustomHudLayoutState& GetLayoutState(int nSlot); CCSCustomHudLayoutState& GetLayoutState(CCSPlayerController* pController = nullptr); + void SetHasClass(std::string sPanelId, std::string sClassName, bool bHasClass, int nSlot); void SetHasClass(std::string sPanelId, std::string sClassName, bool bHasClass, CCSPlayerController* pController = nullptr); + + void SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, int nSlot); void SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, CCSPlayerController* pController = nullptr); + + // NOTE: These will not apply any visible changes to ingame clients, despite them seeing empty vectors. + // They are only applicable for cleanup after clients disconnect. + void ClearClasses(int nSlot = -1); + void ClearDialogVariables(int nSlot = -1); + + void SetInputCaptureEnabled(bool bEnable, int nSlot); void SetInputCaptureEnabled(bool bEnable, CCSPlayerController* pController); - bool IsInputCaptureEnabled(CCSPlayerController* pController); + bool IsInputCaptureEnabled(int nSlot); + bool IsInputCaptureEnabled(CCSPlayerController* pController = nullptr); - void AddClickCallback(CustomHudClickCallback_t callback); + void SetClickCallback(CustomHudClickCallback_t callback); void OnClick(CCSPlayerController* pController, const std::string& sButtonId); + // All layouts spawned with Create will by default have DefaultOnDisconnect as their disconnect callback + // which clears all classes and variables for a slot, so only use this if you want custom logic + void SetDisconnectCallback(CustomHudDisconnectCallback_t callback); + static void OnClientDisconnect(int nSlot); + static void DefaultOnDisconnect(CCSCustomHudLayout* pLayout, int nSlot); + void OnEntityDeleted(); + +private: + static std::unordered_map sm_mapCustomLayoutCallbacks; }; \ No newline at end of file diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index 8e3c8185..a2f0f932 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -395,7 +395,7 @@ void CS2Fixes::OnLevelShutdown() if (g_cvarVoteManagerEnable.Get()) g_pMapVoteSystem->OnLevelShutdown(); - CCSCustomHudLayout::ClearClickCallbacks(); + CCSCustomHudLayout::ClearCallbacks(); } bool CS2Fixes::Pause(char* error, size_t maxlen) From 3929c65a53a46ac71bc5d896d1c0fbe403afe0ab Mon Sep 17 00:00:00 2001 From: Vauff Date: Wed, 16 Sep 2026 17:37:26 -0400 Subject: [PATCH 05/11] Fix several hud cleanup issues --- src/addresses.cpp | 6 ++-- src/cs2_sdk/entity/customhudlayout.cpp | 41 +++++++++++++++++++------- src/cs2fixes.cpp | 2 +- src/detours.cpp | 2 +- src/mempatch.cpp | 2 +- src/utils/khook_helpers.h | 4 +-- src/virtual_hooks.cpp | 3 +- 7 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/addresses.cpp b/src/addresses.cpp index f846e2cf..29eecf9d 100644 --- a/src/addresses.cpp +++ b/src/addresses.cpp @@ -27,10 +27,10 @@ #include "tier0/memdbgon.h" -#define RESOLVE_SIG(name, variable) \ +#define RESOLVE_SIG(name, variable) \ variable = (decltype(variable))g_GameConfig->ResolveSignature(name); \ - if (!variable) \ - return false; \ + if (!variable) \ + return false; \ Message("Found %s at 0x%p\n", name, variable); #define RESOLVE_SF(scriptDesc, funcName, variable) \ diff --git a/src/cs2_sdk/entity/customhudlayout.cpp b/src/cs2_sdk/entity/customhudlayout.cpp index 7e740d48..72f25a87 100644 --- a/src/cs2_sdk/entity/customhudlayout.cpp +++ b/src/cs2_sdk/entity/customhudlayout.cpp @@ -55,17 +55,20 @@ void CCSCustomHudLayout::OnClick(CCSPlayerController* pController, const std::st void CCSCustomHudLayout::OnClientDisconnect(int slot) { - for (auto& info : sm_mapCustomLayoutCallbacks) + auto iterator = sm_mapCustomLayoutCallbacks.begin(); + + while (iterator != sm_mapCustomLayoutCallbacks.end()) { - auto pLayout = CHandle(info.first).Get(); + auto pLayout = CHandle(iterator->first).Get(); if (!pLayout) { - sm_mapCustomLayoutCallbacks.erase(info.first); + iterator = sm_mapCustomLayoutCallbacks.erase(iterator); continue; } - info.second.m_OnDisconnect(pLayout, slot); + iterator->second.m_OnDisconnect(pLayout, slot); + iterator++; } } @@ -142,9 +145,14 @@ void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::stri auto dialogVariableIndex = layoutState.m_vecDialogVariableStrings->Find(dialogVariable); if (dialogVariableIndex == -1) + { layoutState.m_vecDialogVariableStrings->AddToTail(dialogVariable); + } else + { layoutState.m_vecDialogVariableStrings->Element(dialogVariableIndex).m_sValue = sValue.c_str(); + layoutState.m_vecDialogVariableStrings->Element(dialogVariableIndex).m_bIsSet = true; + } } void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::string sVariableName, std::string sValue, CCSPlayerController* pController) @@ -155,17 +163,30 @@ void CCSCustomHudLayout::SetDialogVariableString(std::string sPanelId, std::stri void CCSCustomHudLayout::ClearClasses(int nSlot) { auto& layoutState = GetLayoutState(nSlot); - static auto pfnManipulator = layoutState.m_vecHasClasses.GetManipulator(); - auto pVecHasClasses = layoutState.m_vecHasClasses(); - pfnManipulator(SCHEMA_COLLECTION_MANIPULATOR_ACTION_REMOVE_MULTIPLE, pVecHasClasses, 0, pVecHasClasses->Count()); + + for (int i = 0; i < layoutState.m_vecHasClasses->Count(); i++) + { + if (layoutState.m_vecHasClasses->Element(i).m_eClassStatus == k_eHudPanelClassStatus_Undefined) + continue; + + auto pClass = (HUDPanelHasClass_t*)layoutState.m_vecHasClasses.GetManipulator()(SCHEMA_COLLECTION_MANIPULATOR_ACTION_GET_ELEMENT, layoutState.m_vecHasClasses, i, 0); + pClass->m_eClassStatus = k_eHudPanelClassStatus_Undefined; + } } void CCSCustomHudLayout::ClearDialogVariables(int nSlot) { auto& layoutState = GetLayoutState(nSlot); - static auto pfnManipulator = layoutState.m_vecDialogVariableStrings.GetManipulator(); - auto pVecDialogVariableStrings = layoutState.m_vecDialogVariableStrings(); - pfnManipulator(SCHEMA_COLLECTION_MANIPULATOR_ACTION_REMOVE_MULTIPLE, pVecDialogVariableStrings, 0, pVecDialogVariableStrings->Count()); + + for (int i = 0; i < layoutState.m_vecDialogVariableStrings->Count(); i++) + { + if (!layoutState.m_vecDialogVariableStrings->Element(i).m_bIsSet) + continue; + + auto pString = (HUDPanelDialogVariableString_t*)layoutState.m_vecDialogVariableStrings.GetManipulator()(SCHEMA_COLLECTION_MANIPULATOR_ACTION_GET_ELEMENT, layoutState.m_vecDialogVariableStrings, i, 0); + pString->m_bIsSet = false; + pString->m_sValue = ""; + } } void CCSCustomHudLayout::SetInputCaptureEnabled(bool bEnable, int nSlot) diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index a2f0f932..a927b82b 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -27,7 +27,6 @@ #include "common.h" #include "ctimer.h" #include "cvarwhitelist.h" -#include "khook_helpers.h" #include "discord.h" #include "entities.h" #include "entity/customhudlayout.h" @@ -41,6 +40,7 @@ #include "hud_manager.h" #include "icvar.h" #include "idlemanager.h" +#include "khook_helpers.h" #include "map_votes.h" #include "mapmigrations.h" #include "networkstringtabledefs.h" diff --git a/src/detours.cpp b/src/detours.cpp index 2253b229..904e1c60 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -30,7 +30,6 @@ #include "customio.h" #include "cvarwhitelist.h" #include "detours.h" -#include "khook_helpers.h" #include "entities.h" #include "entity/cbasemodelentity.h" #include "entity/cbeam.h" @@ -47,6 +46,7 @@ #include "gameconfig.h" #include "igameevents.h" #include "irecipientfilter.h" +#include "khook_helpers.h" #include "map_votes.h" #include "mapmigrations.h" #include "module.h" diff --git a/src/mempatch.cpp b/src/mempatch.cpp index 5db8fb4a..2dcd025e 100644 --- a/src/mempatch.cpp +++ b/src/mempatch.cpp @@ -19,10 +19,10 @@ #include "mempatch.h" #include "common.h" +#include "khook.hpp" #include "tier0/dbg.h" #include "tier1/strtools.h" #include "utils/module.h" -#include "khook.hpp" #include "tier0/memdbgon.h" diff --git a/src/utils/khook_helpers.h b/src/utils/khook_helpers.h index 1d9e540a..774c1db0 100644 --- a/src/utils/khook_helpers.h +++ b/src/utils/khook_helpers.h @@ -156,7 +156,7 @@ class CKHookVirtual : public CKHookBase { GetKHookList().push_back(this); } - + ~CKHookVirtual() { if (m_hook.IsActive()) @@ -170,7 +170,7 @@ class CKHookVirtual : public CKHookBase g_bRequiredInitLoaded = false; return; } - + m_hook.Add(m_pInstance); } diff --git a/src/virtual_hooks.cpp b/src/virtual_hooks.cpp index 47d762e1..53ca54bc 100644 --- a/src/virtual_hooks.cpp +++ b/src/virtual_hooks.cpp @@ -40,11 +40,11 @@ #include "icvar.h" #include "idlemanager.h" #include "iserver.h" +#include "khook_helpers.h" #include "leader.h" #include "map_votes.h" #include "mapmigrations.h" #include "module.h" -#include "khook_helpers.h" #include "networkstringtabledefs.h" #include "panoramavote.h" #include "playermanager.h" @@ -264,6 +264,7 @@ KHook::Return Hook_ClientDisconnect_Post(IServerGameClients* pThis, CPlaye if (reason != NETWORK_DISCONNECT_LOOPSHUTDOWN && reason != NETWORK_DISCONNECT_SHUTDOWN) g_pAdminSystem->AddDisconnectedPlayer(pszName, xuid, pPlayer ? pPlayer->GetIpAddress() : ""); + CCSCustomHudLayout::OnClientDisconnect(slot.Get()); g_playerManager->OnClientDisconnect(slot); return {KHook::Action::Ignore}; From 76c7f48ec5c7f8ff7479390fc434e9e2dc5fa423 Mon Sep 17 00:00:00 2001 From: Vauff Date: Wed, 16 Sep 2026 21:22:48 -0400 Subject: [PATCH 06/11] Fix compile warnings --- CS2Fixes.vcxproj | 2 +- src/utils/khook_helpers.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj index 61c8a9b8..2f1965e5 100644 --- a/CS2Fixes.vcxproj +++ b/CS2Fixes.vcxproj @@ -311,7 +311,7 @@ - + diff --git a/src/utils/khook_helpers.h b/src/utils/khook_helpers.h index 774c1db0..fd004856 100644 --- a/src/utils/khook_helpers.h +++ b/src/utils/khook_helpers.h @@ -36,7 +36,7 @@ class CKHookFunction : public CKHookBase using fnCallback = KHook::Return (*)(ARGS...); CKHookFunction(const char* pSignature, fnCallback cbPre, fnCallback cbPost) : - m_pSignatureName(pSignature), m_hook(cbPre, cbPost) + m_hook(cbPre, cbPost), m_pSignatureName(pSignature) { GetKHookList().push_back(this); } @@ -94,7 +94,7 @@ class CKHookMember : public CKHookBase using fnCallback = KHook::Return (*)(CLASS*, ARGS...); CKHookMember(const char* pSignature, fnCallback cbPre, fnCallback cbPost) : - m_pSignatureName(pSignature), m_hook(cbPre, cbPost) + m_hook(cbPre, cbPost), m_pSignatureName(pSignature) { GetKHookList().push_back(this); } From 05191ebc5e1218d3b0710a6684d4c6eef245d295 Mon Sep 17 00:00:00 2001 From: Vauff Date: Thu, 17 Sep 2026 04:20:25 -0400 Subject: [PATCH 07/11] Overhaul map migrations & custom cfg parser - Overhauled map migrations to fix an out-of-bounds rendermode issue & run cleanly during map load when possible, similar to Stripper - Overhauled the custom cfg parser to skip the command buffer, and process changes immediately - Added an additional early cfg execution, to ensure map migrations has access to correct cvar values for the upcoming map --- CS2Fixes.vcxproj | 1 + CS2Fixes.vcxproj.filters | 3 + gamedata/cs2fixes.jsonc | 7 +++ src/addresses.cpp | 1 + src/addresses.h | 1 + src/cfgparser.cpp | 61 ++++++++++++++----- src/cfgparser.h | 2 + src/cs2_sdk/map.h | 40 ++++++++++++ src/cs2fixes.cpp | 1 + src/detours.cpp | 22 +++++++ src/detours.h | 2 + src/entitylistener.cpp | 2 - src/gameconfig.cpp | 2 + src/map_votes.cpp | 69 ++++++++++++++++----- src/map_votes.h | 4 +- src/mapmigrations.cpp | 127 ++++++++++++--------------------------- src/mapmigrations.h | 34 ++--------- src/virtual_hooks.cpp | 26 ++++---- src/virtual_hooks.h | 3 +- 19 files changed, 241 insertions(+), 167 deletions(-) create mode 100644 src/cs2_sdk/map.h diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj index 2f1965e5..40a1b174 100644 --- a/CS2Fixes.vcxproj +++ b/CS2Fixes.vcxproj @@ -270,6 +270,7 @@ + diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters index 5b04460d..8da178c1 100644 --- a/CS2Fixes.vcxproj.filters +++ b/CS2Fixes.vcxproj.filters @@ -412,6 +412,9 @@ Header Files\cs2_sdk + + Header Files\cs2_sdk + Header Files diff --git a/gamedata/cs2fixes.jsonc b/gamedata/cs2fixes.jsonc index bb983481..186bea51 100644 --- a/gamedata/cs2fixes.jsonc +++ b/gamedata/cs2fixes.jsonc @@ -372,6 +372,13 @@ "library": "server", "windows": "48 83 EC ? 4C 8B C2 48 8D 0D ? ? ? ? 48 8D 54 24 ? FF", "linux": "55 48 89 F2 48 8D 35 ? ? ? ? 48 89 E5 48 83 EC" + }, + // "CWorldRendererMgr::CreateWorld_Internal( %s ): Blocking load because marked for deletion during load" + "CWorldRendererMgr_CreateWorld_Internal": + { + "library": "worldrenderer", + "windows": "48 89 5C 24 ? 48 89 54 24 ? 55 56 57 48 81 EC", + "linux": "55 48 89 E5 41 56 41 55 41 54 49 89 FC 53 48 89 F3 48 83 EC ? F6 46" } }, "Offsets": diff --git a/src/addresses.cpp b/src/addresses.cpp index 29eecf9d..9b4ce84d 100644 --- a/src/addresses.cpp +++ b/src/addresses.cpp @@ -51,6 +51,7 @@ bool addresses::Initialize() modules::networksystem = new CModule(ROOTBIN, "networksystem"); modules::vphysics2 = new CModule(ROOTBIN, "vphysics2"); modules::matchmaking = new CModule(GAMEBIN, "matchmaking"); + modules::worldrenderer = new CModule(ROOTBIN, "worldrenderer"); modules::client = nullptr; if (!CommandLine()->HasParm("-dedicated")) diff --git a/src/addresses.h b/src/addresses.h index b69d453a..09ac769e 100644 --- a/src/addresses.h +++ b/src/addresses.h @@ -36,6 +36,7 @@ namespace modules inline CModule* networksystem; inline CModule* vphysics2; inline CModule* matchmaking; + inline CModule* worldrenderer; #ifdef _WIN32 inline CModule* hammer; #endif diff --git a/src/cfgparser.cpp b/src/cfgparser.cpp index adfe3adf..5b80cf7a 100644 --- a/src/cfgparser.cpp +++ b/src/cfgparser.cpp @@ -24,18 +24,19 @@ CCfgParser* g_pCfgParser = nullptr; -CON_COMMAND_F(exec_custom, " - Execute a cfg through the custom cfg parser", FCVAR_SPONLY | FCVAR_LINKED_CONCOMMAND) +void CCfgParser::PreLevelLoad(const char* pszMapName) { - if (args.ArgC() < 2) - { - Message("Usage: exec_custom \n"); - return; - } - - g_pCfgParser->ParseCfg(args[1]); + // Make sure convars are set before map load, currently used by map migrations + ExecuteConfigs(pszMapName); } void CCfgParser::ApplyGameSettings(const char* pszMapName) +{ + // Execute again for good measure, because this is when configs would normally execute + ExecuteConfigs(pszMapName); +} + +void CCfgParser::ExecuteConfigs(const char* pszMapName) { // Run plugin cfg g_pCfgParser->ParseCfg("cs2fixes/cs2fixes"); @@ -47,10 +48,9 @@ void CCfgParser::ApplyGameSettings(const char* pszMapName) return; // Run map cfg (if present) - // We call ParseCfg indirectly through exec_custom, so any commands within the map cfg will be added to the command buffer after nested executes in previous configs - char cmd[MAX_PATH]; - V_snprintf(cmd, sizeof(cmd), "exec_custom cs2fixes/maps/%s", pszMapName); - g_pEngineServer2->ServerCommand(cmd); + char szCfgPath[MAX_PATH]; + V_snprintf(szCfgPath, sizeof(szCfgPath), "cs2fixes/maps/%s", pszMapName); + ParseCfg(szCfgPath); } void CCfgParser::ParseCfg(const char* pszCfgPath) @@ -71,10 +71,39 @@ void CCfgParser::ParseCfg(const char* pszCfgPath) while (std::getline(cfgFile, strCommand)) { - if (!strCommand.empty() && strCommand.back() == '\r') - strCommand.pop_back(); + CCommand args; + + if (!args.Tokenize(strCommand.c_str()) || !args.ArgC()) + continue; + + if (!V_strcasecmp(args[0], "exec_custom")) + { + if (args.ArgC() < 2) + Message("Usage: exec_custom \n"); + else + ParseCfg(args[1]); + + continue; + } + + ConCommandRef command(args[0], true); + ConVarRefAbstract convar(args[0], true); + + if (command.IsValidRef()) + { + CCommandContext context(CT_FIRST_SPLITSCREEN_CLIENT, -1); + command.Dispatch(context, args); + continue; + } + + if (convar.IsValidRef()) + { + if (args.ArgC() > 1 && !convar.SetString(args[1])) + Message("Failed to execute \"%s %s\"\n", args[0], args[1]); + + continue; + } - if (!strCommand.empty()) - g_pEngineServer2->ServerCommand(strCommand.c_str()); + Message("Unknown command \"%s\"\n", args[0]); } } diff --git a/src/cfgparser.h b/src/cfgparser.h index 78433b7d..53f3296c 100644 --- a/src/cfgparser.h +++ b/src/cfgparser.h @@ -22,7 +22,9 @@ class CCfgParser { public: + void PreLevelLoad(const char* pszMapName); void ApplyGameSettings(const char* pszMapName); + void ExecuteConfigs(const char* pszMapName); void ParseCfg(const char* pszCfgPath); }; diff --git a/src/cs2_sdk/map.h b/src/cs2_sdk/map.h new file mode 100644 index 00000000..74bbe3e8 --- /dev/null +++ b/src/cs2_sdk/map.h @@ -0,0 +1,40 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2026 Source2ZE + * ============================================================================= + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License, version 3.0, as published by the + * Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +#pragma once + +#include "utlstring.h" + +class CSingleWorldRep +{ +private: + void* vtable; + +public: + CUtlString m_name; + char pad[0x20]; + void* m_pCWorld; +}; + +struct LumpData +{ + CUtlString m_name; + char pad[0x18]; + CKV3Arena* m_allocatorContext; +}; diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index a927b82b..8e3290d2 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -100,6 +100,7 @@ bool CS2Fixes::Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool GET_V_IFACE_ANY(GetServerFactory, g_pSource2GameEntities, ISource2GameEntities, SOURCE2GAMEENTITIES_INTERFACE_VERSION); GET_V_IFACE_ANY(GetServerFactory, g_pSource2GameClients, IServerGameClients, SOURCE2GAMECLIENTS_INTERFACE_VERSION); GET_V_IFACE_ANY(GetEngineFactory, g_pNetworkServerService, INetworkServerService, NETWORKSERVERSERVICE_INTERFACE_VERSION); + GET_V_IFACE_ANY(GetEngineFactory, g_pEngineServiceMgr, IEngineServiceMgr, ENGINESERVICEMGR_INTERFACE_VERSION); GET_V_IFACE_ANY(GetEngineFactory, g_gameEventSystem, IGameEventSystem, GAMEEVENTSYSTEM_INTERFACE_VERSION); GET_V_IFACE_ANY(GetEngineFactory, g_pNetworkMessages, INetworkMessages, NETWORKMESSAGES_INTERFACE_VERSION); GET_V_IFACE_ANY(GetEngineFactory, g_pGameTypes, IGameTypes, GAMETYPES_INTERFACE_VERSION); diff --git a/src/detours.cpp b/src/detours.cpp index 904e1c60..4349b165 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -92,6 +92,7 @@ KHOOK_MEMBER(CCSGameRules_GoToIntermission, Detour_CCSGameRules_GoToIntermission KHOOK_MEMBER(SetBeamOrigin, Detour_SetBeamOrigin, nullptr); KHOOK_MEMBER(SetBeamEndPos, Detour_SetBeamEndPos, nullptr); KHOOK_FUNCTION(IsCommandWhitelisted, Detour_IsCommandWhitelisted, nullptr); +KHOOK_MEMBER(CWorldRendererMgr_CreateWorld_Internal, nullptr, Detour_CWorldRendererMgr_CreateWorld_Internal_Post); std::vector& GetKHookList() { @@ -931,3 +932,24 @@ KHook::Return Detour_IsCommandWhitelisted(void* pAddonManager, const char* return {KHook::Action::Supersede, g_pConvarWhitelist->IsWhitelisted(pszCommandName)}; } + +KHook::Return Detour_CWorldRendererMgr_CreateWorld_Internal_Post(IWorldRendererMgr* pThis, CSingleWorldRep* singleWorld) +{ + // The world can fail to be created and the function will return nullptr. + if (!KHook::GetOriginalReturn()) + return {KHook::Action::Ignore}; + + auto pWorld = singleWorld->m_pCWorld; + auto vecLumpData = (CUtlVector*)((uint8_t*)pWorld + 0x1E0); + + FOR_EACH_VEC(*vecLumpData, i) + { + auto& lump = (*vecLumpData)[i]; + auto lumpData = *(LumpData**)lump; + auto vecEntityKeyValues = (CUtlVector*)((uint8_t*)lumpData + 0x1220); + + g_pMapMigrations->RunMigrations(vecEntityKeyValues); + } + + return {KHook::Action::Ignore}; +} diff --git a/src/detours.h b/src/detours.h index 75a94f24..b31a9f5d 100644 --- a/src/detours.h +++ b/src/detours.h @@ -20,6 +20,7 @@ #pragma once #include "cs2_sdk/entityio.h" #include "khook.hpp" +#include "map.h" #include class CCheckTransmitInfo; @@ -120,3 +121,4 @@ KHook::Return Detour_CCSGameRules_GoToIntermission(CCSGameRules* pThis, bo KHook::Return Detour_SetBeamOrigin(CBeam* pThis, const Vector* pVecWorldPosition); KHook::Return Detour_SetBeamEndPos(CBeam* pThis, const Vector* pVecWorldPosition); KHook::Return Detour_IsCommandWhitelisted(void* pAddonManager, const char* pszCommandName); +KHook::Return Detour_CWorldRendererMgr_CreateWorld_Internal_Post(IWorldRendererMgr* pThis, CSingleWorldRep* pSingleWorld); diff --git a/src/entitylistener.cpp b/src/entitylistener.cpp index e4f72ffd..ab5d84a0 100644 --- a/src/entitylistener.cpp +++ b/src/entitylistener.cpp @@ -27,7 +27,6 @@ #include "entityclass.h" #include "entwatch.h" #include "gameconfig.h" -#include "mapmigrations.h" #include "plat.h" CEntityListener* g_pEntityListener = nullptr; @@ -45,7 +44,6 @@ void CEntityListener::OnEntitySpawned(CEntityInstance* pEntity) reinterpret_cast(pEntity)->SetCollisionGroup(COLLISION_GROUP_DEBRIS); EntityHandler_OnEntitySpawned(reinterpret_cast(pEntity)); - g_pMapMigrations->OnEntitySpawned_Post(reinterpret_cast(pEntity)); if (g_cvarEnableEntWatch.Get()) EW_OnEntitySpawned(pEntity); diff --git a/src/gameconfig.cpp b/src/gameconfig.cpp index b4295bac..5113c301 100644 --- a/src/gameconfig.cpp +++ b/src/gameconfig.cpp @@ -161,6 +161,8 @@ CModule** CGameConfig::GetModule(const char* name) return &modules::networksystem; else if (strcmp(library, "matchmaking") == 0) return &modules::matchmaking; + else if (strcmp(library, "worldrenderer") == 0) + return &modules::worldrenderer; #ifdef _WIN32 else if (strcmp(library, "hammer") == 0) return &modules::hammer; diff --git a/src/map_votes.cpp b/src/map_votes.cpp index 72dd2479..8c3df85f 100644 --- a/src/map_votes.cpp +++ b/src/map_votes.cpp @@ -24,6 +24,7 @@ #include "entity/cgamerules.h" #include "eventlistener.h" #include "iserver.h" +#include "mapmigrations.h" #include "playermanager.h" #include "steam/steam_gameserver.h" #include "strtools.h" @@ -73,6 +74,10 @@ CON_COMMAND_CHAT_FLAGS(map, " - Change map", ADMFLAG_CHANGEMAP) return -1.0f; }); + // Map migrations need map info ahead of time + if (pMap->GetWorkshopId() != 0) + CMapSystemWorkshopDetailsQuery::Create(pMap->GetWorkshopId()); + ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Changing map to \x06%s\x01...", pMap->GetName()); }); } @@ -340,7 +345,13 @@ void CMapVoteSystem::StartVote() for (int i = 0; i < m_iVoteSize; i++) { int iMapIndex = g_pGameRules->m_nEndMatchMapGroupVoteOptions[i]; + uint64 iWorkshopId = GetMapByIndex(iMapIndex)->GetWorkshopId(); + Message("The %d-th chosen map index %d is %s\n", i, iMapIndex, GetMapName(iMapIndex)); + + // Map migrations need map info ahead of time + if (iWorkshopId != 0) + CMapSystemWorkshopDetailsQuery::Create(iWorkshopId); } static ConVarRefAbstract mp_endmatch_votenextleveltime("mp_endmatch_votenextleveltime"); @@ -858,7 +869,10 @@ void CMapVoteSystem::ForceNextMap(CCSPlayerController* pController, const char* return; } - // When found, print the map and store the forced map + // Map migrations need map info ahead of time + if (pMap->GetWorkshopId() != 0) + CMapSystemWorkshopDetailsQuery::Create(pMap->GetWorkshopId()); + g_pMapVoteSystem->SetForcedNextMap(pMap); ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "\x06%s \x01has been forced as the next map.\n", g_pMapVoteSystem->GetForcedNextMap()->GetName()); }); @@ -1144,9 +1158,6 @@ void CMapVoteSystem::OnPlayerCountChange() void CMapVoteSystem::ApplyGameSettings(const char* pszMapName, uint64 iWorkshopId) { - if (!g_cvarVoteManagerEnable.Get()) - return; - auto pair = GetMapInfoByIdentifiers(pszMapName, iWorkshopId); if (pair.first != -1) @@ -1154,6 +1165,9 @@ void CMapVoteSystem::ApplyGameSettings(const char* pszMapName, uint64 iWorkshopI else SetCurrentMap(std::make_shared(pszMapName, iWorkshopId, pszMapName[0] == '\0' && iWorkshopId == 0)); + if (!g_cvarVoteManagerEnable.Get()) + return; + ProcessGroupCooldowns(); } @@ -1396,8 +1410,7 @@ std::shared_ptr CMapSystemWorkshopDetailsQuery:: { if (!GetSteamUGC()) { - Panic("A workshop map query was attempted on null ISteamUGC, returning early.\n"); - ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Failed to query workshop map information for ID \x06%llu\x01.", iWorkshopId); + ReportCreateFailure(iWorkshopId, pController, callbackSuccess); return nullptr; } @@ -1406,13 +1419,20 @@ std::shared_ptr CMapSystemWorkshopDetailsQuery:: if (hQuery == k_UGCQueryHandleInvalid) { - ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Failed to query workshop map information for ID \x06%llu\x01.", iWorkshopId); + ReportCreateFailure(iWorkshopId, pController, callbackSuccess); return nullptr; } GetSteamUGC()->SetAllowCachedResponse(hQuery, 0); SteamAPICall_t hCall = GetSteamUGC()->SendQueryUGCRequest(hQuery); + if (hCall == k_uAPICallInvalid) + { + GetSteamUGC()->ReleaseQueryUGCRequest(hQuery); + ReportCreateFailure(iWorkshopId, pController, callbackSuccess); + return nullptr; + } + auto pQuery = std::make_shared(hQuery, iWorkshopId, pController, callbackSuccess); g_pMapVoteSystem->AddWorkshopDetailsQuery(pQuery); pQuery->m_CallResult.Set(hCall, pQuery.get(), &CMapSystemWorkshopDetailsQuery::OnQueryCompleted); @@ -1420,23 +1440,40 @@ std::shared_ptr CMapSystemWorkshopDetailsQuery:: return pQuery; } +void CMapSystemWorkshopDetailsQuery::ReportCreateFailure(uint64 iWorkshopId, CCSPlayerController* pController, QueryCallback_t callbackSuccess) +{ + Message("Failed to query workshop map information for ID %llu\n", iWorkshopId); + + if (callbackSuccess) + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Failed to query workshop map information for ID \x06%llu\x01.", iWorkshopId); +} + void CMapSystemWorkshopDetailsQuery::OnQueryCompleted(SteamUGCQueryCompleted_t* pCompletedQuery, bool bFailed) { CCSPlayerController* pController = m_hController.Get(); SteamUGCDetails_t details; - // Only allow null controller if controller was originally null (console) - if (m_bConsole || pController) + // Is this a player map lookup? Only allow null controller if controller was originally null (console) + bool bMapLookupActive = m_callbackSuccess && (m_bConsole || pController); + + if (bFailed || pCompletedQuery->m_eResult != k_EResultOK || pCompletedQuery->m_unNumResultsReturned < 1 || !GetSteamUGC()->GetQueryUGCResult(pCompletedQuery->m_handle, 0, &details) || details.m_eResult != k_EResultOK) { - if (bFailed || pCompletedQuery->m_eResult != k_EResultOK || pCompletedQuery->m_unNumResultsReturned < 1 || !GetSteamUGC()->GetQueryUGCResult(pCompletedQuery->m_handle, 0, &details) || details.m_eResult != k_EResultOK) - { + if (bMapLookupActive) ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "Failed to query workshop map information for ID \x06%llu\x01.", m_iWorkshopId); - } - else if (details.m_nConsumerAppID != 730 || details.m_eFileType != k_EWorkshopFileTypeCommunity) - { - ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "The ID \x06%llu\x01 is not a valid CS2 workshop map.", m_iWorkshopId); - } else + Message("Failed to query workshop map information for ID %llu\n", m_iWorkshopId); + } + else if (details.m_nConsumerAppID != 730 || details.m_eFileType != k_EWorkshopFileTypeCommunity) + { + if (bMapLookupActive) + ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "The ID \x06%llu\x01 is not a valid CS2 workshop map.", m_iWorkshopId); + } + else + { + // Map migrations also need to know when maps were updated + g_pMapMigrations->UpdateMapUpdateTime(m_iWorkshopId, details.m_rtimeUpdated); + + if (bMapLookupActive) { // Try to get a head start on downloading the map if needed GetSteamUGC()->DownloadItem(m_iWorkshopId, false); diff --git a/src/map_votes.h b/src/map_votes.h index ac55efac..1742a812 100644 --- a/src/map_votes.h +++ b/src/map_votes.h @@ -131,7 +131,6 @@ class CGroup float m_fCooldown; }; -// Implementation is a bit hardcoded for HandlePlayerMapLookup use class CMapSystemWorkshopDetailsQuery : public std::enable_shared_from_this { public: @@ -149,7 +148,8 @@ class CMapSystemWorkshopDetailsQuery : public std::enable_shared_from_this Create(uint64 iWorkshopId, CCSPlayerController* pController, QueryCallback_t callbackSuccess); + static std::shared_ptr Create(uint64 iWorkshopId, CCSPlayerController* pController = nullptr, QueryCallback_t callbackSuccess = nullptr); + static void ReportCreateFailure(uint64 iWorkshopId, CCSPlayerController* pController, QueryCallback_t callbackSuccess); private: void OnQueryCompleted(SteamUGCQueryCompleted_t* pCompletedQuery, bool bFailed); diff --git a/src/mapmigrations.cpp b/src/mapmigrations.cpp index 03db6b16..c21d26fe 100644 --- a/src/mapmigrations.cpp +++ b/src/mapmigrations.cpp @@ -20,8 +20,8 @@ #include "mapmigrations.h" #include "cs2fixes.h" #include "entity.h" -#include "entity/cbasemodelentity.h" #include "entity/cbasetoggle.h" +#include "map_votes.h" #include "utils.h" #include "vprof.h" @@ -33,77 +33,72 @@ const time_t g_time20260420 = 1776725888; CConVar g_cvarMapMigrations20260121("cs2f_mapmigrations_20260121", FCVAR_NONE, "Current mode for 2026-01-21 CS2 update map migrations. [0 = Force disabled, 1 = Force enabled, 2 = Automatically enabled for maps updated before 2026-01-21 & disabled if updated after]", 2); CConVar g_cvarMapMigrations20260420("cs2f_mapmigrations_20260420", FCVAR_NONE, "Current mode for 2026-04-20 CS2 update map migrations. [0 = Force disabled, 1 = Force enabled, 2 = Automatically enabled for maps updated before 2026-04-20 & disabled if updated after]", 2); -void CMapMigrations::ApplyGameSettings(uint64 iWorkshopId) +void CMapMigrations::PreLevelLoad(uint64 iWorkshopId) { m_timeMapUpdated = std::numeric_limits::max(); // Don't run on default maps if (iWorkshopId != 0) - CMapMigrationWorkshopDetailsQuery::Create(iWorkshopId); + { + if (m_mapUpdateTimes.contains(iWorkshopId)) + { + m_timeMapUpdated = m_mapUpdateTimes[iWorkshopId]; + } + else + { + Message("Skipping pre-load map migrations for %llu, update time is not available\n", iWorkshopId); + + // Try to get the update time anyways, for later migrations + CMapSystemWorkshopDetailsQuery::Create(iWorkshopId); + } + } } void CMapMigrations::OnRoundPrestart() { - m_vecModelEntitiesUsingRendermodeEnum.clear(); m_vecEquippedWeapons.clear(); } -void CMapMigrations::OnEntitySpawned_Pre(CBaseEntity* pEntity, const CEntityKeyValues* pKeyValues) -{ - // Stupid workaround for CEntityKeyValues being inaccessible after entity spawn - // We need access to this in 2026-01-21 rendermode migrations when called from UpdateMapUpdateTime - if (pEntity->AsBaseModelEntity() && V_StringToInt32(pKeyValues->GetString("rendermode"), -1, NULL, NULL, PARSING_FLAG_SKIP_WARNING) == -1) - m_vecModelEntitiesUsingRendermodeEnum.push_back(pEntity->GetHandle()); -} - -void CMapMigrations::OnEntitySpawned_Post(CBaseEntity* pEntity) -{ - RunMigrations(pEntity); -} - void CMapMigrations::OnEquipWeapon(CBasePlayerWeapon* pWeapon) { if (Migrations20260420Enabled()) Migrations_20260420(pWeapon); } -void CMapMigrations::RunMigrations(CBaseEntity* pEntity) +void CMapMigrations::RunMigrations(CUtlVector* pVecEntityKeyValues) { if (g_cvarMapMigrations20260121.Get() == 1 || (g_cvarMapMigrations20260121.Get() == 2 && m_timeMapUpdated < g_time20260121)) - Migrations_20260121(pEntity); + Migrations_20260121(pVecEntityKeyValues); } -void CMapMigrations::Migrations_20260121(CBaseEntity* pEntity) +void CMapMigrations::Migrations_20260121(CUtlVector* pVecEntityKeyValues) { - if (!V_strcasecmp(pEntity->GetClassname(), "func_door_rotating")) + FOR_EACH_VEC(*pVecEntityKeyValues, i) { - uint32 spawnFlags = pEntity->m_spawnflags(); + auto pKeyValues = (*pVecEntityKeyValues)[i]; - if (!(spawnFlags & SF_DOOR_ONEWAY)) - pEntity->m_spawnflags = spawnFlags + SF_DOOR_ONEWAY; - } + if (!V_strcasecmp(pKeyValues->GetString("classname"), "func_door_rotating") && pKeyValues->HasValue("spawnflags")) + { + uint32 spawnFlags = pKeyValues->GetUint("spawnflags"); + pKeyValues->SetUint("spawnflags", spawnFlags | SF_DOOR_ONEWAY); + } - CBaseModelEntity* pModelEntity = pEntity->AsBaseModelEntity(); + if (!pKeyValues->HasValue("rendermode")) + continue; - if (pModelEntity) - { - // Also need to make sure the entity is using index-based rendermodes, and not enum-based ones (which will already automatically migrate correctly) - // This differentiation is lost after entity spawn, so we had to check the original keyvalue earlier instead - for (int i = 0; i < m_vecModelEntitiesUsingRendermodeEnum.size(); i++) - if (m_vecModelEntitiesUsingRendermodeEnum[i] == pModelEntity->GetHandle()) - return; + int renderMode = V_StringToInt32(pKeyValues->GetString("rendermode"), -1, NULL, NULL, PARSING_FLAG_SKIP_WARNING); - RenderMode_t renderMode = pModelEntity->m_nRenderMode(); + // Enum-named render modes already migrate correctly + if (renderMode == -1) + continue; - // Legacy kRenderTransAlpha if (renderMode == 4) - pModelEntity->m_nRenderMode = kRenderTransAlpha; - // Legacy kRenderNone + pKeyValues->SetString("rendermode", "kRenderTransAlpha"); else if (renderMode == 10) - pModelEntity->m_nRenderMode = kRenderNone; + pKeyValues->SetString("rendermode", "kRenderNone"); // All other removed render modes, fall back to normal else if (renderMode > kRenderNormal) - pModelEntity->m_nRenderMode = kRenderNormal; + pKeyValues->SetString("rendermode", "kRenderNormal"); } } @@ -156,55 +151,11 @@ bool CMapMigrations::Migrations20260420Enabled() return g_cvarMapMigrations20260420.Get() == 1 || (g_cvarMapMigrations20260420.Get() == 2 && m_timeMapUpdated < g_time20260420); } -void CMapMigrations::UpdateMapUpdateTime(time_t timeMapUpdated) -{ - m_timeMapUpdated = timeMapUpdated; - - CBaseEntity* pTarget = nullptr; - - // May be called late, so also check any existing entities first - while ((pTarget = UTIL_FindEntityByClassname(pTarget, "*"))) - RunMigrations(pTarget); -} - -std::shared_ptr CMapMigrationWorkshopDetailsQuery::Create(uint64 iWorkshopId) +void CMapMigrations::UpdateMapUpdateTime(uint64 iWorkshopId, time_t timeMapUpdated) { - if (!GetSteamUGC()) - { - Panic("Map migrations failed to find current map update time: null ISteamUGC\n"); - return nullptr; - } - - uint64 iWorkshopIDArray[1] = {iWorkshopId}; - UGCQueryHandle_t hQuery = GetSteamUGC()->CreateQueryUGCDetailsRequest(iWorkshopIDArray, 1); - - if (hQuery == k_UGCQueryHandleInvalid) - { - Panic("Map migrations failed to find current map update time: failed to query workshop map information for ID %llu\n", iWorkshopId); - return nullptr; - } - - GetSteamUGC()->SetAllowCachedResponse(hQuery, 0); - SteamAPICall_t hCall = GetSteamUGC()->SendQueryUGCRequest(hQuery); - - auto pQuery = std::make_shared(hQuery, iWorkshopId); - g_pMapMigrations->AddWorkshopDetailsQuery(pQuery); - pQuery->m_CallResult.Set(hCall, pQuery.get(), &CMapMigrationWorkshopDetailsQuery::OnQueryCompleted); - - return pQuery; -} - -void CMapMigrationWorkshopDetailsQuery::OnQueryCompleted(SteamUGCQueryCompleted_t* pCompletedQuery, bool bFailed) -{ - SteamUGCDetails_t details; - - if (bFailed || pCompletedQuery->m_eResult != k_EResultOK || pCompletedQuery->m_unNumResultsReturned < 1 || !GetSteamUGC()->GetQueryUGCResult(pCompletedQuery->m_handle, 0, &details) || details.m_eResult != k_EResultOK) - Panic("Map migrations failed to find current map update time: failed to query workshop map information for ID %llu\n", m_iWorkshopId); - else - g_pMapMigrations->UpdateMapUpdateTime(details.m_rtimeUpdated); - - if (GetSteamUGC()) - GetSteamUGC()->ReleaseQueryUGCRequest(m_hQuery); + m_mapUpdateTimes[iWorkshopId] = timeMapUpdated; - g_pMapMigrations->RemoveWorkshopDetailsQuery(shared_from_this()); + // If we get triggered through PreLevelLoad + if (g_pMapVoteSystem->GetCurrentMap() && g_pMapVoteSystem->GetCurrentMap()->GetWorkshopId() == iWorkshopId) + m_timeMapUpdated = timeMapUpdated; } \ No newline at end of file diff --git a/src/mapmigrations.h b/src/mapmigrations.h index 6d4cca13..60b017c6 100644 --- a/src/mapmigrations.h +++ b/src/mapmigrations.h @@ -23,8 +23,6 @@ #include "convar.h" #include "cs2_sdk/entity/ccsweaponbase.h" #include "ehandle.h" -#include "entitysystem.h" -#include "steam/isteamugc.h" #include #undef max @@ -32,43 +30,21 @@ extern CConVar g_cvarMapMigrations20260121; #define SF_DOOR_ONEWAY 16 -class CMapMigrationWorkshopDetailsQuery : public std::enable_shared_from_this -{ -public: - CMapMigrationWorkshopDetailsQuery(UGCQueryHandle_t hQuery, uint64 iWorkshopId) : - m_hQuery(hQuery), m_iWorkshopId(iWorkshopId) - {} - - static std::shared_ptr Create(uint64 iWorkshopId); - -private: - void OnQueryCompleted(SteamUGCQueryCompleted_t* pCompletedQuery, bool bFailed); - - UGCQueryHandle_t m_hQuery; - CCallResult m_CallResult; - uint64 m_iWorkshopId; -}; - class CMapMigrations { public: - void ApplyGameSettings(uint64 iWorkshopId); + void PreLevelLoad(uint64 iWorkshopId); void OnRoundPrestart(); - void OnEntitySpawned_Pre(CBaseEntity* pEntity, const CEntityKeyValues* pKeyValues); - void OnEntitySpawned_Post(CBaseEntity* pEntity); void OnEquipWeapon(CBasePlayerWeapon* pWeapon); - void RunMigrations(CBaseEntity* pEntity); - void Migrations_20260121(CBaseEntity* pEntity); + void RunMigrations(CUtlVector* pVecEntityKeyValues); + void Migrations_20260121(CUtlVector* pVecEntityKeyValues); void Migrations_20260420(CBasePlayerWeapon* pWeapon); bool Migrations20260420Enabled(); - void UpdateMapUpdateTime(time_t timeMapUpdated); - void AddWorkshopDetailsQuery(std::shared_ptr pQuery) { m_vecWorkshopDetailsQueries.push_back(pQuery); } - void RemoveWorkshopDetailsQuery(std::shared_ptr pQuery) { m_vecWorkshopDetailsQueries.erase(std::remove(m_vecWorkshopDetailsQueries.begin(), m_vecWorkshopDetailsQueries.end(), pQuery), m_vecWorkshopDetailsQueries.end()); } + void UpdateMapUpdateTime(uint64 iWorkshopId, time_t timeMapUpdated); private: time_t m_timeMapUpdated = std::numeric_limits::max(); - std::vector> m_vecWorkshopDetailsQueries; - std::vector> m_vecModelEntitiesUsingRendermodeEnum; + std::unordered_map m_mapUpdateTimes; std::vector> m_vecEquippedWeapons; }; diff --git a/src/virtual_hooks.cpp b/src/virtual_hooks.cpp index 53ca54bc..86a06263 100644 --- a/src/virtual_hooks.cpp +++ b/src/virtual_hooks.cpp @@ -27,6 +27,7 @@ #include "cs_gameevents.pb.h" #include "cstrike15_usermessages.pb.h" #include "ctimer.h" +#include "engine/IEngineService.h" #include "entities.h" #include "entity/ccsplayercontroller.h" #include "entity/customhudlayout.h" @@ -74,11 +75,11 @@ KHOOK_VIRTUAL(PostEventAbstract, &IGameEventSystem::PostEventAbstract, g_gameEve KHOOK_VIRTUAL(StartupServer, &INetworkServerService::StartupServer, g_pNetworkServerService, nullptr, Hook_StartupServer_Post); KHOOK_VIRTUAL(CheckTransmit, &ISource2GameEntities::CheckTransmit, g_pSource2GameEntities, nullptr, Hook_CheckTransmit_Post); KHOOK_VIRTUAL(DispatchConCommand, &ICvar::DispatchConCommand, g_pCVar, Hook_DispatchConCommand, nullptr); +KHOOK_VIRTUAL(SwitchToLoop, &IEngineServiceMgr::SwitchToLoop, g_pEngineServiceMgr, Hook_SwitchToLoop, nullptr); KHook::Virtual createWorkshopMapGroupHook(0U, Hook_CreateWorkshopMapGroup, nullptr); KHook::Virtual loadEventsFromFileHook(&IGameEventManager2::LoadEventsFromFile, Hook_LoadEventsFromFile, nullptr); KHook::Virtual fireEventHook(&IGameEventManager2::FireEvent, Hook_FireEvent, nullptr); -KHook::Virtual spawnHook(&CEntitySystem::Spawn, Hook_Spawn, nullptr); KHook::Virtual processVoiceDataHook(&CServerSideClient::ProcessVoiceData, Hook_ProcessVoiceData, nullptr); KHook::Virtual setGameSpawnGroupMgrHook(&INetworkGameServer::SetGameSpawnGroupMgr, Hook_SetGameSpawnGroupMgr, nullptr); KHook::Virtual*, bool> getTouchingListHook(0U, nullptr, Hook_GetTouchingList_Post); @@ -92,7 +93,6 @@ KHook::Virtual onTakeDamageAliveHook(0U KHook::Virtual playerPawnTeleportHook(0U, Hook_CCSPlayerPawn_Teleport, Hook_CCSPlayerPawn_Teleport_Post); IGameEventManager2* g_pCGameEventManagerVTable = nullptr; -CEntitySystem* g_pCEntitySystemVTable = nullptr; CVPhys2World* g_pCVPhys2WorldVTable = nullptr; CCSPlayer_MovementServices* g_pCCSPlayer_MovementServicesVTable = nullptr; CCSPlayer_WeaponServices* g_pCCSPlayer_WeaponServicesVTable = nullptr; @@ -145,7 +145,6 @@ void InitVirtualHooks() SetupGlobalVirtualHook(loadEventsFromFileHook, g_pCGameEventManagerVTable, modules::server, "CGameEventManager"); SetupGlobalVirtualHook(fireEventHook, g_pCGameEventManagerVTable, modules::server, "CGameEventManager"); - SetupGlobalVirtualHook(spawnHook, g_pCEntitySystemVTable, modules::server, "CGameEntitySystem"); SetupGlobalVirtualHook(processVoiceDataHook, g_pCServerSideClientVTable, modules::engine, "CServerSideClient"); SetupGlobalVirtualHook(getTouchingListHook, g_pCVPhys2WorldVTable, modules::vphysics2, "CVPhys2World", "CVPhys2World::GetTouchingList"); SetupGlobalVirtualHook(checkMovingGroundHook, g_pCCSPlayer_MovementServicesVTable, modules::server, "CCSPlayer_MovementServices", "CCSPlayer_MovementServices::CheckMovingGround"); @@ -162,7 +161,6 @@ void RemoveVirtualHooks() { loadEventsFromFileHook.RemoveGlobal((IGameEventManager2*)&g_pCGameEventManagerVTable); fireEventHook.RemoveGlobal((IGameEventManager2*)&g_pCGameEventManagerVTable); - spawnHook.RemoveGlobal((CEntitySystem*)&g_pCEntitySystemVTable); processVoiceDataHook.RemoveGlobal((CServerSideClient*)&g_pCServerSideClientVTable); setGameSpawnGroupMgrHook.Remove(GetNetworkGameServer()); createWorkshopMapGroupHook.Remove(g_pGameTypes); @@ -230,7 +228,6 @@ KHook::Return Hook_ApplyGameSettings(IServerGameDLL* pThis, KeyValues* pKV g_pCfgParser->ApplyGameSettings(pszMapName); g_pMapVoteSystem->ApplyGameSettings(pszMapName, iWorkshopId); - g_pMapMigrations->ApplyGameSettings(iWorkshopId); return {KHook::Action::Ignore}; } @@ -724,6 +721,17 @@ KHook::Return Hook_DispatchConCommand(ICvar* pThis, ConCommandRef cmdHandl return {KHook::Action::Ignore}; } +KHook::Return Hook_SwitchToLoop(IEngineServiceMgr* pThis, const char* pszLoopModeName, KeyValues* pKV, uint32 nId, const char* pszAddonName, bool bUnk) +{ + if (pKV && !V_strcmp(pszLoopModeName, "levelload")) + { + g_pCfgParser->PreLevelLoad(pKV->GetString("levelname", "")); + g_pMapMigrations->PreLevelLoad(pKV->GetUint64("customgamemode", 0)); + } + + return {KHook::Action::Ignore}; +} + KHook::Return Hook_CreateWorkshopMapGroup(IGameTypes* pThis, const char* name, const CUtlStringList& mapList) { if (g_cvarVoteManagerEnable.Get() && g_pMapVoteSystem->IsMapListLoaded()) @@ -751,14 +759,6 @@ KHook::Return Hook_FireEvent(IGameEventManager2* pThis, IGameEvent* pEvent return {KHook::Action::Ignore}; } -KHook::Return Hook_Spawn(CEntitySystem* pThis, int nCount, const EntitySpawnInfo_t* pInfo) -{ - for (int i = 0; i < nCount; i++) - g_pMapMigrations->OnEntitySpawned_Pre(reinterpret_cast(pInfo[i].m_pEntity->m_pInstance), pInfo[i].m_pKeyValues); - - return {KHook::Action::Ignore}; -} - KHook::Return Hook_ProcessVoiceData(CServerSideClient* pClient, const CCLCMsg_VoiceData_t& msg) { ZEPlayer* pPlayer = g_playerManager->GetPlayer(pClient->GetPlayerSlot()); diff --git a/src/virtual_hooks.h b/src/virtual_hooks.h index 90871bb6..36f0b450 100644 --- a/src/virtual_hooks.h +++ b/src/virtual_hooks.h @@ -36,6 +36,7 @@ class CCSPlayerPawn; class CVPhys2World; class CTriggerGravity; class CGameConfig; +class IEngineServiceMgr; void InitVirtualHooks(); void RemoveVirtualHooks(); @@ -57,10 +58,10 @@ KHook::Return Hook_StartupServer_Post(INetworkServerService* pThis, const KHook::Return Hook_CheckTransmit_Post(ISource2GameEntities* pThis, CCheckTransmitInfo** ppInfoList, int infoCount, CBitVec<16384>& unionTransmitEdicts, CBitVec<16384>&, const Entity2Networkable_t** pNetworkables, const uint16* pEntityIndicies, int nEntities); KHook::Return Hook_DispatchConCommand(ICvar* pThis, ConCommandRef cmd, const CCommandContext& ctx, const CCommand& args); +KHook::Return Hook_SwitchToLoop(IEngineServiceMgr* pThis, const char* pszLoopModeName, KeyValues* pKV, uint32 nId, const char* pszAddonName, bool bUnk); KHook::Return Hook_CreateWorkshopMapGroup(IGameTypes* pThis, const char* name, const CUtlStringList& mapList); KHook::Return Hook_LoadEventsFromFile(IGameEventManager2* pThis, const char* filename, bool bSearchAll); KHook::Return Hook_FireEvent(IGameEventManager2* pThis, IGameEvent* pEvent, bool bDontBroadcast); -KHook::Return Hook_Spawn(CEntitySystem* pThis, int nCount, const EntitySpawnInfo_t* pInfo); KHook::Return Hook_ProcessVoiceData(CServerSideClient* pClient, const CCLCMsg_VoiceData_t& msg); KHook::Return Hook_SetGameSpawnGroupMgr(INetworkGameServer* pThis, IGameSpawnGroupMgr* pSpawnGroupMgr); KHook::Return Hook_GetTouchingList_Post(CVPhys2World* pThis, CUtlVector* pList, bool unknown); From 999d2d075f24390f44b0537b91fcb123c8f0de2e Mon Sep 17 00:00:00 2001 From: Vauff Date: Thu, 17 Sep 2026 17:34:35 -0400 Subject: [PATCH 08/11] Run rendermode migrations independent of update time Index-based rendermodes only come from the S1 map importer. S2 Hammer always uses enum-named render modes, so we can confidently assume that new maps will not be caught off by this, while we can also extend this migration to new/recently updated ports --- src/mapmigrations.cpp | 25 ++++++++++++++++++------- src/mapmigrations.h | 1 + 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/mapmigrations.cpp b/src/mapmigrations.cpp index c21d26fe..2b61d8c1 100644 --- a/src/mapmigrations.cpp +++ b/src/mapmigrations.cpp @@ -67,22 +67,19 @@ void CMapMigrations::OnEquipWeapon(CBasePlayerWeapon* pWeapon) void CMapMigrations::RunMigrations(CUtlVector* pVecEntityKeyValues) { + if (g_cvarMapMigrations20260121.Get() > 0) + Migrations_Rendermode(pVecEntityKeyValues); + if (g_cvarMapMigrations20260121.Get() == 1 || (g_cvarMapMigrations20260121.Get() == 2 && m_timeMapUpdated < g_time20260121)) Migrations_20260121(pVecEntityKeyValues); } -void CMapMigrations::Migrations_20260121(CUtlVector* pVecEntityKeyValues) +void CMapMigrations::Migrations_Rendermode(CUtlVector* pVecEntityKeyValues) { FOR_EACH_VEC(*pVecEntityKeyValues, i) { auto pKeyValues = (*pVecEntityKeyValues)[i]; - if (!V_strcasecmp(pKeyValues->GetString("classname"), "func_door_rotating") && pKeyValues->HasValue("spawnflags")) - { - uint32 spawnFlags = pKeyValues->GetUint("spawnflags"); - pKeyValues->SetUint("spawnflags", spawnFlags | SF_DOOR_ONEWAY); - } - if (!pKeyValues->HasValue("rendermode")) continue; @@ -102,6 +99,20 @@ void CMapMigrations::Migrations_20260121(CUtlVector* pVecEnti } } +void CMapMigrations::Migrations_20260121(CUtlVector* pVecEntityKeyValues) +{ + FOR_EACH_VEC(*pVecEntityKeyValues, i) + { + auto pKeyValues = (*pVecEntityKeyValues)[i]; + + if (!V_strcasecmp(pKeyValues->GetString("classname"), "func_door_rotating") && pKeyValues->HasValue("spawnflags")) + { + uint32 spawnFlags = pKeyValues->GetUint("spawnflags"); + pKeyValues->SetUint("spawnflags", spawnFlags | SF_DOOR_ONEWAY); + } + } +} + void CMapMigrations::Migrations_20260420(CBasePlayerWeapon* pWeapon) { VPROF("CMapMigrations::Migrations_20260420"); diff --git a/src/mapmigrations.h b/src/mapmigrations.h index 60b017c6..69a4465e 100644 --- a/src/mapmigrations.h +++ b/src/mapmigrations.h @@ -37,6 +37,7 @@ class CMapMigrations void OnRoundPrestart(); void OnEquipWeapon(CBasePlayerWeapon* pWeapon); void RunMigrations(CUtlVector* pVecEntityKeyValues); + void Migrations_Rendermode(CUtlVector* pVecEntityKeyValues); void Migrations_20260121(CUtlVector* pVecEntityKeyValues); void Migrations_20260420(CBasePlayerWeapon* pWeapon); bool Migrations20260420Enabled(); From a9c2f7e5e247c54ec1805f9085251fc2374b6663 Mon Sep 17 00:00:00 2001 From: Vauff Date: Thu, 17 Sep 2026 17:58:34 -0400 Subject: [PATCH 09/11] Fix possible race condition with Steam callback --- src/mapmigrations.cpp | 16 ++++++++-------- src/mapmigrations.h | 1 + src/virtual_hooks.cpp | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/mapmigrations.cpp b/src/mapmigrations.cpp index 2b61d8c1..7750b06d 100644 --- a/src/mapmigrations.cpp +++ b/src/mapmigrations.cpp @@ -41,19 +41,19 @@ void CMapMigrations::PreLevelLoad(uint64 iWorkshopId) if (iWorkshopId != 0) { if (m_mapUpdateTimes.contains(iWorkshopId)) - { m_timeMapUpdated = m_mapUpdateTimes[iWorkshopId]; - } else - { Message("Skipping pre-load map migrations for %llu, update time is not available\n", iWorkshopId); - - // Try to get the update time anyways, for later migrations - CMapSystemWorkshopDetailsQuery::Create(iWorkshopId); - } } } +void CMapMigrations::ApplyGameSettings(uint64 iWorkshopId) +{ + // If map update time wasn't available before map load, try to get the update time anyways, for later migrations + if (iWorkshopId != 0 && !m_mapUpdateTimes.contains(iWorkshopId)) + CMapSystemWorkshopDetailsQuery::Create(iWorkshopId); +} + void CMapMigrations::OnRoundPrestart() { m_vecEquippedWeapons.clear(); @@ -166,7 +166,7 @@ void CMapMigrations::UpdateMapUpdateTime(uint64 iWorkshopId, time_t timeMapUpdat { m_mapUpdateTimes[iWorkshopId] = timeMapUpdated; - // If we get triggered through PreLevelLoad + // If we get triggered through ApplyGameSettings if (g_pMapVoteSystem->GetCurrentMap() && g_pMapVoteSystem->GetCurrentMap()->GetWorkshopId() == iWorkshopId) m_timeMapUpdated = timeMapUpdated; } \ No newline at end of file diff --git a/src/mapmigrations.h b/src/mapmigrations.h index 69a4465e..dfcf31e8 100644 --- a/src/mapmigrations.h +++ b/src/mapmigrations.h @@ -34,6 +34,7 @@ class CMapMigrations { public: void PreLevelLoad(uint64 iWorkshopId); + void ApplyGameSettings(uint64 iWorkshopId); void OnRoundPrestart(); void OnEquipWeapon(CBasePlayerWeapon* pWeapon); void RunMigrations(CUtlVector* pVecEntityKeyValues); diff --git a/src/virtual_hooks.cpp b/src/virtual_hooks.cpp index 86a06263..dcdeb789 100644 --- a/src/virtual_hooks.cpp +++ b/src/virtual_hooks.cpp @@ -228,6 +228,7 @@ KHook::Return Hook_ApplyGameSettings(IServerGameDLL* pThis, KeyValues* pKV g_pCfgParser->ApplyGameSettings(pszMapName); g_pMapVoteSystem->ApplyGameSettings(pszMapName, iWorkshopId); + g_pMapMigrations->ApplyGameSettings(iWorkshopId); return {KHook::Action::Ignore}; } From 3fdd580e9ea27fe9df092627157d64bf76210b38 Mon Sep 17 00:00:00 2001 From: Vauff Date: Fri, 18 Sep 2026 00:30:28 -0400 Subject: [PATCH 10/11] Fix missing FireOutputInternal hook definition --- src/detours.cpp | 3 ++- src/detours.h | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/detours.cpp b/src/detours.cpp index 4349b165..6508f208 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -81,6 +81,7 @@ KHOOK_MEMBER(CCSPlayerPawn_GetMaxSpeed, Detour_CCSPlayerPawn_GetMaxSpeed, nullpt KHOOK_MEMBER(FindUseEntity, Detour_FindUseEntity, Detour_FindUseEntity_Post); KHOOK_FUNCTION(TraceFunc, Detour_TraceFunc, nullptr); KHOOK_FUNCTION(TraceShape, Detour_TraceShape, nullptr); +KHOOK_MEMBER(CEntityIOOutput_FireOutputInternal, Detour_CEntityIOOutput_FireOutputInternal, nullptr); KHOOK_MEMBER(CBasePlayerPawn_GetEyePosition, Detour_CBasePlayerPawn_GetEyePosition, nullptr); KHOOK_MEMBER(CBasePlayerPawn_GetEyeAngles, Detour_CBasePlayerPawn_GetEyeAngles, nullptr); KHOOK_MEMBER(CBaseFilter_InputTestActivator, Detour_CBaseFilter_InputTestActivator, nullptr); @@ -751,7 +752,7 @@ KHook::Return Detour_TraceShape(int64* a1, int64 a2, int64 a3, int64 a4, C return {KHook::Action::Ignore}; } -KHook::Return Detour_CEntityIOOutput_FireOutputInternal(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay, void* a6, void* a7) +KHook::Return Detour_CEntityIOOutput_FireOutputInternal(CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay, void* a6, void* a7) { if (g_cvarEnableButtonWatch.Get()) ButtonWatch(pThis, pActivator, pCaller, value, flDelay); diff --git a/src/detours.h b/src/detours.h index b31a9f5d..65da62a4 100644 --- a/src/detours.h +++ b/src/detours.h @@ -102,7 +102,7 @@ KHook::Return Detour_FindUseEntity(CCSPlayer_UseServices* pThis, f KHook::Return Detour_FindUseEntity_Post(CCSPlayer_UseServices* pThis, float unk2, int64_t unk3); KHook::Return Detour_TraceFunc(int64*, int*, float*, uint64); KHook::Return Detour_TraceShape(int64*, int64, int64, int64, CTraceFilter*, int64); -KHook::Return Detour_CEntityIOOutput_FireOutputInternal(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay, void*, void*); +KHook::Return Detour_CEntityIOOutput_FireOutputInternal(CEntityIOOutput* pThis, CEntityInstance* pActivator, CEntityInstance* pCaller, const CVariant* value, float flDelay, void*, void*); #ifdef PLATFORM_WINDOWS KHook::Return Detour_CBasePlayerPawn_GetEyePosition(CBasePlayerPawn*, Vector*); KHook::Return Detour_CBasePlayerPawn_GetEyeAngles(CBasePlayerPawn*, QAngle*); From 9f699db32ba0f9766384436a7abda36860e6fe37 Mon Sep 17 00:00:00 2001 From: Vauff Date: Fri, 18 Sep 2026 15:19:08 -0400 Subject: [PATCH 11/11] Harden map download queue logic & try to avoid rate limit --- src/map_votes.cpp | 51 ++++++++++++++++++++++++++++++----------------- src/map_votes.h | 3 ++- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/map_votes.cpp b/src/map_votes.cpp index 8c3df85f..1f2a1364 100644 --- a/src/map_votes.cpp +++ b/src/map_votes.cpp @@ -900,30 +900,24 @@ void CMapVoteSystem::PrintDownloadProgress() void CMapVoteSystem::OnMapDownloaded(DownloadItemResult_t* pResult) { - if (std::find(m_DownloadQueue.begin(), m_DownloadQueue.end(), pResult->m_nPublishedFileId) == m_DownloadQueue.end() || !GetSteamUGC()) + if (m_DownloadQueue.empty() || m_DownloadQueue.front() != pResult->m_nPublishedFileId) return; - // Some weird rate limiting that's been observed? Back off for a while then retry download + // This result is also used for some kind of rate limiting, back off for a while then retry download if (pResult->m_eResult == k_EResultNoConnection) { - PublishedFileId_t workshopID = m_DownloadQueue.front(); - Message("Addon %llu download failed with status code 3, retrying in 2 minutes\n", workshopID); - - m_pRateLimitedDownloadTimer = CTimer::Create(120.0f, TIMERFLAG_NONE, [workshopID]() { - GetSteamUGC()->DownloadItem(workshopID, false); - - return -1.0f; - }); - + Message("Addon %llu download failed with status code 3, retrying in 5 minutes\n", m_DownloadQueue.front()); + StartMapDownload(300.0f); return; } - m_DownloadQueue.pop_front(); + if (pResult->m_eResult != k_EResultOK) + Message("Addon %llu download failed with status code %i, skipping\n", pResult->m_nPublishedFileId, pResult->m_eResult); - if (GetDownloadQueueSize() == 0) - return; + m_DownloadQueue.pop_front(); - GetSteamUGC()->DownloadItem(m_DownloadQueue.front(), false); + // Go slowly, try to avoid the rate limit because it can break required on-demand downloads elsewhere + StartMapDownload(30.0f); } void CMapVoteSystem::QueueMapDownload(PublishedFileId_t iWorkshopId) @@ -934,7 +928,28 @@ void CMapVoteSystem::QueueMapDownload(PublishedFileId_t iWorkshopId) m_DownloadQueue.push_back(iWorkshopId); if (m_DownloadQueue.front() == iWorkshopId) - GetSteamUGC()->DownloadItem(iWorkshopId, false); + StartMapDownload(); +} + +void CMapVoteSystem::StartMapDownload(float flDelay) +{ + if (auto pTimer = m_pDownloadTimer.lock()) + pTimer->Cancel(); + + if (m_DownloadQueue.empty()) + return; + + PublishedFileId_t workshopID = m_DownloadQueue.front(); + + m_pDownloadTimer = CTimer::Create(flDelay, TIMERFLAG_NONE, [workshopID]() { + if (!GetSteamUGC() || !GetSteamUGC()->DownloadItem(workshopID, false)) + { + Message("Addon %llu download failed to start, retrying\n", workshopID); + return 30.0f; + } + + return -1.0f; + }); } bool CMapVoteSystem::LoadMapList() @@ -1376,8 +1391,8 @@ bool CMapVoteSystem::ReloadMapList(bool bReloadMap) if (!m_pDownloadProgressTimer.expired()) m_pDownloadProgressTimer.lock()->Cancel(); - if (!m_pRateLimitedDownloadTimer.expired()) - m_pRateLimitedDownloadTimer.lock()->Cancel(); + if (!m_pDownloadTimer.expired()) + m_pDownloadTimer.lock()->Cancel(); } if (!g_pMapVoteSystem->LoadMapList()) diff --git a/src/map_votes.h b/src/map_votes.h index 1742a812..e6990288 100644 --- a/src/map_votes.h +++ b/src/map_votes.h @@ -202,6 +202,7 @@ class CMapVoteSystem bool IsMapListLoaded() { return m_bMapListLoaded; } CUtlStringList CreateWorkshopMapGroup(); void QueueMapDownload(PublishedFileId_t iWorkshopId); + void StartMapDownload(float flDelay = 0.0f); void PrintDownloadProgress(); std::shared_ptr GetCurrentMap() { return m_pCurrentMap; } void SetCurrentMap(std::shared_ptr pCurrentMap) { m_pCurrentMap = pCurrentMap; } @@ -245,7 +246,7 @@ class CMapVoteSystem bool g_bDisableCooldowns = false; std::filesystem::file_time_type m_timeMapListModified = std::filesystem::file_time_type::min(); std::weak_ptr m_pDownloadProgressTimer; - std::weak_ptr m_pRateLimitedDownloadTimer; + std::weak_ptr m_pDownloadTimer; std::vector> m_vecWorkshopDetailsQueries; int m_iSessionMaxPlayerCount = 0; };