diff options
author | Tony J. White <tjw@tjw.org> | 2009-10-03 11:44:02 +0000 |
---|---|---|
committer | Tim Angus <tim@ngus.net> | 2013-01-03 00:15:17 +0000 |
commit | 122de31ac2834809fb15088eda6f5278d4525fb1 (patch) | |
tree | dcb4c460a2cae6df5195bd4eb21c32a7ee309ba1 /src | |
parent | 82ef46c4c099fbfed24fa326ee6f501ad5535a30 (diff) |
* voice chat stuff (no, not like TeamSpeak)
Diffstat (limited to 'src')
-rw-r--r-- | src/cgame/cg_local.h | 11 | ||||
-rw-r--r-- | src/cgame/cg_main.c | 13 | ||||
-rw-r--r-- | src/cgame/cg_players.c | 11 | ||||
-rw-r--r-- | src/cgame/cg_servercmds.c | 193 | ||||
-rw-r--r-- | src/game/bg_public.h | 56 | ||||
-rw-r--r-- | src/game/bg_voice.c | 652 | ||||
-rw-r--r-- | src/game/g_active.c | 6 | ||||
-rw-r--r-- | src/game/g_client.c | 8 | ||||
-rw-r--r-- | src/game/g_cmds.c | 123 | ||||
-rw-r--r-- | src/game/g_local.h | 9 | ||||
-rw-r--r-- | src/game/g_main.c | 8 |
11 files changed, 1086 insertions, 4 deletions
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 ) ); |