Created By: Achilles
eMail: achilles@inside3d.com
Difficulty Scale: Intermediate


Alright, what were going to do in this Tutorial is add a Team Radio feature. What this means is the players can now use pre-recorded .WAV files to send audio queues to eachother. Ranging from taunts, stupidity, to team based sounds.

I will make a couple of asumptions though.

  • You are familiar with your Compiler and its interface
  • You can find aspects of the code based on Function and not simply line numbers.
  • You are using the v3.20 source codebase from id Software

    The first things we want to accomplish is add a functionality to turn the radio on and off. This is accomplished by adding a new value to the Player's info that survives a player respawn, i.e. we don't want it to reset everytime the player gets killed.

    In g_local.h find the client_respawn_t structure, it should look somthing like this:

    typedef struct
    {
    	client_persistant_t	coop_respawn;	// what to set client->pers to on a respawn
    	int			enterframe;			// level.framenum the client entered the game
    	int			score;			// frags, etc
    
    	vec3_t		cmd_angles;			// angles sent over in the last command
    
    	qboolean		spectator;			// client is a spectator
    } client_respawn_t;
    
    Underneath int score; you need to add our value for the Radio toggle.
    // Inside3D Team Radio Tutorial - begin
    	int			radio_power;    // Its a toggle that functions as the power
    // Inside3D Team Radio Tutorial - end
    
    Now we the data we need built into the player that allows the radio to be turned on and off. In the code it can now be accessed via the ent->client->resp.radio_power value. Since its a toggle it can be true or false.

    Were all done with g_local.h

    We now need to set the value of ent->client->resp.radio_power when the client connects.

    Open up p_client.c

    Your looking for the the ClientConnect Subroutine, the header for the routine looks like this:

    /*
    ===========
    ClientConnect
    
    Called when a player begins connecting to the server.
    The game can refuse entrance to a client by returning false.
    If the client is allowed, the connection process will continue
    and eventually get to ClientBegin()
    Changing levels will NOT cause this to be called again, but
    loadgames will.
    ============
    */
    qboolean ClientConnect (edict_t *ent, char *userinfo)
    {
    	char	*value;
    
    	// check to see if they are on the banned IP list
    	value = Info_ValueForKey (userinfo, "ip");
    	if (SV_FilterPacket(value)) {
    		Info_SetValueForKey(userinfo, "rejmsg", "Banned.");
    		return false;
    	}
    
    	.
    	.
    	.
    
    What we need to do is find where exactly in this routine the value can be set. It just so happen I did this for you.

    Directly underneath the following If statement contained in the ClientConnect function.

    	.
    	.
    	.
    	if (ent->inuse == false)
    	{
    		InitClientResp (ent->client);
    		if (!game.autosaved || !ent->client->pers.weapon)
    			InitClientPersistant (ent->client);
    	}
    
    Simply add the following
    // Inside3D Team Radio Tutorial - begin
    	ent->client->resp.radio_power = 1;
    // Inside3D Team Radio Tutorial - end
    
    For a quick check of your code, go ahead and compile it now, no actual differences were made that can be used yet, but you should get a nice clean compile, if not re-check your steps. Now go grab a Coke, Pepsi or Mountain Dew. Nice work!

    Now is when the tutorial gets a bit tricky, we need to add two new files to the Project. This is done slightly different in every compiler out there. Simply add both files and make sure they contained in the make file.

    The files we want to add is called p_radio.c and p_radio.h

    p_radio.h containes the following

    // These are functions for the team radio
    
    #define for_each_player(PLAYER,INDEX)				\
    for(INDEX=1;INDEX<=maxclients->value;INDEX++)			\
    	if ((PLAYER=&g_edicts[i]) && PLAYER->inuse)
    
    
    void RadioToggle_f(edict_t *self);
    
    void Radio_f(edict_t *self, char *channel, char *msg);
    
    That file contains our prototypes for all our radio functions so we can call them up easily.

    Now the big file, p_radio.c contains the follow:

    #include "g_local.h"
    #include "p_radio.h"
    
    void RadioToggle_f(edict_t *self)
    {
    	if (self->client->resp.radio_power)
    	{
    		self->client->resp.radio_power = 0;
    		gi.cprintf(self, PRINT_HIGH, "Radio OFF\n");
    	}
    	else
    	{
    		self->client->resp.radio_power = 1;
    		gi.cprintf(self, PRINT_HIGH, "Radio ON\n");
    	}
    }
    
    
    // this functions is used to send commands to the clients.
    void stuffcmd(edict_t *ent, char *s) 	
    {
       	gi.WriteByte (11);	        
    	gi.WriteString (s);
    	gi.unicast (ent, true);	
    }
    
    void Radio_f(edict_t *self, char *channel, char *msg)
    {
    	edict_t	*player;
    	int		i;
    	char	*cmd, *pos;
    	char	*prefix;
    	char	*info;
    
    	cmd = "\0";
    
          // if the players radio is off, lets not bother doing anything
    	if (!self->client->resp.radio_power)
    		return;
    
    	// Well this is where we check the model to see what sound to send out.
    	// Based this check from the IsFemale() Subroutine
    
    	info = Info_ValueForKey (self->client->pers.userinfo, "skin");
    	if (info[0] == 'f' || info[0] == 'F')
    		prefix = "fem_";
    	else if (info[0] == 'c' || info[0] == 'C')
    	{
    		if (info[1] == 'y' || info[1] == 'Y')
    			prefix = "cyb_";
    		if (info[1] == 'r' || info[1] == 'R')
    			prefix = "crk_";
    	}
          // If its an unidentifiable model, use the male sounds by default
    	else
    		prefix = "male_";
    
    	// Okay gotta make sure people can't stuff extra commands down to kill someone.
    	// This is also why i send everything to the guy that sent it originally also
    	if (pos = strstr(msg,";"))
    		pos[0] = 0;
    		
    	
          // If the sound is bound for all players in the game this is what is executed
          // .WAVs are stored in sound/world/*.wav
    	if (Q_stricmp (channel, "ALL") == 0)
    	{
    		for_each_player(player, i)
    		{
    			if (player->client->resp.radio_power)
    			{
    				sprintf(cmd, "play world/%s%s\n", prefix, msg);
    				stuffcmd(player, cmd);
    			}
    		}
    		return;
    	}
          // If it's only based for players nearby AKA the player is yelling to people in game this is ran.
          // .WAVs are stored in sound/voice/*.wav
    	else if (Q_stricmp (channel, "ROOM") == 0)
    	{
    		sprintf(cmd, "voice/%s%s.wav", prefix, msg);
    		gi.sound (self, CHAN_VOICE, gi.soundindex(cmd), 1, ATTN_NORM, 0);
    		return;
    	}
    }
    
    Radio_Toggle_F is simply a toggle function, when it is called it changes the value of ent->client->resp.radio_power

    Radio_F is commented heavily and should be fairly self-explanatory.

    Now again lets see if the code will compile cleanly. All the functions are there and ready to go, just nothing is being called. This is a good place to check to see if the files were added to the project correctly and so forth. We'll get into the actual usage in a moment, right now, lets see if what we have so far is good and clean.

    I am assuming since your going on everything compiled and the new files are being included in the compile.

    Basically what happens is when a sound is played, the Radio_F routine check to find all players that should receive the sound. All players that are receiving the sound are sent a command that is executed on the local console. Example

    play male_buzzoff.wav
    It simply plays the .WAV to the speakers. Because I am using the players console, there is a check to prevent other things from being sent like Kill and quit commands.

    Now the last part of our tutorial, adding the command so these routines actually get used.

    This is done in g_cmds.c

    At the bottom of the file in the ClientCommand Subroutine you need to add some code after these lines

    	else if (Q_stricmp(cmd, "playerlist") == 0)
    		Cmd_PlayerList_f(ent);
    
    you need to add the following lines
    // Inside3D Team Radio Tutorial - begin
    	else if (Q_stricmp (cmd, "radio") == 0)  // Radio Toggle
    		RadioToggle_f(ent);
    	else if (Q_stricmp (cmd, "play_world") == 0 && !ent->client->resp.spectator)  // Radio to everybody
    			Radio_f(ent, "ALL", gi.argv(1));
    	else if (Q_stricmp (cmd, "play_voice") == 0 && !ent->client->resp.spectator)  // Talk to Everyone within a Earshot
    			Radio_f(ent, "ROOM", gi.argv(1));
    // Inside3D Team Radio Tutorial - end
    
    this adds a couple extra command for the players to bind.

    RADIO all by itself will toggle the radio on and off. Off simply means he will not hear any of the sounds sent by other players.

    play_world command will play a command to every player in the game with the radio on. Command in reality is part of the name in the WAV file. IE play_world buzzoff wil actually play a file called male_buzzoff.wav etc...

    play_voice is identical to play_word, except it only players to players in close proximity to the sender.

    The !ent->client->resp.spectator is a check so Spectators to a game cannot send audio commands to the players in game.