ARTICLE 9 - Cvars, commands, and VM Communication
by HypoThermia
One of the most common questions I'm asked through e-mail is "How do I get
the client to do something from the server code?"... or variations on the
theme. The issue is made more complicated by the fact that client, server, and
user interface each have their own methods and restrictions.
This article aims to de-mystify the methods you can use to pass information
between the three Virtual Machines (VM's). There are several complementary methods,
and I'll outline the advantages and problems with each.
We'll start with a desciption of the client/server/interface model in Quake3.
Understanding the relationship between these components is at the core of the
need to communicate between them. Some of the restrictions in the system are also
clarified.
We'll then move onto the main methods: the setting of variables, also
called Cvars, and how they're related to configuration strings maintained by
the server. We'll then look at the sending of console commands, and finish off
by seeing how we can drive the server scripting engine.
I've tried to write each main section as an overview and introduction to
the sub-sections that follow. As the ideas and definitions can get confusing IT IS
IMPORTANT THAT YOU READ AND UNDERSTAND EACH OF THESE OVERVIEWS before
proceeding with the more specialized descriptions.
1. Server, client, and user interface
Whether you're compiling binaries or bytecode for the platform
independant VMs, it's clear that the game is split into three parts:
server (qagame), client (cgame), and user interface
(ui). Each carries responsibility for implementing a separate
part of the game. What isn't so obvious, and is often forgotten, is that
there's a fourth part: the executable that runs the VMs.
The server and client complement each other and carry just about everything
needed to play the game. The server controls and arbitrates the game: it decides where a player
is on the map, whether they've been hit by that rocket, and where the
momentum kick sends their corpse. The final word in what happens in the
game, the server makes sure that the game world remains consistant.
On the other hand, the client has only a limited view of the world.
It's given information by the server that it needs to present this localized
view to the player... and little else.
The client is responsible for drawing the
screen, playing the sounds, and adding all those little meaty gibs. It
also includes an understanding of the game physics so it can predict motion
and make gameplay smoother over an Internet connection, but it doesn't settle
the final position of the other players (or yourself).
To make this clear with an example: the server concerns itself over who has
the quad, applying the damage multiplier, and settling disputes when two
players try to pick it up at the same time. The client is told who has the
quad, plays announcements and damage sfx, and draws that blue glow that warns
"something dangerous comes this way!"
It's possible for the client and server to be on separate machines, this
is how an Internet game is played. Communication has to occur over a time
delay determined by the connection quality and latency (ping), so there will
be a delay between a command being sent and received. There's nothing
that can be done about that.
The client and user interface are always on your local machine.
Think of them as connected to your graphics and sound cards, and you
won't go far wrong. Servers don't care about these things directly, the
closest they get is sending out a command saying "play this sound"
or "do this action".
The user interface covers all of the menus and pages of controls
used to set up personal preferences and game parameters. It is the user
interface that draws the menu when you hit the Escape key while playing a
game. Anything the behaves like a dialog box or page of controls is drawn (and
interacted with) in the user interface.
Finally, it's important to recognize that there's a fourth part to all this,
hidden away from prying eyes because the source code isn't available: the
binary executable. This runs the VMs required to play the game, manages
the communication between the VMs, controls the network connection if there
is one, and handles all the OpenGL drawing of the graphics.
Any communication between VMs is going to have to pass through the binary
executable. It's the only way.
2. Cvars and configuration strings - passive changes of state
Console variables (Cvars) store the current state of the game system in a
way that can be accessed through the console while playing, and directly
in the game code. Many Cvars are read only: you can't change their value.
Some can only be modified when cheats are enabled. Most are saved
for the next time you play Q3.
As a variable a Cvar isn't a command that must be obeyed immediately.
What usually happens is that your code will check the value the next time
it needs to use it, and adjusts its behaviour depending on the value it finds
stored.
This is helped by the way a Cvar is updated. A copy of the Cvar is
maintained in the source code, this can be read and used in any part of that
source code module. This local copy is updated at regular intervals so you
have both consistancy of value, and an (almost) up to date value.
Note that we are accessing a "copy" in the source code, the original
or "master" of the variable is stored and maintained by the executable. We'll
talk about this in more detail when we look at how and when the Cvar copy
is updated (section 2.2).
2.1 Where to place your Cvar
A Cvar is created in the code as a data structure. You need to provide
a default initialization value, and flags that control access and if
it will be saved in q3config.cfg for permanent storage.
The Cvar that you access in your code is placed in either game/g_main.c,
cgame/cg_main.c, or ui/ui_main.c. You need to choose the most
appropriate place: there is no point in putting a Cvar that controls behaviour
of the server in the user interface code!
There are benefits to putting the Cvar in the correct place, described
in section 2.2.
You can place a Cvar in another source code file, but you won't get
the benefit of automatic initialization and updating.
Example - Setting up a Cvar
We'll take a quick look at how you initialize a Cvar and make it available
for the rest of your code to use. This example looks at the cg_drawFPS
variable in the client that draws the frame rate counter on the screen.
Although taken from cgame code it applies equally well to the
ui in ui_main.c. Its done slightly differently in game,
and we'll look at that in a moment.
Taken from cg_main.c:
vmCvar_t cg_drawFPS;
cvarTable_t cvarTable[] = {
// earlier Cvars snipped
{ &cg_drawFPS, "cg_drawFPS", "0", CVAR_ARCHIVE },
// following Cvars snipped
};
A variable of type vmCvar_t is created to store the current value.
To take advantage of the automatic updating of the Cvar we need a reference
in the array cvarTable[]. This reference takes four components, from
left to right they are:
- &cg_drawFPS
- A pointer to the vmCvar_t that stores the Cvar value
- "cg_drawFPS"
- The name of the Cvar as typed into the console
- "0"
- The default initialization for the variable, this can
be any string
- CVAR_ARCHIVE
- A flag controlling the behaviour of the variable
You can find more of the CVAR_* flags in q_shared.h, they're
quite well documented there.
Finally, so that the Cvar can be accessed easily from within related source
code files, you need to add a reference to the vmCvar_t into the local
header file. For cgame this is cg_local.h, while the ui
and game have their equivalent in ui_local.h and g_local.h
respectively.
For our example you'll find this in cg_local.h:
extern vmCvar_t cg_drawFPS;
I mentioned earlier that the server was slightly different. The change
is in how the Cvar is linked into the list for automatic initialization.
Here's what a typical Cvar looks like in g_main.c:
vmCvar_t g_gametype;
cvarTable_t gameCvarTable[] = {
// earlier Cvars snipped
// latched vars
{ &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_LATCH, 0, qfalse },
// following Cvars snipped
};
The first four entries are identical to their cgame and ui
counterparts, but there are two new entries at the end:
- 0
- Always initialized as zero, this is the number of times
the value of the variable has changed.
- qfalse
- If set to qtrue then an announcement
(text message) is sent to all clients that this Cvar has been changed.
2.2 When Cvars are updated
Even though we can access the Cvars from within the source code, these
are not the "master" or "primary" record of the Cvar
value. This might sound surprising, but arises from the need to communicate
these values between VMs.
The authoritative value of each Cvar is stored within the executable
running the client or server, and the Cvars within the source code are updated
to reflect new values on a regular basis. This update might be slightly
delayed, but ensures consistancy in use. More on this in a moment.
In the situation where the client and server are running on separate
machines, the client still has access to most of the server Cvars. They
can only be read by the client, and not modified.
The reverse situation where the server accesses a clients Cvars is
complicated by the fact that the server can have multiple clients. The mechanism
by which a server can access the client is covered in detail in
section 2.3.3.
The copy of the Cvars stored in each of the *_main.c is updated
on a regular basis at a "convenient" point in the game play cycle.
This point is usually slightly delayed from the time when it was modified in
the primary record.
For the client this update occurs just before the next screen is drawn by
CG_DrawActiveFrame() in cg_view.c. This makes sense when you
realize that these local copies can be accessed through the vmCvar_t
data structure without reference to the primary record. For the entire frame
that is about to be drawn you will get a consistant value for the Cvar if you
use the value stored in a vmCvar_t variable structure.
This is important. Go back, read and understand that last paragraph again.
Even if the Cvar changes in the primary record while the client is rendering
the screen, you get a consistant value to work with while drawing the
entire screen. If you always grab the "most up to date value" with
a trap_* call then you might get rendering errors for that frame. Also,
if you change the value of a Cvar through a trap_* call then you won't
see the new value until the next update.
For the user interface this Cvar update occurs every time the screen is
drawn in UI_Refresh() in ui_atoms.c. For the server code the update
is triggered in G_RunFrame() in g_main.c, when the server updates
the postion of each entity in the world.
As you can see, the Cvars are updated frequently, but not so frequently
as to disrupt a frame of activity. The benefit of these local copies
is twofold: constancy and speed of access to a local data structure.
2.3 Reading and setting a Cvar
With the Cvar created, we need to look at how the value within it can
be accessed and changed. The user interface and client behave in a similar
way, while there are special considerations for the server.
By using a trap_* function to set the variable the primary record
is updated correctly. You MUST NOT change the value of the local copy
of the Cvar - it won't be moved to the primary record, and will only
be over-written at the next update. If the Cvar is archived in
q3config.cfg then the new value won't be saved either.
A Cvar data structure looks like this:
typedef struct {
cvarHandle_t handle;
int modificationCount;
float value;
int integer;
char string[MAX_CVAR_VALUE_STRING];
} vmCvar_t;
You can read the value of the Cvar by accessing the value,
integer, or string fields. This is only possible when the Cvar
is stored within the VM you are coding in. In this case it is the optimal
method of access. For a Cvar from another VM refer to the methods described below.
Although, in principle, you can read and modify Cvars from other VMs, the most
cautious approach is to treat Cvars from a different VM as read-only. You might
need to know a clients preferences to help make the server run more efficiently,
but indiscriminantly changing a clients preferences from the server is
anti-social.
2.3.1 Cvars in the ui
The method used to set the value of the Cvar happens to be the same in
all three modules, treating the Cvar as a string value that can be changed:
void trap_Cvar_Set( const char *var_name, const char *value );
- var_name
- The name of the Cvar as typed on the console
- value
- The new string that replaces the old one
The ui also has an additional method that sets the string
using a float value rather than a string:
void trap_Cvar_SetValue( const char *var_name, float value );
Retrieving the value of the string is done through the complementary methods:
void trap_Cvar_VariableStringBuffer(const char *var_name, char *buffer, int bufsize );
float trap_Cvar_VariableValue( const char *var_name );
- var_name
- The name of the Cvar as typed in the console
- buffer
- Pointer to the char array to store the
current string value in
- bufsize
- The size of the buffer for storing the string,
prevents memory overflow and corruption
Special methods - Configuration Strings
These are described in great detail in the discussion of client Cvars
(section 2.3.2, Special methods - Configuration strings).
They represent a method that allows the client and user interface to read a set of strings
that the server has set: these strings being common to (and used by) all connected clients.
You can access these strings through a call to the following function:
int trap_GetConfigString( int index, char* buff, int buffsize );
- index
- type of string you want to retrieve
- buff
- pointer to the buffer to store the configuration string into
- buffsize
- size of the buff array, I'd recommend using
MAX_INFO_STRING
You can't change the value of the configuration strings from the user
interface. Check below in the client and server sections on how to
process and extract information from them.
(The truth be told: I didn't find out that the user interface could access
these strings until after I'd written the client description of them. I'm
too lazy to re-write that description for here :)
2.3.2 Cvars in cgame
Setting the value of a Cvar in cgame occurs through the same
function call as in ui:
void trap_Cvar_Set( const char *var_name, const char *value );
You can read a Cvar's string value by making a call into the
following function:
void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize );
and this behaves in the same way as the identically named function in
the ui module.
Special methods - Configuration Strings
For quick access to important Cvars stored in the server, there is a
flexible and powerful method available for use. Strictly speaking
not all of variables passed through these configuration strings
are Cvars, neither are all the server Cvars accessible through this
method. However just about all the important ones you'll need in the
client are available.
These configuration strings are set and maintained by the server,
and replicated to each client when a connection is first made. When the server
updates any of these configuration strings, each connected client gets an
updated version.
These configuration strings are also used to
pass information about other clients connected to a server. This information
is limited (but useful), and includes things like player name, choice of model,
and railgun trail colour. Take a look in ClientUserinfoChanged()
in game/g_client.c to see how this string is constructed by the server,
and in CG_NewClientInfo() in cgame/cg_players.c for parsing of
this info in the client.
The most up to date version of the configuration strings known to the
client can be read through a call to:
const char *CG_ConfigString( int index );
- index
- takes a value related to the type of string(s) you want
to retrieve
Most of the values that index can take are
documented in bg_public.h as the CS_* family of flags. You can
extend these values by adding new CS_* flags, it looks like there are
gaps before CS_MODELS available for expansion, and about 350 free slots
after the current value of CS_MAX.
When a change is made to one of these configuration strings, the client
gets notification though CG_ConfigStringModified() in
cg_servercmds.c. You can add or extend any processing of the
changed configuration string here, or examine how existing
strings are parsed to gain further understanding.
Setting these strings is described in the next section about the server.
You can also add strings using two of the existing values of
index provided (described below).
You shouldn't move any of the CS_* values around because
comments about the sound values in the source code suggests optimizations within the
executable for sending this information across the network. As you can't change
the executable, don't mess with the CS_* flags too much!
The pointer returned by CG_ConfigString() can currently take two forms,
and you need to find out which by examining how these CS_* returned
strings are used in the source code. Take a look in
CG_ConfigStringModified() in cg_servercmds.c or do a GREP
through the source code to find out where else they're used or set.
The first form is a simple string that contains the current value. An
index value of CS_MOTD, CS_WARMUP, and
CS_MESSAGE behave in this way. You can then process them with an
atoi() if they're numerical, or treat them as strings.
The second form is a list of variable names and value pairs. These can be
queried through the provided method:
char *Info_ValueForKey( const char *string, const char *key );
- string
- the pointer returned by CG_ConfigString()
- key
- the name of the Cvar string found on the server, and
located in string
You'll find that the two most powerful values of index
are CS_SYSTEMINFO, and CS_SERVERINFO. They contain the
server Cvars that are described respectively by the CVAR_SYSTEMINFO and
CVAR_SERVERINFO flags in g_main.c.
You can add your own Cvars in the server using these flags, they'll then
be sent to each client when they're updated by the server. Use
CVAR_SERVERINFO to describe a string that characterizes the game
playing on the server (gametype, fraglimit, mapname etc.). Typically these are
the values that a program like GameSpy-3D queries and displays.
For the remaining Cvars that describe the low-level state of the sytem,
CVAR_SYSTEMINFO should be used (g_syncronousClients is an example).
Don't put frequently changing values into the CVAR_SERVERINFO
or CVAR_SYSTEMINFO flags as the entire string is resent for the change
of one variable. Very network inefficient.
How the server sets these configuration strings is described later in
this article.
Example - Using Info_ValueForKey()
Taken from CG_DrawInformation(void) in cg_info.c,
we'll get the short name of the map (q3dm10, q3tourney6 etc.) from
CS_SERVERINFO and load an image of the level ready for drawing.
const char *s;
const char *info;
qhandle_t levelshot;
info = CG_ConfigString( CS_SERVERINFO );
s = Info_ValueForKey( info, "mapname" );
levelshot = trap_R_RegisterShaderNoMip( va( "levelshots/%s.tga", s ) );
if ( !levelshot ) {
levelshot = trap_R_RegisterShaderNoMip( "menu/art/unknownmap" );
}
Initialization of mapname is slightly unusual and occurs as part
of the bot AI in ai_main.c instead of g_main.c. Nevertheless,
it has been correctly registered and flagged as CVAR_SERVERINFO.
2.3.3 Cvars in game
Just as in ui and cgame, we have to set the new value of
a Cvar through this function:
void trap_Cvar_Set( const char *var_name, const char *value );
There are two methods available for reading the value of a Cvar. These are
int trap_Cvar_VariableIntegerValue( const char *var_name );
void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize );
The former takes a Cvar name and returns its integer value, and the latter
is used in the way described in the ui (section 2.3.1), above.
Special methods - accessing Cvars set in a client
Of particular importance to the server is the ability to obtain some
of the client parameters. Some of these parameters might be used to optimize the
servers behaviour towards that client, others repackaged and sent on to
the remaining clients for accurate representation.
For a client Cvar to be forwarded to the server when it is changed, it must
be described by the CVAR_USERINFO flag. The only example of this is the
teamoverlay Cvar in the client: it's also a CVAR_ROM so it can only be
changed from within the source code, not on the console. This Cvar controls
whether the server sends bandwith sucking stats during team games. Other
critical Cvars appear to be flagged as CVAR_USERINFO by the
executable directly.
When a client changes one of its Cvars, the updated value is sent to the
server and notification arrives at ClientUserinfoChanged() in
g_client.c. The information can be validated (if required), modified
(if absolutely necessary), or repackaged as a configuration string and sent on
for other clients to use.
The client configuration is read and set through the following functions:
void trap_GetUserinfo( int num, char *buffer, int bufferSize );
void trap_SetUserinfo( int num, const char *buffer );
- num
- the index value identifying the client
- buffer
- pointer to the buffer string for storing/setting the
user info. I'd recommend char buffer[MAX_INFO_STRING]
- bufferSize
- size of the buffer string, preventing overflow
on read
The client index is just the array index of the global server array
level.clients that points to the gclient_t data structure
representing that player. Look in ClientConnect() in g_client.c
to see this. To obtain the client index given a gentity_t* use the
following code (which relies on the properties of C pointer subtraction):
int clientNum;
gclient_t* client;
gentity_t* ent; // must point to a valid entity structure
client = ent->client; // must be non-zero for an entity that's a client
clientNum = client - level.clients;
Just like the configuration strings described earlier, you can access
the value pairs using Info_ValueForKey(). This example checks
whether cg_predictItems is enabled in the client. It's derived from
ClientUserinfoChanged() in g_client.c:
int clientNum; // set in the argument to ClientUserinfoChanged()
gentity_t *ent;
char *s;
gclient_t *client;
char userinfo[MAX_INFO_STRING];
ent = g_entities + clientNum;
client = ent->client;
trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) );
// check the item prediction
s = Info_ValueForKey( userinfo, "cg_predictItems" );
if ( !atoi( s ) ) {
client->pers.predictItemPickup = qfalse;
} else {
client->pers.predictItemPickup = qtrue;
}
In order to use the special parsing function Info_ValueForKey()
when constructing your own configuration strings, you need to know how to
build these strings.
There's a working example in ClientUserinfoChanged() in g_client.c,
but we'll take a trivial one for clarity. This is how the string would
be constructed in the C source (note that the double slash as used in the source code
is reduced to a single slash by the compiler).
"string_one_key\\string_one_value\\string_two_key\\string_two_value"
Values are paired up: each having a unique identifying name (key) and an
associated value. The '\\' character is used to separate the values,
and you shouldn't pass this character as part of a key name or value
(this will prevent Info_ValueForKey() from working),
nor a quote ("), or a semi-colon (;). I got these limitations from
Info_Validate() in q_shared.c, but be wary of using
non-standard characters, and never use a string termination '\0'.
In this example string_one_key and string_two_key are the
possible arguments to Info_ValueForKey(), and string_one_value
and string_two_value will be returned.
The key values shouldn't need to be Cvars, they're just a means of identifying
the associated value uniquely. If you are only ever going to send a
single string value, then you don't need to use this key/value pair system.
You will almost certainly end up constructing these strings using the va()
variable argument substitution strings, I deliberately avoided them to reduce
the complexity of this explanation.
Although there are no examples of the server over-riding the client settings
using trap_SetUserinfo(), presumable you just need to send the
variables you want changed using the key/value pairing described
above. The best place for the server to do this is in
ClientUserinfoChanged() in g_client.c
Special methods - configuration strings in the server
As already described earlier from the point of view of the client,
configuration strings contain global information that is maintained by the
server. It is transmitted to all the clients connected to the server,
and updated when changes are made by the server. The client can't modify
these strings, but can (and does) make use of the information.
A configuration string is read or changed using these functions:
void trap_GetConfigstring( int num, char *buffer, int bufferSize );
void trap_SetConfigstring( int num, const char *string );
- num
- the index of the configuration string
- buffer
- destination buffer for copying the config stirng to.
Use a char array of size MAX_INFO_STRING
- bufferSize
- the size of buffer, prevents overflow
- string
- new value for the configuration string
Each configuration string is described by an index CS_*, a list of
these values can be found in bg_public.h. Although there's space for
MAX_CONFIGSTRINGS (1024) values, the smallest amount of change that can
be transmitted across a network connection is a single string. This is a
network efficient way of transmitting state changes in the server, and other
global variables maintained by the server.
Two special config strings are CS_SYSTEMINFO and
CS_SERVERINFO. Described in more detail in the client section above,
the Cvars that belong to this group are automatically updated when they're
modified by the server using trap_Cvar_Set(). Try to avoid putting
frequently changing values in here because it's almost certain that all Cvar
strings in this group are transmitted in one go, and this is network
inefficient. You can create your own specific CS_* flags and
groups instead (like CS_FLAGSTATUS).
For an example we'll take a look at how the server updates
CS_FLAGSTATUS, this code is from g_team.c:
void Team_SetFlagStatus( int team, flagStatus_t status )
{
qboolean modified = qfalse;
switch (team) {
case TEAM_RED :
if ( teamgame.redStatus != status ) {
teamgame.redStatus = status;
modified = qtrue;
}
break;
case TEAM_BLUE :
if ( teamgame.blueStatus != status ) {
teamgame.blueStatus = status;
modified = qtrue;
}
break;
}
if (modified) {
char st[4];
st[0] = '0' + (int)teamgame.redStatus;
st[1] = '0' + (int)teamgame.blueStatus;
st[2] = 0;
trap_SetConfigstring( CS_FLAGSTATUS, st );
}
}
The flag status is sent as a two digit pair, one each for the
red and blue flags. Sending the values as a pair of numbers is
a limitation of strings: we must avoid sending anything that might
prematurely terminate the string (a char value of '\0'), so
its encoded based on the ASCII value for the character '0'.
This code fragment shows how its decoded in the client, in the function
CG_ConfigStringModified() in cg_servercmds.c:
} else if ( num == CS_FLAGSTATUS ) {
// format is rb where its red/blue, 0 is at base, 1 is taken, 2 is dropped
cgs.redflag = str[0] - '0';
cgs.blueflag = str[1] - '0';
}
and you can see that the binary value of zero, if stored directly in
str[0], would have prevented the rest of the string from
being transmitted. The status of the blue flag would then be unknown.
2.4 Some special Cvars
There are some Cvars that have slightly unusual properties. There are only
a few of these Cvars, and I'll only touch on them briefly.
One reason for these special Cvars is so that one VM can control or influence
another in a transparent manner. The three
Cvars that fall into this category are sv_running, cl_paused,
and g_syncronousClients.
If sv_running is set then the server is running on your local machine.
This is important as its presence enables some menu items in the ui
that can be used to influence the local server.
You can also check for this in the client code by accessing
cgs.localServer (zero if we're connected to a remote server),
it's set in the following manner:
char var[MAX_TOKEN_CHARS];
trap_Cvar_VariableStringBuffer( "sv_running", var, sizeof( var ) );
cgs.localServer = atoi( var );
cl_paused is used to indicate to the client that the user interface
has drawn a menu on screen while the client is running.
Setting this variable to a value of 1 (one) will pause
the game if the server is local. This appears to be managed by the executable,
stopping the server and client VM's temporarily, rather than in any
of the VM code itself.
The third variable is g_syncronousClients, this is set if the server
allows demo recording, and it also plays a part in client side prediction.
It is created in the server VM, and replicated to the client VM via
the CS_SYSTEMINFO configuration string. The client
can't modify this value, but it can be read in and acted upon.
Note that the creation in the client of the vmCvar_t structure
doesn't have any CVAR_* flags associated with it, this is because the
server stored value is the one with the CVAR_ARCHIVE flag set. In other
words, the server has control over the value, while the client contains only
a reference.
Another variable, apparently set by the executable, is bot_enable.
If this is set to zero then bots aren't loaded onto the system. There is no
corresponding copy stored in a VM, it appears to be managed by the
executable running the server (but is available to the client). Again,
some menu options depend on this value to determine whether they are
enabled or not.
3. Console commands - do this, now!
With the large amount of ground covered by the Cvars and server
configuration strings, you're going to find console commands has less content.
Unfortunately the picture is made difficult to follow because of the
possibility of confusion between the client and server consoles.
In addition the server can issue commands to the client,
in a way that looks like the console commands, but is in fact separate.
I've tried to give the best description of these as I possibly can below.
Client console commands, as their name suggests, can be typed in from the drop
down console. There is also a way of executing these commands from within
the source code. Whether the command originates from the console or code
is irrelevant when it's acted upon. This means that these commands can't
be hidden from the user in the same way as configuration strings can.
A client console command can be handled in either the user interface, client,
or server parts of the code. This makes it a very useful way of asking another
VM to do something for you (for example: displaying a menu system
using the user interface). We'll look at the client console in
section 3.1.
The server console is another matter. These commands control how the
server behaves and they can only be accessed when the server is
running on the same machine, or a particular client has the
administrator privilige to change the setup of the server remotely.
Some commands that fall into this category are addip and addbot,
and we'll cover the server console in section 3.2.
What is potentially confusing is that when the client and server are running on
the same machine, both the client and server console commands
are entered in the same way. The typing of commands through the drop down
console is, at best, a confusing use of terminology.
Apply this rule of thumb: A command typed in by the player is a
client console command if is expected to behave the same whether
the server is local (running on the same machine) or remote (on a LAN
or across the Internet).
3.1 Client console commands
Once a console command has been recognized (Cvars are filtered out first),
it arrives at the handler for each of the VM modules. For the user interface
this handling function is UI_ConsoleCommand() in ui_atoms.c,
the client has CG_ConsoleCommand() in cg_consolecmds.c, while
the server uses ClientCommand() in g_cmds.c.
Some pre-processing has already been done for you, and you can get
at arguments for the command by using trap_Argv(int pos) (the argument at a
given position), and trap_Argc() (the total number of arguments). The
user interface and client have "wrappered" versions that can also be
accessed through UI_Argv() and CG_Argv(). You'll find that
Argv(0) is always the command string itself.
3.1.1 Client console commands in the user interface
There's no rocket science to adding a console command to the user
interface: just add a Q_stricmp( cmd , "yourcmd")==0 comparison into
UI_ConsoleCommand() and route it to you handling function.
The most obvious use of a command in the user interface is to pull up a
menu for the user to interact with, and there's an obvious example in the
ui_teamOrders command. One thing to be cautious of if you're
displaying a menu: you'll need to protect against repeatedly adding new menus
until the menu stack is used up.
Surprisingly there's no way of sending out a console command from the
user interface, it has to go through Cvars or server scripting (see
section 4).
I would guess that's because it's possible for the user interface to
be running when the client and server code aren't. Any Cvars that are
important and stored outside the user interface are latched anyway - so any
updated value is still passed correctly.
You can get some information about the state of the client through a call
to trap_GetClientState(uiClientState_t*). This data structure is
described in ui_public.h, and includes (amongst other things) access
to the connection state of the client. This might influence how you draw
the menu: in a connected game you'll want to leave the background "transparent"
so the player can see the game screen. This code fragment
taken from UI_ConfirmMenu() in ui_confirm.c shows how this is
done:
uiClientState_t cstate;
trap_GetClientState( &cstate );
if ( cstate.connState >= CA_CONNECTED ) {
s_confirm.menu.fullscreen = qfalse; // no black background
}
else {
s_confirm.menu.fullscreen = qtrue;
}
See my articles on the user interface menu for more information
on how and why ths works.
Special treatment - The In Game (ESC) Menu
When you press the Escape key while playing a game you get the In Game Menu that
allows you to change teams, add bots, change game settings etc. This menu is
unusual: it doesn't use a console command to activate it.
The ESC key is recognized in the executable and passed into
the user interface VM through vmMain() in ui_main.c. Although
several menus are created by this method, and all pass through
UI_SetActiveMenu() in ui_atoms.c I haven't been able to find
a way for the user to extend this and add their own menus.
The good news is that you can use console commands to create your own menus.
Just make sure that the commands in UI_SetActiveMenu() don't overwrite
your menu, and that UIMENU_NONE is always honoured.
3.1.2 Client console commands in cgame
Once again, adding a client console command is straight forward. It's made even
easier by the use of an array of console commands and associated function
handlers in cg_consolecmds.c.
As the code fragment below shows, just add an new entry into the the
commands[] array, with your command name and your handling function.
This handling function must be of type void function_name(void) for the
array to work.
typedef struct {
char *cmd;
void (*function)(void);
} consoleCommand_t;
static consoleCommand_t commands[] = {
{ "testgun", CG_TestGun_f },
{ "testmodel", CG_TestModel_f },
// code snipped
{ "tcmd", CG_TargetCommand_f },
{ "loaddefered", CG_LoadDeferredPlayers } // spelled wrong, but not changing for demo...
};
You can send out a client command to another VM by calling
trap_SendConsoleCommand(const char* cmd_string). You can add
arguments with additional information so long as you separate each of them with a
space. When they arrive at the VM that handles them they'll already be
processed so each argument is easily accessible.
There is also a similarly named (and possibly confusing) function
called trap_SendClientCommand(const char* cmd_string). This
sends the command string directly to the server, without a chance being given
to the user interface to act upon it. Use it as part of exclusively client-server
issues.
When the command string arrives at the server, whether it came from
trap_SendConsoleCommand() or trap_SendClientCommand(), it is
handled in ClientCommand() in g_cmds.c.
This example shows how the tell_target client console command is
broken down into a single tell command. Ultimately this will be processed in
the server for distribution to the victim under the crosshair.
static void CG_TellTarget_f( void ) {
int clientNum;
char command[128];
char message[128];
clientNum = CG_CrosshairPlayer();
if ( clientNum == -1 ) {
return;
}
trap_Args( message, 128 );
Com_sprintf( command, 128, "tell %i %s", clientNum, message );
trap_SendClientCommand( command );
}
If you create a command in the server and you want TAB completion to work
in the client, then you should also register the server command in the client.
This is done in CG_InitConsoleCommands() in cg_consolecmds.c
by adding another trap_AddCommand() to the list.
Why do it this way? Well if the server is running on another machine
then you don't want TAB completion to depend on the availability of
the network connection.
3.1.3 Client console commands in the server
There are two types of command that the server can accept, so it's important
to put your command into the right place. Go back and read the start of
section 3 to understand the
difference. We'll look at the client console commands here, while
dealing with the server console in section 3.2.
Since we're looking at client console commands we need to work in
ClientCommand() in g_cmds.c. Each command that you add is just
a straight forward Q_stricmp() comparison that passes control to your
handler if passed.
Note that there is a watershed between commands that can be executed at any
point, and those commands which don't make any sense during the intermission
(when a level is over and the scoreboard is displayed, but before the next level
is loaded).
If you want TAB completion of the command name in the client, then you'll
need to add the name of the command as described at the end of
section 3.1.2, above.
I've not found a way for the server to send a client console command to
a particular client (such a command could reach either the ui or
cgame). However, there is a way that the server can send a command
to the client (cgame), and this is covered in
section 3.3. Although this seems to
be the reciprocal method for sending a client console command from the server,
the commands it sends can't be typed into the client - they can only be sent
by the server.
3.2 Server console commands
Notice the change of subject!
We're now talking about commands that get to
the server console, described and defined at the start of
section 3.
These are the commands that can be typed on a client drop down console
when the server is running on the same machine, or if the client has
administrative privilige on a remote machine. Usually these commands have a drastic
effect on gameplay or the behaviour of the server. You want to be careful
who you let use these commands, and many of them might be made available through
the voting system.
All of these commands arrive in ConsoleCommand() in g_svcmds.c
for processing (note that console in this case means server console).
You don't need to do anything special to separate them
from the client console commands, this is done for you automatically
(and is outside your control!)
As this snipped version of ConsoleCommand() shows, you need to
return qtrue if you do handle the server command. Trace through
the code to Svcmd_AddBot_f() in g_bot.c to see how
trap_Argv() is used to grab the arguments to addbot.
qboolean ConsoleCommand( void ) {
char cmd[MAX_TOKEN_CHARS];
trap_Argv( 0, cmd, sizeof( cmd ) );
// code snipped
if (Q_stricmp (cmd, "addbot") == 0) {
Svcmd_AddBot_f();
return qtrue;
}
// code snipped
return qfalse;
}
3.3 Commands issued to the client from the server
When the server needs to send a command to one or all clients then it uses
the following function:
trap_SendServerCommand(int clientNum, const char* command_string)
The clientNum is the identifying index of the client,
look in section 2.3.3 for an example of how to
get a clientNum if it isn't provided for you. clientNum may
also be -1, in which case the command is broadcast to all connected clients.
The command will arrive in the client in CG_ServerCommand()
in cg_servercmds.c, all ready for the client to deal with.
In an attempt to make this clear to a possibly confused reader, try the following
experiment: start up Q3 and get a map going. Pull down the console and
type in "/cp test" (without the quotes of course!). It will show an
error message.
Now look at CG_ServerCommand() in cg_servercmds.c for
the cp command. It draws some text in the centre of the screen
(cp - center print) and is sent by the server. Can you now see that
commands sent by the server aren't connected to the client console?
4. Server scripting - play the game
The last method of control is aimed purely at the server. You can send a
list of commands to the server (to be accurate, the binary executable running
the game VM), that are equivalent to running a script. These commands
remain in memory and are used (for example) to set up a map rotation. Think of
this method as having the same effect as using the /exec command in
the drop down console.
You can add these commands from the user interface part of the
source code, or as part of the game code. You are effectively limited
to setting up and controlling a server on your machine only, or having a
server manipulate its own script. It shouldn't be possible for a local
user interface to manipulate a remote server using these functions.
From within the user interface code:
void trap_Cmd_ExecuteText( int exec_when, const char *text );
and from within the server code:
void trap_SendConsoleCommand( int exec_when, const char *text );
Don't be confused by the fact it carries the same name as a function
that's used in the client, its behaviour isn't the same. Think of it
as acting on the server console only.
The text argument is easy enough: it's the text string that contains
a command or sets a variable. You should add a newline character '\n'
when you issue a single command, or you can put a group of commands together
in the same string, separated by a semi-colon, and terminated with a newline.
If you don't put the newline in then you'll get two commands running together
and confusing the executable's script engine.
The other argument, exec_when, is a bit more difficult to understand
from just code reading and without access to the source code for the executable.
Here's the allowed values and what I've been able to glean:
- EXEC_NOW
- The string is executed immediately and control won't
return to the VM until it's completed. Use with extreme caution in case a
command that you use causes the VM to unload and then reload.
- EXEC_INSERT
- This flag causes a command to be
"forgotten" about after it's executed. Control will return
immediately while the command is queued for execution in the immediate future.
- EXEC_APPEND
- Think of this as appending text to a script file
that's stored by the server. Control returns immediately, and values will
remain stored for future reference. This is the most commonly used option.
Although the EXEC_NOW flag is clear enough, it's possible for the
other two to be confused. If you start with the idea that EXEC_APPEND is
building a script (list of commands) within the executable running the server,
then you can see that this script will always be present until the server is
shut down. Although you can add new commands and have them executed, you would
use the EXEC_INSERT so that it wasn't permanently added to the script
being built by EXEC_APPEND.
It appears that the EXEC_INSERT command isn't used in the
user interface, possibly because the server can't be guaranteed to be
running. If you do try to use EXEC_INSERT in the user interface,
use it with caution.
Example - setting up a server from the ui
Taken from ServerOptions_Start() in ui_startserver.c, this
code fragment shows the bots selected for a map being added to the server
script through the use of EXEC_APPEND. This code is run when you
create a server from the multiplayer menu, or have a skirmish from the single
player menu.
Notice that each string for the addbot command is finished off with
the '\n' character.
The wait command is interesting: some of the server parameters are
set through trap_Cvar_SetValue(), and this allows their value to
propagate through to the binary executable. As this code is run when we set up
a server on our local machine, the time delay only needs to be a short one.
int skill;
int n;
char buf[64];
// add bots
trap_Cmd_ExecuteText( EXEC_APPEND, "wait 3\n" );
for( n = 1; n < PLAYER_SLOTS; n++ ) {
if( s_serveroptions.playerType[n].curvalue != 1 ) {
continue;
}
if( s_serveroptions.playerNameBuffers[n][0] == 0 ) {
continue;
}
if( s_serveroptions.playerNameBuffers[n][0] == '-' ) {
continue;
}
if( s_serveroptions.gametype >= GT_TEAM ) {
Com_sprintf( buf, sizeof(buf), "addbot %s %i %s\n",
s_serveroptions.playerNameBuffers[n], skill,
playerTeam_list[s_serveroptions.playerTeam[n].curvalue] );
}
else {
Com_sprintf( buf, sizeof(buf), "addbot %s %i\n",
s_serveroptions.playerNameBuffers[n], skill );
}
trap_Cmd_ExecuteText( EXEC_APPEND, buf );
}
5. The end of the road...
We've come a long way with this article, and covered a lot of ground that
turned out to be more complex that even I'd originally thought. So long as
you keep a clear picture in your mind of the differences and relationships
between the user interface, the client, the server, and the executable, then
you should be able to use this information to the full.
When I originally planned this article I was going to talk about manipulating
information stored in entities too - I'll have to save that for another time
as this article is now far too long to do entities justice.
There are also some bot commands for setting variables, but I've left
them out for reasons of space and specialization. The ideas that govern the
use of these bot commands are going to be very similar to the conventional
methods I've described in this article.
Let me know if there are any problems with the article, and if it's been
of help to you.
HypoThermia
|