From 122de31ac2834809fb15088eda6f5278d4525fb1 Mon Sep 17 00:00:00 2001
From: "Tony J. White" <tjw@tjw.org>
Date: Sat, 3 Oct 2009 11:44:02 +0000
Subject: * voice chat stuff (no, not like TeamSpeak)

---
 Makefile                  |   2 +
 src/cgame/cg_local.h      |  11 +-
 src/cgame/cg_main.c       |  13 +-
 src/cgame/cg_players.c    |  11 +
 src/cgame/cg_servercmds.c | 193 ++++++++++++++
 src/game/bg_public.h      |  56 ++++
 src/game/bg_voice.c       | 652 ++++++++++++++++++++++++++++++++++++++++++++++
 src/game/g_active.c       |   6 +
 src/game/g_client.c       |   8 +-
 src/game/g_cmds.c         | 123 +++++++++
 src/game/g_local.h        |   9 +
 src/game/g_main.c         |   8 +
 12 files changed, 1088 insertions(+), 4 deletions(-)
 create mode 100644 src/game/bg_voice.c

diff --git a/Makefile b/Makefile
index 68c9ee05..31f60014 100644
--- a/Makefile
+++ b/Makefile
@@ -1333,6 +1333,7 @@ CGOBJ_ = \
   $(B)/base/cgame/bg_slidemove.o \
   $(B)/base/cgame/bg_lib.o \
   $(B)/base/cgame/bg_alloc.o \
+  $(B)/base/cgame/bg_voice.o \
   $(B)/base/cgame/cg_consolecmds.o \
   $(B)/base/cgame/cg_buildable.o \
   $(B)/base/cgame/cg_animation.o \
@@ -1384,6 +1385,7 @@ GOBJ_ = \
   $(B)/base/game/bg_slidemove.o \
   $(B)/base/game/bg_lib.o \
   $(B)/base/game/bg_alloc.o \
+  $(B)/base/game/bg_voice.o \
   $(B)/base/game/g_active.o \
   $(B)/base/game/g_client.o \
   $(B)/base/game/g_cmds.o \
diff --git a/src/cgame/cg_local.h b/src/cgame/cg_local.h
index 2aeaf95f..de97e48b 100644
--- a/src/cgame/cg_local.h
+++ b/src/cgame/cg_local.h
@@ -771,6 +771,9 @@ typedef struct
 
   sfxHandle_t customFootsteps[ 4 ];
   sfxHandle_t customMetalFootsteps[ 4 ];
+
+  char        voice[ MAX_VOICE_NAME_LEN ];
+  int         voiceTime;
 } clientInfo_t;
 
 
@@ -1037,7 +1040,6 @@ typedef struct
 
   // attacking player
   int           attackerTime;
-  int           voiceTime;
 
   // reward medals
   int           rewardStack;
@@ -1404,6 +1406,9 @@ typedef struct
 
   // media
   cgMedia_t           media;
+
+  voice_t       *voices;
+  clientList_t  ignoreList;
 } cgs_t;
 
 //==============================================================================
@@ -1540,6 +1545,8 @@ extern  vmCvar_t    cg_painBlendZoom;
 extern  vmCvar_t    cg_stickySpec;
 extern  vmCvar_t    cg_alwaysSprint;
 
+extern  vmCvar_t    cg_debugVoices;
+
 extern  vmCvar_t    ui_currentClass;
 extern  vmCvar_t    ui_carriage;
 extern  vmCvar_t    ui_stages;
@@ -1553,6 +1560,8 @@ extern  vmCvar_t    cg_debugRandom;
 extern  vmCvar_t    cg_optimizePrediction;
 extern  vmCvar_t    cg_projectileNudge;
 
+extern  vmCvar_t    cg_voice;
+
 //
 // cg_main.c
 //
diff --git a/src/cgame/cg_main.c b/src/cgame/cg_main.c
index 9dd1a953..c36c447d 100644
--- a/src/cgame/cg_main.c
+++ b/src/cgame/cg_main.c
@@ -230,6 +230,8 @@ vmCvar_t  cg_painBlendZoom;
 vmCvar_t  cg_stickySpec;
 vmCvar_t  cg_alwaysSprint;
 
+vmCvar_t  cg_debugVoices;
+
 vmCvar_t  ui_currentClass;
 vmCvar_t  ui_carriage;
 vmCvar_t  ui_stages;
@@ -243,6 +245,8 @@ vmCvar_t  cg_debugRandom;
 vmCvar_t  cg_optimizePrediction;
 vmCvar_t  cg_projectileNudge;
 
+vmCvar_t  cg_voice;
+
 
 typedef struct
 {
@@ -352,6 +356,8 @@ static cvarTable_t cvarTable[ ] =
   { &cg_painBlendMax, "cg_painBlendMax", "0.7", 0 },
   { &cg_painBlendScale, "cg_painBlendScale", "7.0", 0 },
   { &cg_painBlendZoom, "cg_painBlendZoom", "0.65", 0 },
+  
+  { &cg_debugVoices, "cg_debugVoices", "0", 0 },
 
   { &ui_currentClass, "ui_currentClass", "0", 0 },
   { &ui_carriage, "ui_carriage", "", 0 },
@@ -390,7 +396,9 @@ static cvarTable_t cvarTable[ ] =
   { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE},
   { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE},
   { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE},
-  { &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE}
+  { &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE},
+  
+  { &cg_voice, "voice", "default", CVAR_USERINFO|CVAR_ARCHIVE}
 };
 
 static int   cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );
@@ -1850,6 +1858,9 @@ void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum )
   CG_UpdateMediaFraction( 1.0f );
 
   CG_InitBuildables( );
+ 
+  cgs.voices = BG_VoiceInit( );
+  BG_PrintVoices( cgs.voices, cg_debugVoices.integer );
 
   CG_RegisterClients( );   // if low on memory, some clients will be deferred
 
