Command API, Part 3 of 3

Today’s wiki update is a short addition about client-only commands. What are client-only commands? They are commands that are ONLY meant to be processed from clients, and never from the server.

In Half-Life 1, the distinction was simple. If a command was typed through the client’s console, it would be sent through the DLL_FUNCTIONS::ClientCommand function. Server commands had to be explicitly registered, whereas client command strings were always forwarded through one function. AMX Mod X derived four hooks from this:

  • register_srvcmd: Explicitly registers one server command through the HL1 engine.
  • register_clcmd: Adds a filter rule for catching strings passed through ClientCommand.
  • register_concmd: Adds both a filter rule for ClientCommand AND registers an explicit server command.
  • client_command(id): Forward called whenever a string passes through ClientCommand.

How does Half-Life 2 complicate this? When explicit server commands are registered, they can optionally be flagged as FCVAR_GAMEDLL (and in fact any command the game mod registers should have this flag). When a client sends a command string from their console, the Engine does the following:

  1. Are there any server commands matching what the client typed?
  2. If not, pass the string normally through IServerGameClients::ClientCommand.
  3. If so, does the server command have the FCVAR_GAMEDLL flag?
  4. If not, pass the string normally through IServerGameDLL::ClientCommand.
  5. If so, instead, act as if the server command itself was called. The server command must then listen to IServerGameClients::SetCommandClient to see whether the server or a client sent the command.

This is actually much closer to the AMX Mod X model, because it allows for dual-purpose commands with one implementation. Let’s take a look at how the SourceMod functions work with this new system:

  1. RegServerCmd: Explicitly registers a server command, or hooks an already existing one. The callback is only used if the source was the server, and not a client.
  2. RegConsoleCmd/RegAdminCmd: Hooks an existing console command; if it doesn’t exist, a new one is created without the FCVAR_GAMEDLL flag. The callback is called whether or not it’s from a client. It is hooked from both the explicit server command’s function and the IServerGameClients::ClientCommand function, because when a client uses it, it may go to either one.
  3. OnClientCommand(id, args): Hooks any command string passing through IServerGameClients::ClientCommand.

It should now be apparent why we really couldn’t implement a register_clcmd filter — not all client commands pass through IServerGameClients::ClientCommand! Likewise, this means the OnClientCommand forward will not recieve some client commands. Most importantly is that “say” is one of these dual commands, as it can be used by both the server and the client. The most useful command to filter can’t be filtered!

This is actually a somewhat clever idea on Valve’s part, so it’s not a design flaw. It just means that in order to hook say, you must register it as a console command, and that it will never pass through OnClientCommand (at least in Counter-Strike:Source).

Leave a Reply

You must be logged in to post a comment.