Skip to content

Fixes #3160 - New onClientCommand event - #4426

Open
MohabCodeX wants to merge 20 commits into
multitheftauto:masterfrom
MohabCodeX:feat/on-client-command
Open

Fixes #3160 - New onClientCommand event#4426
MohabCodeX wants to merge 20 commits into
multitheftauto:masterfrom
MohabCodeX:feat/on-client-command

Conversation

@MohabCodeX

@MohabCodeX MohabCodeX commented Sep 7, 2025

Copy link
Copy Markdown
Contributor

Summary of Changes

Adds the onClientCommand event to MTA client-side logic, enabling Lua resources to intercept, inspect, and cancel client commands prior to local handler execution or server transmission. (Fixes #3160)

Motivation & Semantics

  • Client vs. Server Semantics: Server-side commands can already be intercepted via onConsole and addCommandHandler logic. Previously, client-side commands executed locally or transmitted over the network without giving client resources an early interception hook.
  • Goal: onClientCommand provides client-side parity, allowing resource event handlers to invoke cancelEvent() to block execution locally and prevent sending cancelled commands across the network to the server.

Testing & Verification

Client Lua:

Testing Script
addEventHandler("onClientCommand", root, function(command, ...)
    if command == "restricted" then
        cancelEvent() 
        outputChatBox("This command is disabled!", 255, 100, 100)
    end
end)

addEventHandler("onClientCommand", root, function(command, ...)
    local args = {...}
    local executedByFunction = args[#args]
    table.remove(args, #args) 
    
    local blockedCommands = {"run", "exec", "execute", "lua", "script"}
    for _, blocked in ipairs(blockedCommands) do
        if command == blocked then
            cancelEvent()
            triggerServerEvent("onPlayerUsedExecutor", localPlayer, command)
            return
        end
    end
    
    if not executedByFunction then
        local currentTime = getTickCount()
        if currentTime - (lastCommandTime or 0) < 100 then
            triggerServerEvent("onPlayerUsedExecutor", localPlayer, "rapid_execution")
        end
        lastCommandTime = currentTime
    end
end)

</details>

@FileEX FileEX added the enhancement New feature or request label Sep 7, 2025
Comment thread Client/mods/deathmatch/logic/CClientGame.cpp Outdated
Comment thread Client/mods/deathmatch/logic/CRegisteredCommands.cpp Outdated
Comment thread Client/mods/deathmatch/ClientCommands.cpp
Comment thread Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.h Outdated
Comment thread Client/mods/deathmatch/logic/CRegisteredCommands.cpp Outdated
@MohabCodeX
MohabCodeX requested a review from FileEX September 8, 2025 09:01
FileEX
FileEX previously approved these changes Sep 23, 2025
@MohabCodeX MohabCodeX closed this Jan 6, 2026
@MohabCodeX MohabCodeX reopened this Jan 6, 2026
@MohabCodeX
MohabCodeX requested a review from a team as a code owner January 10, 2026 11:19
Copilot AI lite review requested due to automatic review settings August 3, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new client-side onClientCommand event intended to let scripts observe and potentially cancel command execution, and adjusts command handling on both client and server to better report/propagate “unknown command” cases.

Changes:

  • Adds the onClientCommand built-in client event and invokes it during client command execution flow.
  • Refactors client registered-command execution to return a CommandExecutionResult instead of a boolean.
  • Updates server console command handling to track whether a script handler processed a command and to report unknown commands to players.

Please ensure the final commit message clearly captures the motivation/goal of the change (incl. intended semantics for client vs server commands), and how you tested it (manual steps and/or automated coverage).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
Server/mods/deathmatch/logic/CConsole.cpp Tracks whether script commands were handled; adds unknown-command feedback and returns handled state.
Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp Updates executeCommandHandler to use the new CommandExecutionResult return type.
Client/mods/deathmatch/logic/CRegisteredCommands.h Introduces CommandExecutionResult and updates ProcessCommand signature accordingly.
Client/mods/deathmatch/logic/CRegisteredCommands.cpp Implements the new CommandExecutionResult return type for client command processing.
Client/mods/deathmatch/logic/CClientGame.cpp Registers the new onClientCommand built-in event.
Client/mods/deathmatch/ClientCommands.cpp Invokes onClientCommand and changes client command routing behavior based on local Lua handler presence.
Suppressed comments (1)

Client/mods/deathmatch/ClientCommands.cpp:77

  • onClientCommand currently receives executedByFunction as the 2nd parameter (pushed before the args). The PR description’s usage example expects executedByFunction to be the last vararg (after all args), so scripts will misinterpret the values as soon as any arguments are present.
        CLuaArguments arguments;
        arguments.PushString(szCommandBufferPointer);
        arguments.PushBoolean(false); // executedByFunction
        
        if (szArguments && *szArguments)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +70
// First try to process with registered Lua commands
CommandExecutionResult commandResult = g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommandBufferPointer, szArguments, false);

// If command was handled by Lua, don't send to server
if (commandResult.wasExecuted)
{
return true; // Command was handled locally, don't send to server
}
Comment on lines +94 to +95
CommandExecutionResult result = m_pRegisteredCommands->ProcessCommand(strKey, strArgs, true);
if (result.wasExecuted && !result.wasCancelled)
Comment on lines +154 to +158
CommandExecutionResult CRegisteredCommands::ProcessCommand(const char* szKey, const char* szArguments, bool executedByFunction)
{
assert(szKey);

CommandExecutionResult result;
// Console events
m_Events.AddEvent("onClientConsole", "text", NULL, false);
m_Events.AddEvent("onClientCoreCommand", "command", NULL, false);
m_Events.AddEvent("onClientCommand", "command, executedByFunction, ...", NULL, false);
Comment on lines +127 to +131
// If command wasn't handled, it's unknown
if (!wasHandled)
{
return false;
}
Add the onClientCommand event to allow client-side resource scripts to intercept, handle, or cancel client commands before execution via cancelEvent().

Goals & Motivation:
- Provide parity with server-side command cancellation capabilities.
- Allow resources to intercept client commands early and prevent execution or server transmission when cancelled.

Key Changes:
- Dispatch onClientCommand event on localPlayer prior to Lua command execution in COMMAND_Executed.
- If WasEventCancelled() is true, immediately abort command execution and server dispatch.
- Refactor ProcessCommand to return a CommandExecutionResult struct tracking execution and cancellation status.
- Revert premature 'Unknown command' server echo in CConsole.cpp to maintain onConsole handler compatibility.
- Formatted changed C++ files using utils/clang-format.ps1.
Copilot AI review requested due to automatic review settings August 3, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

Client/mods/deathmatch/ClientCommands.cpp:86

  • The early return true when a registered Lua command executes means onClientConsole no longer fires for commands handled client-side. The linked issue explicitly calls out onClientConsole as showing all executed commands, so this looks like a backwards-incompatible behavioral change. If the goal is only to stop sending the command to the server, keep firing onClientConsole and only skip the network send when handled locally.

        // If command was handled by Lua, don't send to server
        if (commandResult.wasExecuted)
        {
            return true;

Client/mods/deathmatch/logic/CClientGame.cpp:2752

  • The event signature string for onClientCommand ("command, executedByFunction, ...") does not match the usage shown in the PR description (function(command, ...) with executedByFunction being the last vararg). Update the declared argument list to reflect the actual order/shape that scripts should expect.
    m_Events.AddEvent("onClientCommand", "command, executedByFunction, ...", NULL, false);

Client/mods/deathmatch/ClientCommands.cpp:77

  • onClientCommand is documented in the PR description as function(command, ...) with executedByFunction as the last vararg and command arguments passed as individual varargs. The current implementation passes (command, executedByFunction, "<args string>"), so scripts using the documented pattern will misinterpret the arguments. Consider pushing each argument token as a separate Lua argument and appending executedByFunction last; also use the boolean return of CallEvent rather than reading WasEventCancelled() out-of-band.

This issue also appears on line 82 of the same file.

            CLuaArguments arguments;
            arguments.PushString(szCommandBufferPointer);
            arguments.PushBoolean(false);  // executedByFunction
            arguments.PushString(szArguments ? szArguments : "");

            localPlayer->CallEvent("onClientCommand", arguments, false);

            // If command was intercepted and cancelled by event, don't execute or send to server
            if (g_pClientGame->GetEvents()->WasEventCancelled())
            {

Client/mods/deathmatch/logic/CRegisteredCommands.cpp:159

  • ProcessCommand now accepts executedByFunction and returns CommandExecutionResult.wasCancelled, but executedByFunction is unused and wasCancelled is never set. This makes executeCommandHandler's !result.wasCancelled check ineffective and prevents scripts from cancelling function-driven client commands. Consider triggering onClientCommand here when executedByFunction == true, setting wasCancelled and returning early when the event is cancelled.
CommandExecutionResult CRegisteredCommands::ProcessCommand(const char* szKey, const char* szArguments, bool executedByFunction)
{
    assert(szKey);

    CommandExecutionResult result;

Comment on lines 86 to 87
// Let the script handle it
int iClientType = pClient->GetClientType();

switch (iClientType)
Copilot AI review requested due to automatic review settings August 3, 2026 15:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Client/mods/deathmatch/ClientCommands.cpp:144

  • This hunk changes core-command handling by silently treating unknown core commands as handled when bAllowScriptedBind is true. That behavior change isn’t mentioned in the PR description (which focuses on adding onClientCommand) and could hide legitimate “unknown command” feedback outside of keybind scenarios.
        // Call our comand-handlers for core-executed commands too, if allowed
        if (bAllowScriptedBind)
        {
            CommandExecutionResult coreCommandResult = g_pClientGame->GetRegisteredCommands()->ProcessCommand(szCommand, szArguments, false);

            // If core command failed, don't show unknown message (these are usually keybinds)
            if (!coreCommandResult.wasExecuted)
            {
                // Silently ignore failed keybind commands to prevent spam
                return true;
            }
        }

Client/mods/deathmatch/ClientCommands.cpp:78

  • onClientCommand is currently triggered (and cancellation is honored) for all commands typed by the user, which also prevents server-side commands from being sent to the server when the event is cancelled. Per #3160, cancellation should only stop client-side (Lua-registered) commands, while server-side commands should continue to work as before.

This issue also appears on line 133 of the same file.

        // Trigger onClientCommand event first to allow cancellation of any client command
        if (localPlayer != nullptr)
        {
            CLuaArguments arguments;
            arguments.PushString(szCommandBufferPointer);
            arguments.PushBoolean(false);  // executedByFunction
            arguments.PushString(szArguments ? szArguments : "");

            localPlayer->CallEvent("onClientCommand", arguments, false);

            // If command was intercepted and cancelled by event, don't execute or send to server
            if (g_pClientGame->GetEvents()->WasEventCancelled())
            {
                return true;
            }
        }

Client/mods/deathmatch/logic/lua/CLuaFunctionDefs.Commands.cpp:96

  • executeCommandHandler checks !result.wasCancelled, but CommandExecutionResult::wasCancelled is never set in CRegisteredCommands::ProcessCommand (and executedByFunction is currently unused). As-is, cancellation via the new onClientCommand event can’t affect executeCommandHandler, and the API suggests behavior that isn’t implemented.
            CommandExecutionResult result = m_pRegisteredCommands->ProcessCommand(strKey, strArgs, true);
            if (result.wasExecuted && !result.wasCancelled)
            {

Client/mods/deathmatch/logic/CClientGame.cpp:2752

  • onClientCommand is registered with argument signature "command, executedByFunction, ...", but the implementation currently passes a single arguments string (not varargs). This mismatch makes the event contract/documentation ambiguous.
    m_Events.AddEvent("onClientCommand", "command, executedByFunction, ...", NULL, false);

@MohabCodeX
MohabCodeX requested a review from FileEX August 3, 2026 16:58
Copilot AI review requested due to automatic review settings August 5, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

'onPlayerCommand' event for client-side

4 participants