diff --git a/src/cgame/cg_players.c b/src/cgame/cg_players.c
index dcbe86db..1cebf451 100644
--- a/src/cgame/cg_players.c
+++ b/src/cgame/cg_players.c
@@ -746,6 +746,13 @@ void CG_NewClientInfo( int clientNum )
 
   // the old value
   memset( &newInfo, 0, sizeof( newInfo ) );
+ 
+  // grab our own ignoreList 
+  if( clientNum == cg.predictedPlayerState.clientNum )
+  {
+    v = Info_ValueForKey( configstring, "ig" );
+    BG_ClientListParse( &cgs.ignoreList, v );
+  }
 
   // isolate the player's name
   v = Info_ValueForKey( configstring, "n" );
@@ -784,6 +791,10 @@ void CG_NewClientInfo( int clientNum )
     *slash = 0;
   }
 
+  // voice
+  v = Info_ValueForKey( configstring, "v" );
+  Q_strncpyz( newInfo.voice, v, sizeof( newInfo.voice ) );
+
   // replace whatever was there with the new one
   newInfo.infoValid = qtrue;
   *ci = newInfo;
diff --git a/src/cgame/cg_servercmds.c b/src/cgame/cg_servercmds.c
index 6c45fcb1..4742eabe 100644
--- a/src/cgame/cg_servercmds.c
+++ b/src/cgame/cg_servercmds.c
@@ -901,6 +901,193 @@ void CG_Menu( int menu, int arg )
   }
 }
 
