From 8f06fb9835d2b9611b51bcdca55717489f67076a Mon Sep 17 00:00:00 2001 From: Mohab <133429578+MohabCodeX@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:11:54 +0300 Subject: [PATCH] Implement context-aware command binding methods --- Client/core/CKeyBinds.cpp | 168 +++++++++++++++++- Client/core/CKeyBinds.h | 6 + Client/mods/deathmatch/logic/CResource.cpp | 18 +- .../logic/CStaticFunctionDefinitions.cpp | 17 +- .../mods/deathmatch/logic/rpc/CInputRPCs.cpp | 2 + Client/sdk/core/CKeyBindsInterface.h | 17 +- .../logic/CStaticFunctionDefinitions.cpp | 2 + 7 files changed, 212 insertions(+), 18 deletions(-) diff --git a/Client/core/CKeyBinds.cpp b/Client/core/CKeyBinds.cpp index 365c68c8cd9..1187c80525e 100644 --- a/Client/core/CKeyBinds.cpp +++ b/Client/core/CKeyBinds.cpp @@ -550,7 +550,18 @@ void CKeyBinds::RemoveDeletedBinds() void CKeyBinds::ClearCommandsAndControls() { const auto predicate = [](const KeyBindPtr& bind) { - return !bind->isBeingDeleted && bind->type != KeyBindType::FUNCTION && bind->type != KeyBindType::CONTROL_FUNCTION; + if (bind->isBeingDeleted) + return false; + + if (bind->type == KeyBindType::COMMAND) + { + auto commandBind = static_cast(bind.get()); + // Only remove resource bindings, preserve user bindings + return commandBind->context == BindingContext::RESOURCE; + } + + // Remove all control bindings (GTA_CONTROL) + return bind->type == KeyBindType::GTA_CONTROL; }; RemoveBinds(m_binds, !m_bProcessingKeyStroke, predicate); } @@ -612,9 +623,11 @@ bool CKeyBinds::AddCommand(const char* szKey, const char* szCommand, const char* CCommandBind* pUserAddedBind = FindCommandMatch(NULL, szCommand, szArguments, szResource, szKey, true, bState, true, false); if (pUserAddedBind) { - // Upgrade + // Upgrade user binding to resource binding pUserAddedBind->wasCreatedByScript = true; pUserAddedBind->isReplacingScriptKey = true; + pUserAddedBind->context = BindingContext::RESOURCE; + pUserAddedBind->sourceResource = szResource; assert(pUserAddedBind->originalScriptKey == szKey); return true; } @@ -631,13 +644,20 @@ bool CKeyBinds::AddCommand(const char* szKey, const char* szCommand, const char* if (szResource) { bind->resource = szResource; + bind->sourceResource = szResource; bind->wasCreatedByScript = bScriptCreated; + bind->context = BindingContext::RESOURCE; if (bScriptCreated) bind->originalScriptKey = szKey; else if (szOriginalScriptKey) bind->originalScriptKey = szOriginalScriptKey; // Will wait for script to addcommand before doing replace } + else + { + // User-created binding (via /bind command) + bind->context = BindingContext::USER; + } m_binds.emplace_back(bind.release()); return true; @@ -2632,3 +2652,147 @@ bool CKeyBinds::TriggerKeyStrokeHandler(const SString& strKey, bool bState, bool } return true; } + +bool CKeyBinds::CommandExistsInContext(const char* key, const char* command, BindingContext context, bool checkState, bool state, const char* arguments, const char* resource) +{ + if (!key || !command) + return false; + + for (const KeyBindPtr& bind : m_binds) + { + if (bind->isBeingDeleted || bind->type != KeyBindType::COMMAND) + continue; + + auto commandBind = static_cast(bind.get()); + + if (commandBind->context != context) + continue; + + if (stricmp(commandBind->boundKey->szKey, key) != 0) + continue; + + if (stricmp(commandBind->command.c_str(), command) != 0) + continue; + + if (checkState && commandBind->triggerState != state) + continue; + + if (arguments && commandBind->arguments != arguments) + continue; + + if (resource && commandBind->resource != resource) + continue; + + return true; + } + + return false; +} + +bool CKeyBinds::RemoveCommandFromContext(const char* key, const char* command, BindingContext context, bool checkState, bool state, const char* arguments, const char* resource) +{ + if (!key || !command) + return false; + + const auto predicate = [&](const KeyBindPtr& bind) { + if (bind->isBeingDeleted || bind->type != KeyBindType::COMMAND) + return false; + + auto commandBind = static_cast(bind.get()); + + if (commandBind->context != context) + return false; + + if (stricmp(commandBind->boundKey->szKey, key) != 0) + return false; + + if (stricmp(commandBind->command.c_str(), command) != 0) + return false; + + if (checkState && commandBind->triggerState != state) + return false; + + if (arguments && commandBind->arguments != arguments) + return false; + + if (resource && commandBind->resource != resource) + return false; + + return true; + }; + + return RemoveBinds(m_binds, !m_bProcessingKeyStroke, predicate); +} + +bool CKeyBinds::HasAnyBindingForKey(const char* key, bool checkState, bool state) +{ + if (!key) + return false; + + for (const KeyBindPtr& bind : m_binds) + { + if (bind->isBeingDeleted) + continue; + + if (bind->type == KeyBindType::COMMAND) + { + auto commandBind = static_cast(bind.get()); + if (stricmp(commandBind->boundKey->szKey, key) == 0) + { + if (!checkState || commandBind->triggerState == state) + return true; + } + } + else if (bind->type == KeyBindType::FUNCTION) + { + auto functionBind = static_cast(bind.get()); + if (stricmp(functionBind->boundKey->szKey, key) == 0) + { + if (!checkState || functionBind->triggerState == state) + return true; + } + } + else if (bind->type == KeyBindType::CONTROL_FUNCTION) + { + auto controlBind = static_cast(bind.get()); + if (stricmp(controlBind->boundKey->szKey, key) == 0) + { + if (!checkState || controlBind->triggerState == state) + return true; + } + } + else if (bind->type == KeyBindType::GTA_CONTROL) + { + auto gtaBind = static_cast(bind.get()); + if (stricmp(gtaBind->boundKey->szKey, key) == 0) + return true; + } + } + + return false; +} + +bool CKeyBinds::HasBindingInContext(const char* key, BindingContext context, bool checkState, bool state) +{ + if (!key) + return false; + + for (const KeyBindPtr& bind : m_binds) + { + if (bind->isBeingDeleted || bind->type != KeyBindType::COMMAND) + continue; + + auto commandBind = static_cast(bind.get()); + + if (commandBind->context != context) + continue; + + if (stricmp(commandBind->boundKey->szKey, key) != 0) + continue; + + if (!checkState || commandBind->triggerState == state) + return true; + } + + return false; +} diff --git a/Client/core/CKeyBinds.h b/Client/core/CKeyBinds.h index c5464a5b463..fb61387422d 100644 --- a/Client/core/CKeyBinds.h +++ b/Client/core/CKeyBinds.h @@ -75,6 +75,12 @@ class CKeyBinds final : public CKeyBindsInterface bool bCheckState, bool bState, bool bCheckScriptCreated, bool bScriptCreated); void SortCommandBinds(); + // Context-aware binding methods + bool CommandExistsInContext(const char* key, const char* command, BindingContext context, bool checkState = false, bool state = true, const char* arguments = NULL, const char* resource = NULL); + bool RemoveCommandFromContext(const char* key, const char* command, BindingContext context, bool checkState = false, bool state = true, const char* arguments = NULL, const char* resource = NULL); + bool HasAnyBindingForKey(const char* key, bool checkState = false, bool state = true); + bool HasBindingInContext(const char* key, BindingContext context, bool checkState = false, bool state = true); + // Control-bind funcs bool AddGTAControl(const char* szKey, const char* szControl); bool AddGTAControl(const SBindableKey* pKey, SBindableGTAControl* pControl); diff --git a/Client/mods/deathmatch/logic/CResource.cpp b/Client/mods/deathmatch/logic/CResource.cpp index 57a6d61d6c1..75d41bf8418 100644 --- a/Client/mods/deathmatch/logic/CResource.cpp +++ b/Client/mods/deathmatch/logic/CResource.cpp @@ -114,7 +114,23 @@ CResource::~CResource() // Remove all keybinds on this VM g_pClientGame->GetScriptKeyBinds()->RemoveAllKeys(m_pLuaVM); - g_pCore->GetKeyBinds()->SetAllCommandsActive(m_strResourceName, false); + + // Remove all resource-specific command bindings while preserving user bindings + CKeyBindsInterface* pKeyBinds = g_pCore->GetKeyBinds(); + pKeyBinds->SetAllCommandsActive(m_strResourceName, false); + + // Additional cleanup: remove any remaining resource bindings that weren't caught by SetAllCommandsActive + for (auto& bind : *pKeyBinds) + { + if (bind->type == KeyBindType::COMMAND) + { + auto commandBind = static_cast(bind.get()); + if (commandBind->context == BindingContext::RESOURCE && commandBind->resource == m_strResourceName) + { + pKeyBinds->Remove(commandBind); + } + } + } // Destroy the txd root so all dff elements are deleted except those moved out g_pClientGame->GetElementDeleter()->DeleteRecursive(m_pResourceTXDRoot); diff --git a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp index 18430ed7f92..1725830e231 100644 --- a/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp +++ b/Client/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp @@ -7219,27 +7219,16 @@ bool CStaticFunctionDefinitions::UnbindKey(const char* szKey, const char* szHitS } } - pBind = g_pCore->GetKeyBinds()->GetBindFromCommand(szCommandName, NULL, false, szKey, bCheckHitState, bHitState); - + // Use context-aware removal to only remove resource bindings if ((!stricmp(szHitState, "down") || !stricmp(szHitState, "both")) && - pKeyBinds->SetCommandActive(szKey, szCommandName, bHitState, NULL, szResource, false, true, true)) + pKeyBinds->RemoveCommandFromContext(szKey, szCommandName, BindingContext::RESOURCE, bCheckHitState, bHitState, NULL, szResource)) { - pKeyBinds->SetAllCommandsActive(szResource, false, szCommandName, bHitState, NULL, true, szKey); - - if (pBind) - pKeyBinds->Remove(pBind); - bSuccess = true; } bHitState = false; if ((!stricmp(szHitState, "up") || !stricmp(szHitState, "both")) && - pKeyBinds->SetCommandActive(szKey, szCommandName, bHitState, NULL, szResource, false, true, true)) + pKeyBinds->RemoveCommandFromContext(szKey, szCommandName, BindingContext::RESOURCE, bCheckHitState, bHitState, NULL, szResource)) { - pKeyBinds->SetAllCommandsActive(szResource, false, szCommandName, bHitState, NULL, true, szKey); - - if (pBind) - pKeyBinds->Remove(pBind); - bSuccess = true; } } diff --git a/Client/mods/deathmatch/logic/rpc/CInputRPCs.cpp b/Client/mods/deathmatch/logic/rpc/CInputRPCs.cpp index 4b0dcebb500..52d48493ab1 100644 --- a/Client/mods/deathmatch/logic/rpc/CInputRPCs.cpp +++ b/Client/mods/deathmatch/logic/rpc/CInputRPCs.cpp @@ -126,6 +126,7 @@ void CInputRPCs::UnbindKey(NetBitStreamInterface& bitStream) const SBindableKey* pKey = pKeyBinds->GetBindableFromKey(szKey); if (pKey) { + // Only remove server-side function bindings, preserve user command bindings pKeyBinds->RemoveFunction(szKey, CClientGame::StaticProcessServerKeyBind, true, bState); } else @@ -133,6 +134,7 @@ void CInputRPCs::UnbindKey(NetBitStreamInterface& bitStream) SBindableGTAControl* pControl = pKeyBinds->GetBindableFromControl(szKey); if (pControl) { + // Only remove server-side control function bindings, preserve user command bindings pKeyBinds->RemoveControlFunction(szKey, CClientGame::StaticProcessServerControlBind, true, bState); } } diff --git a/Client/sdk/core/CKeyBindsInterface.h b/Client/sdk/core/CKeyBindsInterface.h index badabc59162..210efd0a126 100644 --- a/Client/sdk/core/CKeyBindsInterface.h +++ b/Client/sdk/core/CKeyBindsInterface.h @@ -94,6 +94,13 @@ class CKeyBindWithState : public CKeyBind bool triggerState{false}; // true == "down", false == "up" }; +enum class BindingContext +{ + USER, // Created by user via /bind command + RESOURCE, // Created by resource via bindKey + SYSTEM // Created by system/default +}; + class CCommandBind : public CKeyBindWithState { public: @@ -104,8 +111,10 @@ class CCommandBind : public CKeyBindWithState std::string arguments; std::string resource; std::string originalScriptKey; // Original key set by script + std::string sourceResource; // Resource that created this binding bool wasCreatedByScript{false}; - bool isReplacingScriptKey{false}; // true if script set key is not being used + bool isReplacingScriptKey{false}; // true if script set key is not being used + BindingContext context{BindingContext::USER}; // Context of this binding }; class CKeyFunctionBind : public CKeyBindWithState @@ -174,6 +183,12 @@ class CKeyBindsInterface virtual void UserRemoveCommandBoundKey(CCommandBind* pBind) = 0; virtual CCommandBind* FindMatchingUpBind(CCommandBind* pBind) = 0; + // Context-aware binding methods + virtual bool CommandExistsInContext(const char* key, const char* command, BindingContext context, bool checkState = false, bool state = true, const char* arguments = NULL, const char* resource = NULL) = 0; + virtual bool RemoveCommandFromContext(const char* key, const char* command, BindingContext context, bool checkState = false, bool state = true, const char* arguments = NULL, const char* resource = NULL) = 0; + virtual bool HasAnyBindingForKey(const char* key, bool checkState = false, bool state = true) = 0; + virtual bool HasBindingInContext(const char* key, BindingContext context, bool checkState = false, bool state = true) = 0; + // Control-bind funcs virtual bool AddGTAControl(const char* szKey, const char* szControl) = 0; virtual bool AddGTAControl(const SBindableKey* pKey, SBindableGTAControl* pControl) = 0; diff --git a/Server/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp b/Server/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp index d42467bc53c..3fd08fe57f1 100644 --- a/Server/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp +++ b/Server/mods/deathmatch/logic/CStaticFunctionDefinitions.cpp @@ -9110,6 +9110,8 @@ bool CStaticFunctionDefinitions::UnbindKey(CPlayer* pPlayer, const char* szKey, (pControl && (bSuccess = pKeyBinds->RemoveControlFunction(szKey, pLuaMain, bCheckHitState, bHitState, iLuaFunction)) && !pKeyBinds->ControlFunctionExists(szKey, NULL, bCheckHitState, bHitState))) { + // Only send UNBIND_KEY RPC if there are no more function bindings for this key + // This allows user command bindings to persist unsigned char ucKeyLength = static_cast(strlen(szKey)); CBitStream bitStream;