summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTony J. White <tjw@tjw.org>2009-10-03 11:44:02 +0000
committerTim Angus <tim@ngus.net>2013-01-03 00:15:17 +0000
commit122de31ac2834809fb15088eda6f5278d4525fb1 (patch)
treedcb4c460a2cae6df5195bd4eb21c32a7ee309ba1
parent82ef46c4c099fbfed24fa326ee6f501ad5535a30 (diff)
* voice chat stuff (no, not like TeamSpeak)
-rw-r--r--Makefile2
-rw-r--r--src/cgame/cg_local.h11
-rw-r--r--src/cgame/cg_main.c13
-rw-r--r--src/cgame/cg_players.c11
-rw-r--r--src/cgame/cg_servercmds.c193
-rw-r--r--src/game/bg_public.h56
-rw-r--r--src/game/bg_voice.c652
-rw-r--r--src/game/g_active.c6
-rw-r--r--src/game/g_client.c8
-rw-r--r--src/game/g_cmds.c123
-rw-r--r--src/game/g_local.h9
-rw-r--r--src/game/g_main.c8
12 files changed, 1088 insertions, 4 deletions
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
@@ -903,6 +903,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
The string has been tokenized and can be retrieved with
@@ -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
@@ -931,6 +931,126 @@ static void Cmd_Tell_f( gentity_t *ent )
/*
==================
+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 ) );