+/*
+=================
+CG_Say
+=================
+*/
+static void CG_Say( int clientNum, char *text )
+{
+  clientInfo_t *ci;
+  char sayText[ MAX_SAY_TEXT ] = {""};
+  
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    return;
+
+  ci = &cgs.clientinfo[ clientNum ];
+  Com_sprintf( sayText, sizeof( sayText ),
+    "%s: " S_COLOR_WHITE S_COLOR_GREEN "%s" S_COLOR_WHITE "\n",
+    ci->name, text );
+  
+  CG_RemoveChatEscapeChar( sayText );
+  if( BG_ClientListTest( &cgs.ignoreList, clientNum ) )
+    CG_Printf( "[skipnotify]%s", sayText );
+  else
+    CG_Printf( "%s", sayText );
+}
+
+/*
+=================
+CG_SayTeam
+=================
+*/
+static void CG_SayTeam( int clientNum, char *text )
+{
+  clientInfo_t *ci;
+  char sayText[ MAX_SAY_TEXT ] = {""};
+  
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    return;
+  
+
+  ci = &cgs.clientinfo[ clientNum ];
+  Com_sprintf( sayText, sizeof( sayText ),
+    "%s: " S_COLOR_WHITE S_COLOR_CYAN "%s" S_COLOR_WHITE "\n",
+    ci->name, text );
+  
+  CG_RemoveChatEscapeChar( sayText );
+  if( BG_ClientListTest( &cgs.ignoreList, clientNum ) )
+    CG_Printf( "[skipnotify]%s", sayText );
+  else
+    CG_Printf( "%s", sayText );
+}
+
+/*
+=================
+CG_VoiceTrack
+
+return the voice indexed voice track or print errors quietly to console
+in case someone is on an unpure server and wants to know which voice pak
+is missing or incomplete
+=================
+*/
+static voiceTrack_t *CG_VoiceTrack( char *voice, int cmd, int track )
+{
+  voice_t *v;
+  voiceCmd_t *c;
+  voiceTrack_t *t;
+
+  v = BG_VoiceByName( cgs.voices, voice );
+  if( !v )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find voice \"%s\"\n", voice );
+    return NULL;
+  }
+  c = BG_VoiceCmdByNum( v->cmds, cmd );
+  if( !c )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find command %d "
+      "in voice \"%s\"\n", cmd, voice );
+    return NULL;
+  }
+  t = BG_VoiceTrackByNum( c->tracks, track );
+  if( !t )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find track %d for command %d in "
+      "voice \"%s\"\n", track, cmd, voice );
+    return NULL;
+  }
+  return t;
+}
+
+/*
+=================
+CG_ParseVoice
+
+voice clientNum vChan cmdNum trackNum [sayText]
+=================
+*/
+static void CG_ParseVoice( void )
+{
+  int clientNum;
+  voiceChannel_t vChan;
+  char sayText[ MAX_SAY_TEXT] = {""};
+  voiceTrack_t *track;
+  clientInfo_t *ci;
+
+  if( trap_Argc() < 5 || trap_Argc() > 6 )
+    return;
+
+  if( trap_Argc() == 6 )
+    Q_strncpyz( sayText, CG_Argv( 5 ), sizeof( sayText ) );
+
+  clientNum = atoi( CG_Argv( 1 ) );
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    return;
+
+  vChan = atoi( CG_Argv( 2 ) );
+  if( vChan < 0 || vChan >= VOICE_CHAN_NUM_CHANS )
+    return;
+
+  if( cg_teamChatsOnly.integer && vChan != VOICE_CHAN_TEAM )
+    return;
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  // this joker is still talking
+  if( ci->voiceTime > cg.time )
+    return;
+
+  track = CG_VoiceTrack( ci->voice, atoi( CG_Argv( 3 ) ), atoi( CG_Argv( 4 ) ) );
+
+  // keep track of how long the player will be speaking
+  // assume it takes 3s to say "*unintelligible gibberish*" 
+  if( track )
+    ci->voiceTime = cg.time + track->duration;
+  else
+    ci->voiceTime = cg.time + 3000;
+
+  if( !sayText[ 0 ] )
+  {
+    if( track )
+      Q_strncpyz( sayText, track->text, sizeof( sayText ) );
+    else
+      Q_strncpyz( sayText, "*unintelligible gibberish*", sizeof( sayText ) );
+  }
+ 
+  if( !cg_noVoiceText.integer ) 
+  {
+    switch( vChan )
+    {
+      case VOICE_CHAN_ALL:
+        CG_Say( clientNum, sayText );
+        break;
+      case VOICE_CHAN_TEAM:
+        CG_SayTeam( clientNum, sayText );
+        break;
+      default:
+        break;
+    }
+  }
+
+  // playing voice audio tracks disabled
+  if( cg_noVoiceChats.integer )
+    return;
+
+  // no audio track to play
+  if( !track )
+    return;
+
+  // don't play audio track for lamers
+  if( BG_ClientListTest( &cgs.ignoreList, clientNum ) )
+    return;
+
+  switch( vChan )
+  {
+    case VOICE_CHAN_ALL:
+      trap_S_StartLocalSound( track->track, CHAN_VOICE );
+      break;
+    case VOICE_CHAN_TEAM:
+      trap_S_StartLocalSound( track->track, CHAN_VOICE );
+      break;
+    case VOICE_CHAN_LOCAL:
+      trap_S_StartSound( NULL, clientNum, CHAN_VOICE, track->track );
+      break;
+    default:
+        break;
+  } 
+}
+
 /*
 =================
 CG_ServerCommand
@@ -970,6 +1157,12 @@ static void CG_ServerCommand( void )
     CG_Printf( "%s\n", text );
     return;
   }
+  
+  if( !strcmp( cmd, "voice" ) )
+  {
+    CG_ParseVoice( );
+    return; 
+  }
 
   if( !strcmp( cmd, "scores" ) )
   {
diff --git a/src/game/bg_public.h b/src/game/bg_public.h
index f9527d37..1e1c98ac 100644
--- a/src/game/bg_public.h
+++ b/src/game/bg_public.h
@@ -1225,6 +1225,7 @@ qboolean BG_WeaponIsAllowed( weapon_t weapon );
 qboolean BG_UpgradeIsAllowed( upgrade_t upgrade );
 qboolean BG_ClassIsAllowed( class_t class );
 qboolean BG_BuildableIsAllowed( buildable_t buildable );
+weapon_t BG_PrimaryWeapon( int stats[ ] );
 
 typedef struct 
 {
@@ -1242,3 +1243,58 @@ void BG_ClientListParse( clientList_t *list, const char *s );
 #define FFF_ALIENS         2
 #define FFF_BUILDABLES     4
 
+// bg_voice.c
+#define MAX_VOICES                8
+#define MAX_VOICE_NAME_LEN        16
+#define MAX_VOICE_CMD_LEN         16
+#define VOICE_ENTHUSIASM_DECAY    0.5f // enthusiasm lost per second
+
+typedef enum
+{
+  VOICE_CHAN_ALL,
+  VOICE_CHAN_TEAM ,
+  VOICE_CHAN_LOCAL,
+
+  VOICE_CHAN_NUM_CHANS
+} voiceChannel_t;
+
+typedef struct voiceTrack_s
+{
+#ifdef CGAME
+  sfxHandle_t            track;
+  int                    duration;
+#endif
+  char                   *text;
+  int                    enthusiasm;
+  int                    team;
+  int                    class;
+  int                    weapon;
+  struct voiceTrack_s    *next;
+} voiceTrack_t;
+
+
+typedef struct voiceCmd_s
+{
+  char              cmd[ MAX_VOICE_CMD_LEN ];
+  voiceTrack_t      *tracks;
+  struct voiceCmd_s *next;
+} voiceCmd_t;
+
+typedef struct voice_s
+{
+  char             name[ MAX_VOICE_NAME_LEN ];
+  voiceCmd_t       *cmds;
+  struct voice_s   *next;
+} voice_t;
+
+voice_t *BG_VoiceInit( void );
+void BG_PrintVoices( voice_t *voices, int debugLevel );
+
+voice_t *BG_VoiceByName( voice_t *head, char *name );
+voiceCmd_t *BG_VoiceCmdFind( voiceCmd_t *head, char *name, int *cmdNum );
+voiceCmd_t *BG_VoiceCmdByNum( voiceCmd_t *head, int num);
+voiceTrack_t *BG_VoiceTrackByNum( voiceTrack_t *head, int num );
+voiceTrack_t *BG_VoiceTrackFind( voiceTrack_t *head, team_t team,
+                                 class_t class, weapon_t weapon,
+                                 int enthusiasm, int *trackNum );
+
diff --git a/src/game/bg_voice.c b/src/game/bg_voice.c
new file mode 100644
index 00000000..91f65617
--- /dev/null
+++ b/src/game/bg_voice.c
@@ -0,0 +1,652 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2006 Tim Angus
+Copyright (C) 2008      Tony J. White
+
+This file is part of Tremulous.
+
+Tremulous is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 2 of the License,
+or (at your option) any later version.
+
+Tremulous is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// bg_voice.c -- both games voice functions
+#include "../qcommon/q_shared.h"
+#include "bg_public.h"
+#include "bg_local.h"
+
+int  trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode );
+int  trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize );
+int trap_Parse_LoadSource( const char *filename );
+int trap_Parse_FreeSource( int handle );
+int trap_Parse_ReadToken( int handle, pc_token_t *pc_token );
+int trap_Parse_SourceFileAndLine( int handle, char *filename, int *line );
+
+#ifdef CGAME
+sfxHandle_t trap_S_RegisterSound( const char *sample, qboolean compressed );
+int trap_S_SoundDuration( sfxHandle_t handle );
+#endif
+
+
+/*
+============
+BG_VoiceParseError
+============
+*/
+static void BG_VoiceParseError( fileHandle_t handle, char *err )
+{
+  int line;
+  char filename[ MAX_QPATH ];
+
+  trap_Parse_SourceFileAndLine( handle, filename, &line );
+  trap_Parse_FreeSource( handle );
+  Com_Error( ERR_FATAL, "%s on line %d of %s\n", err, line, filename );
+}
+
+/*
+============
+BG_VoiceList
+============
+*/
+static voice_t *BG_VoiceList( void )
+{
+  char fileList[ MAX_VOICES * ( MAX_VOICE_NAME_LEN + 8 ) ] = {""};
+  int numFiles, i, fileLen = 0;
+  int count = 0;
+  char *filePtr;
+  voice_t *voices = NULL;
+  voice_t *top = NULL;
+
+  numFiles = trap_FS_GetFileList( "voice", ".voice", fileList,
+    sizeof( fileList ) );
+
+  if( numFiles < 1 )
+    return NULL;
+
+  // special case for default.voice.  this file is REQUIRED and will
+  // always be loaded first in the event of overflow of voice definitions
+  if( !trap_FS_FOpenFile( "voice/default.voice", NULL, FS_READ ) )
+  {
+    Com_Printf( "voice/default.voice missing, voice system disabled." );
+    return NULL;
+  }
+      
+  voices = (voice_t*)BG_Alloc( sizeof( voice_t ) );
+  Q_strncpyz( voices->name, "default", sizeof( voices->name ) );
+  voices->cmds = NULL;
+  voices->next = NULL;
+  count = 1;
+
+  top = voices;
+
+  filePtr = fileList;
+  for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 )
+  {
+    fileLen = strlen( filePtr );
+
+    // accounted for above
+    if( !Q_stricmp( filePtr, "default.voice" ) )
+      continue;
+
+    if( fileLen > MAX_VOICE_NAME_LEN + 8 ) {
+      Com_Printf( S_COLOR_YELLOW "WARNING: MAX_VOICE_NAME_LEN is %d. "
+        "skipping \"%s\", filename too long", MAX_VOICE_NAME_LEN, filePtr );
+      continue;
+    }
+
+    // trap_FS_GetFileList() buffer has overflowed
+    if( !trap_FS_FOpenFile( va( "voice/%s", filePtr ), NULL, FS_READ ) )
+    {
+      Com_Printf( S_COLOR_YELLOW "WARNING: BG_VoiceList(): detected "
+        " an invalid .voice file \"%s\" in directory listing.  You have"
+        "probably named one or more .voice files with outrageously long "
+        "names.  gjbs", filePtr );
+      break;
+    }
+
+    if( count >= MAX_VOICES )
+    {
+      Com_Printf( S_COLOR_YELLOW "WARNING: .voice file overflow.  "
+        "%d of %d .voice files loaded.  MAX_VOICES is %d",
+        count, numFiles, MAX_VOICES );
+      break;
+    }
+ 
+    voices->next = (voice_t*)BG_Alloc( sizeof( voice_t ) );
+    voices = voices->next;
+
+    Q_strncpyz( voices->name, filePtr, sizeof( voices->name ) );
+    // strip extension 
+    voices->name[ fileLen - 6 ] = '\0';
+    voices->cmds = NULL;
+    voices->next = NULL;
+    count++;
+  }
+  return top;
+}
+
+/*
+============
+BG_VoiceParseTrack
+============
+*/
+static qboolean BG_VoiceParseTrack( int handle, voiceTrack_t *voiceTrack )
+{
+  pc_token_t token;
+  qboolean found = qfalse;
+  qboolean foundText = qfalse;
+  qboolean foundToken = qfalse;
+
+  foundToken = trap_Parse_ReadToken( handle, &token );
+  while( foundToken )
+  {
+    if( token.string[ 0 ] == '}' )
+    {
+      if( foundText )
+        return qtrue;
+      else
+      {
+        BG_VoiceParseError( handle, "BG_VoiceParseTrack(): "
+          "missing text attribute for track" );
+      }
+    }
+    else if( !Q_stricmp( token.string, "team" ) )
+    {
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      found = qfalse;
+      while( foundToken && token.type == TT_NUMBER )
+      {
+        found = qtrue;
+        if( voiceTrack->team < 0 )
+          voiceTrack->team = 0;
+        voiceTrack->team |= ( 1 << token.intvalue );
+        foundToken = trap_Parse_ReadToken( handle, &token );
+      }
+      if( !found )
+      {
+        BG_VoiceParseError( handle,
+          "BG_VoiceParseTrack(): missing \"team\" value" );
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "class" ) )
+    {
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      found = qfalse;
+      while( foundToken && token.type == TT_NUMBER )
+      {
+        found = qtrue;
+        if( voiceTrack->class < 0 )
+          voiceTrack->class = 0;
+        voiceTrack->class |= ( 1 << token.intvalue );
+        foundToken = trap_Parse_ReadToken( handle, &token );
+      }
+      if( !found )
+      {
+        BG_VoiceParseError( handle, 
+          "BG_VoiceParseTrack(): missing \"class\" value" );
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "weapon" ) )
+    {
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      found = qfalse;
+      while( foundToken && token.type == TT_NUMBER )
+      {
+        found = qtrue;
+        if( voiceTrack->weapon < 0 )
+          voiceTrack->weapon = 0;
+        voiceTrack->weapon |= ( 1 << token.intvalue );
+        foundToken = trap_Parse_ReadToken( handle, &token );
+      }
+      if( !found )
+      {
+        BG_VoiceParseError( handle,
+          "BG_VoiceParseTrack(): missing \"weapon\" value");
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "text" ) )
+    {
+      if( foundText )
+      {
+        BG_VoiceParseError( handle, "BG_VoiceParseTrack(): "
+          "duplicate \"text\" definition for track" );
+      }
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      if( !foundToken )
+      {
+        BG_VoiceParseError( handle, "BG_VoiceParseTrack(): "
+          "missing \"text\" value" );
+      }
+      foundText = qtrue;
+      if( strlen( token.string ) >= MAX_SAY_TEXT )
+      {
+        BG_VoiceParseError( handle, va( "BG_VoiceParseTrack(): "
+          "\"text\" value " "\"%s\" exceeds MAX_SAY_TEXT length",
+          token.string ) );
+      }
+
+      voiceTrack->text = (char *)BG_Alloc( strlen( token.string ) + 1 );
+      Q_strncpyz( voiceTrack->text, token.string, strlen( token.string ) + 1 );
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "enthusiasm" ) )
+    {
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      if( token.type == TT_NUMBER )
+      {
+        voiceTrack->enthusiasm = token.intvalue;
+      }
+      else
+      {
+        BG_VoiceParseError( handle, "BG_VoiceParseTrack(): "
+          "missing \"enthusiasm\" value" );
+      }
+      foundToken = trap_Parse_ReadToken( handle, &token );
+      continue;
+    }
+    else
+    {
+        BG_VoiceParseError( handle, va( "BG_VoiceParseTrack():"
+          " unknown token \"%s\"", token.string ) );
+    }
+  }
+  return qfalse;
+}
+
+/*
+============
+BG_VoiceParseCommand
+============
+*/
+static voiceTrack_t *BG_VoiceParseCommand( int handle )
+{
+  pc_token_t token;
+  qboolean parsingTrack = qfalse;
+  voiceTrack_t *voiceTracks = NULL;
+  voiceTrack_t *top = NULL;
+  
+  while( trap_Parse_ReadToken( handle, &token ) )
+  {
+    if( !parsingTrack && token.string[ 0 ] == '}' )
+      return top;
+
+    if( parsingTrack )
+    {
+      if( token.string[ 0 ] == '{' )
+      {
+        BG_VoiceParseTrack( handle, voiceTracks );
+        parsingTrack = qfalse;
+        continue;
+        
+      }
+      else
+      {
+        BG_VoiceParseError( handle, va( "BG_VoiceParseCommand(): "
+          "parse error at \"%s\"", token.string ) );
+      }
+    }
+
+
+    if( top == NULL )
+    {
+      voiceTracks = BG_Alloc( sizeof( voiceTrack_t ) );
+      top = voiceTracks;
+    }
+    else
+    {
+      voiceTracks->next = BG_Alloc( sizeof( voiceCmd_t ) );
+      voiceTracks = voiceTracks->next;
+    }
+    
+    if( !trap_FS_FOpenFile( va( "%s", token.string ), NULL, FS_READ ) )
+    {
+        int line;
+        char filename[ MAX_QPATH ];
+
+        trap_Parse_SourceFileAndLine( handle, filename, &line );
+        Com_Printf( S_COLOR_YELLOW "WARNING: BG_VoiceParseCommand(): "
+          "track \"%s\" referenced on line %d of %s does not exist\n",
+          token.string, line, filename );
+    }
+    else
+    {
+#ifdef CGAME
+      voiceTracks->track = trap_S_RegisterSound( token.string, qfalse );
+      voiceTracks->duration = trap_S_SoundDuration( voiceTracks->track );
+#endif
+    }
+
+    voiceTracks->team = -1;
+    voiceTracks->class = -1;
+    voiceTracks->weapon = -1;
+    voiceTracks->enthusiasm = 0;
+    voiceTracks->text = NULL;
+    voiceTracks->next = NULL;
+    parsingTrack = qtrue;
+
+  }
+  return NULL;
+}
+
+/*
+============
+BG_VoiceParse
+============
+*/
+static voiceCmd_t *BG_VoiceParse( char *name )
+{
+  voiceCmd_t *voiceCmds = NULL;
+  voiceCmd_t *top = NULL;
+  pc_token_t token;
+  qboolean parsingCmd = qfalse;
+  int handle;
+  
+  handle = trap_Parse_LoadSource( va( "voice/%s.voice", name ) );
+  if( !handle )
+    return NULL;
+
+  while( trap_Parse_ReadToken( handle, &token ) )
+  {
+    if( parsingCmd )
+    {
+      if( token.string[ 0 ] == '{' )
+      {
+        voiceCmds->tracks = BG_VoiceParseCommand( handle );
+        parsingCmd = qfalse;
+	continue;
+      }
+      else
+      {
+        int line;
+        char filename[ MAX_QPATH ];
+
+        trap_Parse_SourceFileAndLine( handle, filename, &line );
+        Com_Error( ERR_FATAL, "BG_VoiceParse(): "
+          "parse error on line %d of %s\n", line, filename );
+      }
+    }
+      
+    if( strlen( token.string ) >= MAX_VOICE_CMD_LEN )
+    {
+        int line;
+        char filename[ MAX_QPATH ];
+
+        trap_Parse_SourceFileAndLine( handle, filename, &line );
+        Com_Error( ERR_FATAL, "BG_VoiceParse(): "
+          "command \"%s\" exceeds MAX_VOICE_CMD_LEN (%d) on line %d of %s\n",
+          token.string, MAX_VOICE_CMD_LEN, line, filename );
+    }
+   
+    if( top == NULL )
+    {
+      voiceCmds = BG_Alloc( sizeof( voiceCmd_t ) );
+      top = voiceCmds;
+    }
+    else
+    {
+      voiceCmds->next = BG_Alloc( sizeof( voiceCmd_t ) );
+      voiceCmds = voiceCmds->next;
+    }
+
+    Q_strncpyz( voiceCmds->cmd, token.string, sizeof( voiceCmds->cmd ) );
+    voiceCmds->next = NULL;
+    parsingCmd = qtrue;
+
+  } 
+
+  trap_Parse_FreeSource( handle );
+  
+  return top;
+}
+
+/*
+============
+BG_VoiceInit
+============
+*/
+voice_t *BG_VoiceInit( void )
+{
+  voice_t *voices;
+  voice_t *voice;
+
+  voices = BG_VoiceList();
+
+  voice = voices;
+  while( voice )
+  {
+    voice->cmds = BG_VoiceParse( voice->name );
+    voice = voice->next;
+  }
+
+  return voices;
+}
+
+
+/*
+============
+BG_PrintVoices
+============
+*/
+void BG_PrintVoices( voice_t *voices, int debugLevel )
+{
+  voice_t *voice = voices;
+  voiceCmd_t *voiceCmd;
+  voiceTrack_t *voiceTrack;
+  
+  int cmdCount;
+  int trackCount;
+
+  if( voice == NULL )
+  {
+    Com_Printf( "voice list is empty\n" );
+    return;
+  }
+
+  while( voice != NULL )
+  {
+    if( debugLevel > 0 )
+      Com_Printf( "voice \"%s\"\n", voice->name );
+    voiceCmd = voice->cmds;
+    cmdCount = 0;
+    trackCount = 0;
+    while( voiceCmd != NULL )
+    {
+      if( debugLevel > 0 )
+        Com_Printf( "  %s\n", voiceCmd->cmd );
+      voiceTrack = voiceCmd->tracks;
+      cmdCount++;
+      while ( voiceTrack != NULL )
+      {
+        if( debugLevel > 1 )
+          Com_Printf( "    text -> %s\n", voiceTrack->text );
+        if( debugLevel > 2 )
+        {
+          Com_Printf( "    team -> %d\n", voiceTrack->team );
+          Com_Printf( "    class -> %d\n", voiceTrack->class );
+          Com_Printf( "    weapon -> %d\n", voiceTrack->weapon );
+          Com_Printf( "    enthusiasm -> %d\n", voiceTrack->enthusiasm );
+#ifdef CGAME
+          Com_Printf( "    duration -> %d\n", voiceTrack->duration );
+#endif
+        }
+        if( debugLevel > 1 )
+          Com_Printf( "\n" );
+        trackCount++; 
+        voiceTrack = voiceTrack->next;
+      }
+      voiceCmd = voiceCmd->next;
+    }
+
+    if( !debugLevel )
+    {
+      Com_Printf( "voice \"%s\": %d commands, %d tracks\n",
+        voice->name, cmdCount, trackCount );
+    }
+    voice = voice->next;
+  }
+}
+
+/*
+============
+BG_VoiceByName
+============
+*/
+voice_t *BG_VoiceByName( voice_t *head, char *name )
+{
+  voice_t *v = head;
+
+  while( v )
+  {
+    if( !Q_stricmp( v->name, name ) )
+      return v;
+    v = v->next;
+  }
+  return NULL;
+}
+
+/*
+============
+BG_VoiceCmdFind
+============
+*/
+voiceCmd_t *BG_VoiceCmdFind( voiceCmd_t *head, char *name, int *cmdNum )
+{
+  voiceCmd_t *vc = head;
+  int i = 0;
+
+  while( vc )
+  {
+    i++;
+    if( !Q_stricmp( vc->cmd, name ) )
+    {
+      *cmdNum = i;
+      return vc;
+    }
+    vc = vc->next;
+  }
+  return NULL;
+}
+
+/*
+============
+BG_VoiceCmdByNum
+============
+*/
+voiceCmd_t *BG_VoiceCmdByNum( voiceCmd_t *head, int num )
+{
+  voiceCmd_t *vc = head;
+  int i = 0;
+
+  while( vc )
+  {
+    i++;
+    if( i == num )
+      return vc;
+    vc = vc->next;
+  }
+  return NULL;
+}
+
+/*
+============
+BG_VoiceTrackByNum
+============
+*/
+voiceTrack_t *BG_VoiceTrackByNum( voiceTrack_t *head, int num )
+{
+  voiceTrack_t *vt = head;
+  int i = 0;
+
+  while( vt )
+  {
+    i++;
+    if( i == num )
+      return vt;
+    vt = vt->next;
+  }
+  return NULL;
+}
+
+/*
+============
+BG_VoiceTrackFind
+============
+*/
+voiceTrack_t *BG_VoiceTrackFind( voiceTrack_t *head, team_t team,
+                                 class_t class, weapon_t weapon,
+                                 int enthusiasm, int *trackNum )
+{
+  voiceTrack_t *vt = head;
+  int highestMatch = 0;
+  int matchCount = 0;
+  int selectedMatch = 0;
+  int i = 0;
+  int j = 0;
+  
+  // find highest enthusiasm without going over
+  while( vt )
+  {
+    if( ( vt->team >= 0 && !( vt->team  & ( 1 << team ) ) ) || 
+        ( vt->class >= 0 && !( vt->class & ( 1 << class ) ) ) || 
+        ( vt->weapon >= 0 && !( vt->weapon & ( 1 << weapon ) ) ) ||
+        vt->enthusiasm > enthusiasm )
+    {
+      vt = vt->next;
+      continue;
+    }
+
+    if( vt->enthusiasm > highestMatch )
+    {
+      matchCount = 0;
+      highestMatch = vt->enthusiasm; 
+    }
+    if( vt->enthusiasm == highestMatch )
+      matchCount++;
+    vt = vt->next;
+  }
+
+  if( !matchCount )
+    return NULL;
+
+  // return randomly selected match
+  selectedMatch = rand() % matchCount;
+  vt = head;
+  i = 0;
+  j = 0;
+  while( vt )
+  {
+    j++;
+    if( ( vt->team >= 0 && !( vt->team  & ( 1 << team ) ) ) || 
+        ( vt->class >= 0 && !( vt->class & ( 1 << class ) ) ) || 
+        ( vt->weapon >= 0 && !( vt->weapon & ( 1 << weapon ) ) ) ||
+        vt->enthusiasm != highestMatch )
+    {
+      vt = vt->next;
+      continue;
+    }
+    if( i == selectedMatch )
+    {
+      *trackNum = j;
+      return vt; 
+    }
+    i++;
+    vt = vt->next;
+  }
+  return NULL;
+}
diff --git a/src/game/g_active.c b/src/game/g_active.c
index 7b03ce0f..cbc9aa5c 100644
--- a/src/game/g_active.c
+++ b/src/game/g_active.c
@@ -812,6 +812,12 @@ void ClientTimerActions( gentity_t *ent, int msec )
     {
       G_Damage( ent, NULL, NULL, NULL, NULL, 5, DAMAGE_NO_ARMOR, MOD_SUICIDE );
     }
+
+    // lose some voice enthusiasm
+    if( client->voiceEnthusiasm > 0.0f )
+      client->voiceEnthusiasm -= VOICE_ENTHUSIASM_DECAY;
+    else
+      client->voiceEnthusiasm = 0.0f;
   }
 
   // Regenerate Adv. Dragoon barbs
diff --git a/src/game/g_client.c b/src/game/g_client.c
index 01f5dbb2..578fc176 100644
--- a/src/game/g_client.c
+++ b/src/game/g_client.c
@@ -1091,14 +1091,18 @@ void ClientUserinfoChanged( int clientNum )
 
   team = client->pers.teamSelection;
 
+  Q_strncpyz( client->pers.voice, Info_ValueForKey( userinfo, "voice" ),
+    sizeof( client->pers.voice ) );
+
   // send over a subset of the userinfo keys so other clients can
   // print scoreboards, display models, and play custom sounds
 
   Com_sprintf( userinfo, sizeof( userinfo ),
     "n\\%s\\t\\%i\\model\\%s\\c1\\%s\\c2\\%s\\"
-    "hc\\%i\\ig\\%16s",
+    "hc\\%i\\ig\\%16s\\v\\%s",
     client->pers.netname, team, model, c1, c2,
-    client->pers.maxHealth, BG_ClientListString( &client->sess.ignoreList ) );
+    client->pers.maxHealth, BG_ClientListString( &client->sess.ignoreList ),
+    client->pers.voice );
 
   trap_SetConfigstring( CS_PLAYERS + clientNum, userinfo );
 
diff --git a/src/game/g_cmds.c b/src/game/g_cmds.c
index 88baf39c..13205415 100644
--- a/src/game/g_cmds.c
+++ b/src/game/g_cmds.c
@@ -929,6 +929,126 @@ static void Cmd_Tell_f( gentity_t *ent )
     G_Say( ent, ent, SAY_TELL, p );
 }
 
+/*
+==================
+Cmd_VSay_f
+==================
+*/
+void Cmd_VSay_f( gentity_t *ent )
+{
+  char            arg[MAX_TOKEN_CHARS];
+  voiceChannel_t  vchan;
+  voice_t         *voice;
+  voiceCmd_t      *cmd;
+  voiceTrack_t    *track;
+  int             cmdNum = 0;
+  int             trackNum = 0;
+  char            voiceName[ MAX_VOICE_NAME_LEN ] = {"default"};
+  char            voiceCmd[ MAX_VOICE_CMD_LEN ] = {""};
+  char            vsay[ 12 ] = {""};
+  weapon_t        weapon;
+
+  if( !ent || !ent->client )
+    Com_Error( ERR_FATAL, "Cmd_VSay_f() called by non-client entity\n" );
+
+  trap_Argv( 0, arg, sizeof( arg ) );
+  if( trap_Argc( ) < 2 )
+  {
+    trap_SendServerCommand( ent-g_entities, va( 
+      "print \"usage: %s command [text] \n\"", arg ) );
+    return;
+  }
+  if( !level.voices )
+  {
+    trap_SendServerCommand( ent-g_entities, va(
+      "print \"%s: voice system is not installed on this server\n\"", arg ) );
+    return;
+  }
+  if( !g_voiceChats.integer )
+  {
+    trap_SendServerCommand( ent-g_entities, va( 
+      "print \"%s: voice system administratively disabled on this server\n\"",
+      arg ) );
+    return;
+  }
+  if( !Q_stricmp( arg, "vsay" ) )
+    vchan = VOICE_CHAN_ALL;
+  else if( !Q_stricmp( arg, "vsay_team" ) )
+    vchan = VOICE_CHAN_TEAM;
+  else if( !Q_stricmp( arg, "vsay_local" ) )
+    vchan = VOICE_CHAN_LOCAL;
+  else
+    return;
+  Q_strncpyz( vsay, arg, sizeof( vsay ) );
+ 
+  if( ent->client->pers.voice[ 0 ] )
+    Q_strncpyz( voiceName, ent->client->pers.voice, sizeof( voiceName ) );
+  voice = BG_VoiceByName( level.voices, voiceName );
+  if( !voice )
+  {
+    trap_SendServerCommand( ent-g_entities, va( 
+      "print \"%s: voice '%s' not found\n\"", vsay, voiceName ) );
+    return;
+  }
+  
+  trap_Argv( 1, voiceCmd, sizeof( voiceCmd ) ) ;
+  cmd = BG_VoiceCmdFind( voice->cmds, voiceCmd, &cmdNum );
+  if( !cmd )
+  {
+    trap_SendServerCommand( ent-g_entities, va( 
+     "print \"%s: command '%s' not found in voice '%s'\n\"",
+      vsay, voiceCmd, voiceName ) );
+    return;
+  }
+
+  // filter non-spec humans by their primary weapon as well
+  weapon = WP_NONE;
+  if( ent->client->sess.spectatorState == SPECTATOR_NOT )
+  {
+    weapon = BG_PrimaryWeapon( ent->client->ps.stats );
+  }
+
+  track = BG_VoiceTrackFind( cmd->tracks, ent->client->pers.teamSelection,
+    ent->client->pers.classSelection, weapon, (int)ent->client->voiceEnthusiasm,
+    &trackNum );
+  if( !track )
+  {
+    trap_SendServerCommand( ent-g_entities, va( 
+      "print \"%s: no available track for command '%s', team %d, "
+      "class %d, weapon %d, and enthusiasm %d in voice '%s'\n\"",
+      vsay, voiceCmd, ent->client->pers.teamSelection,
+      ent->client->pers.classSelection, weapon,
+      (int)ent->client->voiceEnthusiasm, voiceName ) );
+    return; 
+  }
+
+  if( !Q_stricmp( ent->client->lastVoiceCmd, cmd->cmd ) )
+    ent->client->voiceEnthusiasm++;
+
+  Q_strncpyz( ent->client->lastVoiceCmd, cmd->cmd,
+    sizeof( ent->client->lastVoiceCmd ) ); 
+
+  // optional user supplied text
+  trap_Argv( 2, arg, sizeof( arg ) );
+
+  switch( vchan )
+  {
+    case VOICE_CHAN_ALL:
+    case VOICE_CHAN_LOCAL:
+      trap_SendServerCommand( -1, va(
+        "voice %d %d %d %d \"%s\"\n",
+        ent-g_entities, vchan, cmdNum, trackNum, arg ) );
+      break;
+    case VOICE_CHAN_TEAM:
+      G_TeamCommand( ent->client->pers.teamSelection, va(
+        "voice %d %d %d %d \"%s\"\n",
+        ent-g_entities, vchan, cmdNum, trackNum, arg ) );
+      break;
+    default:
+      break;
+  } 
+}
+
 /*
 ==================
 Cmd_Where_f
@@ -2890,6 +3010,9 @@ commands_t cmds[ ] = {
   // can be used even during intermission
   { "say", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f },
   { "say_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f },
+  { "vsay", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
+  { "vsay_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
+  { "vsay_local", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f },
   { "m", CMD_MESSAGE|CMD_INTERMISSION, G_PrivateMessage },
   { "mt", CMD_MESSAGE|CMD_INTERMISSION, G_PrivateMessage },
 
diff --git a/src/game/g_local.h b/src/game/g_local.h
index d4e5b61a..8f5a19a9 100644
--- a/src/game/g_local.h
+++ b/src/game/g_local.h
@@ -356,6 +356,7 @@ typedef struct
   qboolean            muted;
   qboolean            denyBuild;
   int                 adminLevel;
+  char                voice[ MAX_VOICE_NAME_LEN ];
 } clientPersistant_t;
 
 #define MAX_UNLAGGED_MARKERS 10
@@ -452,6 +453,9 @@ struct gclient_s
   unlagged_t          unlaggedBackup;
   unlagged_t          unlaggedCalc;
   int                 unlaggedTime;
+ 
+  float               voiceEnthusiasm;
+  char                lastVoiceCmd[ MAX_VOICE_CMD_LEN ];
 
   int                 lcannonStartTime;
 
@@ -638,6 +642,8 @@ typedef struct
   char              layout[ MAX_QPATH ];
 
   team_t            surrenderTeam;
+
+  voice_t           *voices;
 } level_locals_t;
 
 #define CMD_CHEAT         0x01
@@ -1151,6 +1157,9 @@ extern  vmCvar_t  g_currentMap;
 extern  vmCvar_t  g_initialMapRotation;
 extern  vmCvar_t  g_chatTeamPrefix;
 
+extern  vmCvar_t  g_debugVoices;
+extern  vmCvar_t  g_voiceChats;
+
 extern  vmCvar_t  g_shove;
 
 extern  vmCvar_t  g_mapConfigs;
diff --git a/src/game/g_main.c b/src/game/g_main.c
index 6643b52e..eda9a91a 100644
--- a/src/game/g_main.c
+++ b/src/game/g_main.c
@@ -113,6 +113,9 @@ vmCvar_t  g_currentMapRotation;
 vmCvar_t  g_currentMap;
 vmCvar_t  g_initialMapRotation;
 
+vmCvar_t  g_debugVoices;
+vmCvar_t  g_voiceChats;
+
 vmCvar_t  g_shove;
 
 vmCvar_t  g_mapConfigs;
@@ -233,6 +236,8 @@ static cvarTable_t   gameCvarTable[ ] =
   { &g_currentMapRotation, "g_currentMapRotation", "-1", 0, 0, qfalse  }, // -1 = NOT_ROTATING
   { &g_currentMap, "g_currentMap", "0", 0, 0, qfalse  },
   { &g_initialMapRotation, "g_initialMapRotation", "", CVAR_ARCHIVE, 0, qfalse  },
+  { &g_debugVoices, "g_debugVoices", "0", 0, 0, qfalse  },
+  { &g_voiceChats, "g_voiceChats", "1", CVAR_ARCHIVE, 0, qfalse },
   { &g_shove, "g_shove", "0.0", CVAR_ARCHIVE, 0, qfalse  },
   { &g_mapConfigs, "g_mapConfigs", "", CVAR_ARCHIVE, 0, qfalse  },
   { NULL, "g_mapConfigsLoaded", "0", CVAR_ROM, 0, qfalse  },
@@ -625,6 +630,9 @@ void G_InitGame( int levelTime, int randomSeed, int restart )
   if( g_debugMapRotation.integer )
     G_PrintRotations( );
 
+  level.voices = BG_VoiceInit( );
+  BG_PrintVoices( level.voices, g_debugVoices.integer );
+
   //reset stages
   trap_Cvar_Set( "g_alienStage", va( "%d", S1 ) );
   trap_Cvar_Set( "g_humanStage", va( "%d", S1 ) );
-- 
cgit