//========= Copyright Valve Corporation, All rights reserved. ============// // // Purpose: Voice / Sentence streaming & parsing code // // $Workfile: $ // $Date: $ // $NoKeywords: $ //=============================================================================// //=============================================================================== // VOX. Algorithms to load and play spoken text sentences from a file: // // In ambient sounds or entity sounds, precache the // name of the sentence instead of the wave name, ie: !C1A2S4 // // During sound system init, the 'sentences.txt' is read. // This file has the format: // // C1A2S4 agrunt/vox/You will be exterminated, surrender NOW. // C1A2s5 hgrunt/vox/Radio check, over. // ... // // There must be at least one space between the sentence name and the sentence. // Sentences may be separated by one or more lines // There may be tabs or spaces preceding the sentence name // The sentence must end in a /n or /r // Lines beginning with // are ignored as comments // // Period or comma will insert a pause in the wave unless // the period or comma is the last character in the string. // // If first 2 chars of a word are upper case, word volume increased by 25% // // If last char of a word is a number from 0 to 9 // then word will be pitch-shifted up by 0 to 9, where 0 is a small shift // and 9 is a very high pitch shift. // // We alloc heap space to contain this data, and track total // sentences read. A pointer to each sentence is maintained in g_Sentences. // // When sound is played back in S_StartDynamicSound or s_startstaticsound, we detect the !name // format and lookup the actual sentence in the sentences array // // To play, we parse each word in the sentence, chain the words, and play the sentence // each word's data is loaded directy from disk and freed right after playback. //=============================================================================== #include "audio_pch.h" #include "vox_private.h" #include "characterset.h" #include "vstdlib/random.h" #include "engine/IEngineSound.h" #include "utlsymbol.h" #include "utldict.h" #include "../../MapReslistGenerator.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" // In other C files. // Globals extern IFileSystem *g_pFileSystem; // This is the initial capacity for sentences, the array will grow if necessary #define MAX_EXPECTED_SENTENCES 900 CUtlVector g_Sentences; // FIXME: could get this through common includes const char *COM_Parse (const char *data); extern char com_token[1024]; // Module Locals static char *rgpparseword[CVOXWORDMAX]; // array of pointers to parsed words static char voxperiod[] = "_period"; // vocal pause static char voxcomma[] = "_comma"; // vocal pause #define CVOXMAPNAMESMAX 24 static char *g_rgmapnames[CVOXMAPNAMESMAX]; static int g_cmapnames = 0; // Sentence file list management static void VOX_ListClear( void ); static int VOX_ListFileIsLoaded( const char *psentenceFileName ); static void VOX_ListMarkFileLoaded( const char *psentenceFileName ); static void VOX_InitAllEntnames( void ); void VOX_LookupMapnames( void ); static void VOX_Reload() { VOX_Shutdown(); VOX_Init(); } static ConCommand vox_reload( "vox_reload", VOX_Reload, "Reload sentences.txt file", FCVAR_CHEAT ); static CUtlVector g_GroupLRU; static CUtlVector g_SentenceFile; struct sentencegroup_t { short count; public: short lru; const char *GroupName() const; CUtlSymbol GroupNameSymbol() const; void SetGroupName( const char *pName ); static CUtlSymbol GetSymbol( const char *pName ); private: CUtlSymbol groupname; static CUtlSymbolTable s_SymbolTable; }; const char *sentencegroup_t::GroupName() const { return s_SymbolTable.String( groupname ); } void sentencegroup_t::SetGroupName( const char *pName ) { groupname = s_SymbolTable.AddString( pName ); } CUtlSymbol sentencegroup_t::GroupNameSymbol() const { return groupname; } CUtlSymbol sentencegroup_t::GetSymbol( const char *pName ) { return s_SymbolTable.AddString( pName ); } CUtlVector g_SentenceGroups; CUtlSymbolTable sentencegroup_t::s_SymbolTable( 0, 256, true ); struct WordBuf { WordBuf() { word[ 0 ] = 0; } WordBuf( const WordBuf& src ) { Q_strncpy( word, src.word, sizeof( word ) ); } void Set( char const *w ) { if ( !w ) { word[ 0 ] = 0; return; } Q_strncpy( word, w, sizeof( word ) ); while ( Q_strlen( word ) >= 1 && word[ Q_strlen( word ) - 1 ] == ' ' ) { word[ Q_strlen( word ) - 1 ] = 0; } } char word[ 256 ]; }; struct ccpair { WordBuf token; WordBuf value; WordBuf fullpath; }; static void VOX_BuildVirtualNameList( char *word, CUtlVector< WordBuf >& list ); // This module depends on these engine calls: // DevMsg // S_FreeChannel // S_LoadSound // S_FindName // It also depends on vstdlib/RandomInt (all other random calls go through g_pSoundServices) void VOX_Init( void ) { VOX_InitAllEntnames(); g_SentenceFile.Purge(); g_GroupLRU.Purge(); g_Sentences.RemoveAll(); g_Sentences.EnsureCapacity( MAX_EXPECTED_SENTENCES ); VOX_ListClear(); VOX_ReadSentenceFile( "scripts/sentences.txt" ); VOX_LookupMapnames(); } void VOX_Shutdown( void ) { g_Sentences.RemoveAll(); VOX_ListClear(); g_SentenceGroups.RemoveAll(); g_cmapnames = 0; } //----------------------------------------------------------------------------- // Purpose: This is kind of like strchr(), but we get the actual pointer to the // end of the string when it fails rather than NULL. This is useful // for parsing buffers containing multiple strings // Input : *string - // scan - // Output : char //----------------------------------------------------------------------------- char *ScanForwardUntil( char *string, char scan ) { while( string[0] ) { if ( string[0] == scan ) return string; string++; } return string; } // parse a null terminated string of text into component words, with // pointers to each word stored in rgpparseword // note: this code actually alters the passed in string! char **VOX_ParseString(char *psz) { int i; int fdone = 0; char *pszscan = psz; char c; characterset_t nextWord, skip; memset(rgpparseword, 0, sizeof(char *) * CVOXWORDMAX); if (!psz) return NULL; i = 0; rgpparseword[i++] = psz; CharacterSetBuild( &nextWord, " ,.({" ); CharacterSetBuild( &skip, "., " ); while (!fdone && i < CVOXWORDMAX) { // scan up to next word c = *pszscan; while (c && !IN_CHARACTERSET(nextWord,c) ) c = *(++pszscan); // if '(' then scan for matching ')' if ( c == '(' || c=='{' ) { if ( c == '(' ) pszscan = ScanForwardUntil( pszscan, ')' ); else if ( c == '{' ) pszscan = ScanForwardUntil( pszscan, '}' ); c = *(++pszscan); if (!c) fdone = 1; } if (fdone || !c) fdone = 1; else { // if . or , insert pause into rgpparseword, // unless this is the last character if ((c == '.' || c == ',') && *(pszscan+1) != '\n' && *(pszscan+1) != '\r' && *(pszscan+1) != 0) { if (c == '.') rgpparseword[i++] = voxperiod; else rgpparseword[i++] = voxcomma; if (i >= CVOXWORDMAX) break; } // null terminate substring *pszscan++ = 0; // skip whitespace c = *pszscan; while (c && IN_CHARACTERSET(skip, c)) c = *(++pszscan); if (!c) fdone = 1; else rgpparseword[i++] = pszscan; } } return rgpparseword; } // backwards scan psz for last '/' // return substring in szpath null terminated // if '/' not found, return 'vox/' char *VOX_GetDirectory(char *szpath, int maxpath, char *psz) { char c; int cb = 0; char *pszscan = psz + Q_strlen( psz ) - 1; // scan backwards until first '/' or start of string c = *pszscan; while (pszscan > psz && c != '/') { c = *(--pszscan); cb++; } if (c != '/') { // didn't find '/', return default directory Q_strncpy(szpath, "vox/", maxpath ); return psz; } cb = Q_strlen(psz) - cb; cb = clamp( cb, 0, maxpath - 1 ); // FIXME: Is this safe? Q_memcpy(szpath, psz, cb); szpath[cb] = 0; return pszscan + 1; } // get channel volume scale if word #ifndef SWDS float VOX_GetChanVol(channel_t *ch) { if ( !ch->pMixer ) return 1.0; return ch->pMixer->GetVolumeScale(); /* if ( scale == 1.0 ) return; ch->rightvol = (int) (ch->rightvol * scale); ch->leftvol = (int) (ch->leftvol * scale); if ( g_AudioDevice->Should3DMix() ) { ch->rrightvol = (int) (ch->rrightvol * scale); ch->rleftvol = (int) (ch->rleftvol * scale); ch->centervol = (int) (ch->centervol * scale); } else { ch->rrightvol = 0; ch->rleftvol = 0; ch->centervol = 0; } */ } #endif //=============================================================================== // Get any pitch, volume, start, end params into voxword // and null out trailing format characters // Format: // someword(v100 p110 s10 e20) // // v is volume, 0% to n% // p is pitch shift up 0% to n% // s is start wave offset % // e is end wave offset % // t is timecompression % // // pass fFirst == 1 if this is the first string in sentence // returns 1 if valid string, 0 if parameter block only. // // If a ( xxx ) parameter block does not directly follow a word, // then that 'default' parameter block will be used as the default value // for all following words. Default parameter values are reset // by another 'default' parameter block. Default parameter values // for a single word are overridden for that word if it has a parameter block. // //=============================================================================== int VOX_ParseWordParams(char *psz, voxword_t *pvoxword, int fFirst) { char *pszsave = psz; char c; char ct; char sznum[8]; int i; static voxword_t voxwordDefault; characterset_t commandSet, delimitSet; // List of valid commands CharacterSetBuild( &commandSet, "vpset)" ); // init to defaults if this is the first word in string. if (fFirst) { voxwordDefault.pitch = -1; voxwordDefault.volume = 100; voxwordDefault.start = 0; voxwordDefault.end = 100; voxwordDefault.fKeepCached = 0; voxwordDefault.timecompress = 0; } *pvoxword = voxwordDefault; // look at next to last char to see if we have a // valid format: c = *(psz + strlen(psz) - 1); if (c != ')') return 1; // no formatting, return // scan forward to first '(' CharacterSetBuild( &delimitSet, "()" ); c = *psz; while ( !IN_CHARACTERSET(delimitSet, c) ) c = *(++psz); if ( c == ')' ) return 0; // bogus formatting // null terminate *psz = 0; ct = *(++psz); while (1) { // scan until we hit a character in the commandSet while (ct && !IN_CHARACTERSET(commandSet, ct) ) ct = *(++psz); if (ct == ')') break; memset(sznum, 0, sizeof(sznum)); i = 0; c = *(++psz); if (!V_isdigit(c)) break; // read number while (V_isdigit(c) && i < sizeof(sznum) - 1) { sznum[i++] = c; c = *(++psz); } // get value of number i = atoi(sznum); switch (ct) { case 'v': pvoxword->volume = i; break; case 'p': pvoxword->pitch = i; break; case 's': pvoxword->start = i; break; case 'e': pvoxword->end = i; break; case 't': pvoxword->timecompress = i; break; } ct = c; } // if the string has zero length, this was an isolated // parameter block. Set default voxword to these // values if (strlen(pszsave) == 0) { voxwordDefault = *pvoxword; return 0; } else return 1; } #define CVOXSAVEDWORDSIZE 32 // saved entity name/number based on type of entity & id #define CVOXGLOBMAX 4 // max number of rnd and seqential globals typedef struct _vox_entname { // type is defined by last character of group name. // for instance, V_MYNAME_S has type 'S', which is used for soldiers // V_MYNUM_M has type 'P' which is used for metrocops int type; SoundSource soundsource; // the enity emitting the sentence char *pszname; // a custom name for the entity (this is a word name) char *psznum; // a custom number for the entity (this is a word name) char *pszglobal[CVOXGLOBMAX]; // 1 global word, shared by this type of entity, picked randomly, expires after 5min char *pszglobalseq[CVOXGLOBMAX]; // 1 global word, shared by this type of entity, picked in sequence, expires after 5 min bool fdied; // true if ent died (don't clear, we need its name) int iseq[CVOXGLOBMAX]; // sequence index, for global sequential lookups float timestamp[CVOXGLOBMAX]; // latest update to this ent global timestamp float timestampseq[CVOXGLOBMAX]; // latest update to this ent global sequential timestamp float timedied; // timestamp of death } vox_entname; #define CENTNAMESMAX 64 vox_entname g_entnames[CENTNAMESMAX]; int g_entnamelastsaved = 0; // init all void VOX_InitAllEntnames( void ) { g_entnamelastsaved = 0; Q_memset(g_entnames, 0, sizeof(g_entnames)); Q_memset(g_rgmapnames, 0, sizeof(g_rgmapnames)); g_cmapnames = 0; } // get new index int VOX_GetNextEntnameIndex( void ) { g_entnamelastsaved++; if (g_entnamelastsaved >= CENTNAMESMAX) { g_entnamelastsaved = 0; } return g_entnamelastsaved; } // get index of this ent, or get a new index. if fallocnew is true, // get a new slot if none found. // NOTE: this routine always sets fdied to false - fdied is later // set to true by the caller if in IDIED routine. This // ensures that if an ent is reused, it won't be marked as fdied. int VOX_LookupEntIndex( int type, SoundSource soundsource, bool fallocnew) { int i; for (i = 0; i < CENTNAMESMAX; i++) { if ((g_entnames[i].type == type) && (g_entnames[i].soundsource == soundsource)) { g_entnames[i].fdied = false; return i; } } if ( !fallocnew ) return -1; // new index slot - init int inew = VOX_GetNextEntnameIndex(); g_entnames[inew].type = type; g_entnames[inew].soundsource = soundsource; g_entnames[inew].timedied = 0; g_entnames[inew].fdied = 0; g_entnames[inew].pszname = NULL; g_entnames[inew].psznum = NULL; for (i = 0; i < CVOXGLOBMAX; i++) { g_entnames[inew].pszglobal[i] = NULL; g_entnames[inew].timestamp[i] = 0; g_entnames[inew].iseq[i] = 0; g_entnames[inew].timestampseq[i] = 0; g_entnames[inew].pszglobalseq[i] = NULL; } return inew; } // lookup random first word from this named group, // return static, null terminated string char * VOX_LookupRndVirtual( char *pGroupName ) { // get group index int isentenceg = VOX_GroupIndexFromName( pGroupName ); if ( isentenceg < 0) return NULL; char szsentencename[32]; // get pointer to sentence name within group, using lru int isentence = VOX_GroupPick( isentenceg, szsentencename, sizeof(szsentencename)-1 ); if (isentence < 0) return NULL; // get pointer to sentence data char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); // strip trailing whitespace if (!psz) return NULL; char *pend = Q_strstr(psz, " "); if (pend) *pend = 0; // return pointer to first (and only) word return psz; } // given groupname, get pointer to first word of n'th sentence in group char *VOX_LookupSentenceByIndex( char *pGroupname, int ipick, int *pipicknext ) { // get group index int isentenceg = VOX_GroupIndexFromName( pGroupname ); if ( isentenceg < 0) return NULL; char szsentencename[32]; // get pointer to sentence name within group, using lru int isentence = VOX_GroupPickSequential( isentenceg, szsentencename, sizeof(szsentencename)-1, ipick, true ); if (isentence < 0) return NULL; // get pointer to sentence data char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); // strip trailing whitespace char *pend = Q_strstr(psz, " "); if (pend) *pend = 0; if (pipicknext) *pipicknext = isentence; // return pointer to first (and only) word return psz; } // lookup first word from this named group, group entry 'ipick', // return static, null terminated string char * VOX_LookupNumber( char *pGroupName, int ipick ) { // construct group name from V_NUMBERS + TYPE char sznumbers[16]; int glen = Q_strlen(pGroupName); int slen = Q_strlen("V_NUMBERS"); V_strcpy_safe(sznumbers, "V_NUMBERS"); // insert type character sznumbers[slen] = pGroupName[glen-1]; sznumbers[slen+1] = 0; return VOX_LookupSentenceByIndex( sznumbers, ipick, NULL ); } // lookup ent & type, return static, null terminated string // if no saved string, create one. // UNDONE: init ent/type/string array, wrap when saving char * VOX_LookupMyVirtual( int iname, char *pGroupName, char chtype, SoundSource soundsource) { char *psz = NULL; char **ppsz = NULL; // get existing ent index, or index to new slot int ient = VOX_LookupEntIndex( (int)chtype, soundsource, true ); if (iname == 1) { // lookup saved name psz = g_entnames[ient].pszname; ppsz = &(g_entnames[ient].pszname); } else { // lookup saved number psz = g_entnames[ient].psznum; ppsz = &(g_entnames[ient].psznum); } // if none found for this ent - pick one and save it if (psz == NULL) { // get new string psz = VOX_LookupRndVirtual( pGroupName ); // save pointer to new string in g_entnames *ppsz = psz; } return psz; } // get range or heading from ent to player, // store range in from 1 to 3 words as ppszNew...ppszNew2 // store count of words in pcnew // if fsimple is true, return numeric sequence based on ten digit max void VOX_LookupRangeHeadingOrGrid( int irhg, char *pGroupName, channel_t *pChannel, SoundSource soundsource, char **ppszNew, char **ppszNew1, char **ppszNew2, int *pcnew, bool fsimple ) { Vector SL; // sound -> listener vector char *phundreds = NULL; char *ptens = NULL; char *pones = NULL; int cnew = 0; float dist; int dmeters = 0; int hundreds, tens, ones; VectorSubtract(listener_origin, pChannel->origin, SL); if (irhg == 0) { // get range dist = VectorLength(SL); dmeters = (int)((dist * 2.54 / 100.0)); // convert inches to meters dmeters = clamp(dmeters, 0, 900); } else if (irhg == 1) { // get heading QAngle source_angles; source_angles.Init(0.0, 0.0, 0.0); VectorAngles( SL, source_angles ); dmeters = source_angles[YAW]; } else if (irhg == 2) { // get gridx dmeters = (int)(((16384 + listener_origin.x) * 2.54 / 100.0) / 10) % 20; } else if (irhg == 3) { // get gridy dmeters = (int)(((16384 + listener_origin.y) * 2.54 / 100.0) / 10) % 20; } dmeters = clamp(dmeters, 0, 999); // get hundreds, tens, ones hundreds = dmeters / 100; tens = (dmeters - hundreds * 100) / 10; ones = (dmeters - hundreds * 100 - tens * 10); if (fsimple) { // just return simple ten digit lookups for ones, tens, hundreds pones = VOX_LookupNumber( pGroupName, ones); cnew++; if (tens || hundreds) { ptens = VOX_LookupNumber( pGroupName, tens); cnew++; } if (hundreds) { phundreds = VOX_LookupNumber( pGroupName, hundreds ); cnew++; } goto LookupNumExit; } // get pointer to string from groupname and number // 100,200,300,400,500,600,700,800,900 if (hundreds && !tens && !ones) { if (hundreds <= 3) { phundreds = VOX_LookupNumber( pGroupName, 27 + hundreds); cnew++; } else { phundreds = VOX_LookupNumber( pGroupName, hundreds ); ptens = VOX_LookupNumber( pGroupName, 0); pones = VOX_LookupNumber( pGroupName, 0); cnew++; cnew++; } goto LookupNumExit; } if ( hundreds ) { // 101..999 if (hundreds <= 3 && !tens && ones) phundreds = VOX_LookupNumber( pGroupName, 27 + hundreds); else phundreds = VOX_LookupNumber( pGroupName, hundreds ); cnew++; // 101..109 to 901..909 if (!tens && ones) { pones = VOX_LookupNumber( pGroupName, ones); cnew++; if (hundreds > 3) { ptens = VOX_LookupNumber( pGroupName, 0); cnew++; } goto LookupNumExit; } } // 1..19 if (tens <= 1 && (tens || ones)) { pones = VOX_LookupNumber( pGroupName, ones + tens * 10 ); cnew++; tens = 0; goto LookupNumExit; } // 20..99 if (tens > 1) { if (ones) { pones = VOX_LookupNumber( pGroupName, ones ); cnew++; } ptens = VOX_LookupNumber( pGroupName, 18 + tens); cnew++; } LookupNumExit: // return values *pcnew = cnew; // return switch (cnew) { default: *ppszNew = NULL; return; case 1: // 1..19,20,30,40,50,60,70,80,90,100,200,300 *ppszNew = pones ? pones : (ptens ? ptens : (phundreds ? phundreds : NULL)); return; case 2: if (ptens && pones) { *ppszNew = ptens; *ppszNew1 = pones; } else if (phundreds && pones) { *ppszNew = phundreds; *ppszNew1 = pones; } else if (phundreds && ptens) { *ppszNew = phundreds; *ppszNew1 = ptens; } return; case 3: *ppszNew = phundreds; *ppszNew1 = ptens; *ppszNew2 = pones; return; } } // find most recent ent of this type marked as dead int VOX_LookupLastDeadIndex( int type ) { float timemax = -1; int ifound = -1; int i; for (i = 0; i < CENTNAMESMAX; i++) { if (g_entnames[i].type == type && g_entnames[i].fdied) { if (g_entnames[i].timedied >= timemax) { timemax = g_entnames[i].timedied; ifound = i; } } } return ifound; } ConVar snd_vox_globaltimeout("snd_vox_globaltimeout", "300"); // n second timeout to reset global vox words ConVar snd_vox_seqtimeout("snd_vox_seqtimetout", "300"); // n second timeout to reset global sequential vox words ConVar snd_vox_sectimeout("snd_vox_sectimetout", "300"); // n second timeout to reset global sector id ConVar snd_vox_captiontrace( "snd_vox_captiontrace", "0", 0, "Shows sentence name for sentences which are set not to show captions." ); // return index to ent which knows the current sector. // if no ent found, alloc a new one and establish shector. // sectors expire after approx 5 minutes. #define VOXSECTORMAX 20 static float g_vox_lastsectorupdate = 0; static int g_vox_isector = -1; char *VOX_LookupSectorVirtual( char *pGroupname ) { float curtime = g_pSoundServices->GetClientTime(); if (g_vox_isector == -1) { g_vox_isector = RandomInt(0, VOXSECTORMAX-1); } // update sector every 5 min if (curtime - g_vox_lastsectorupdate > snd_vox_sectimeout.GetInt()) { g_vox_isector++; if (g_vox_isector > VOXSECTORMAX) g_vox_isector = 1; g_vox_lastsectorupdate = curtime; } return VOX_LookupNumber( pGroupname, g_vox_isector ); } char *VOX_LookupGlobalVirtual( int type, SoundSource soundsource, char *pGroupName, int iglobal ) { int i; float curtime = g_pSoundServices->GetClientTime(); // look for ent of this type with un-expired global for (i = 0; i < CENTNAMESMAX; i++) { if (g_entnames[i].type == type) { if (curtime - g_entnames[i].timestamp[iglobal] <= snd_vox_globaltimeout.GetInt()) { // if this ent has an un-expired global, return it, otherwise break if (g_entnames[i].pszglobal[iglobal]) return g_entnames[i].pszglobal[iglobal]; else break; } } } // if not found, construct a new global for this ent // pick random word from groupname char *psz = VOX_LookupRndVirtual( pGroupName ); // get existing ent index, or index to new slot int ient = VOX_LookupEntIndex( type, soundsource, true ); g_entnames[ient].timestamp[iglobal] = curtime; g_entnames[ient].pszglobal[iglobal] = psz; return psz; } // lookup global values in group in sequence - get next value // in sequence. sequence counter expires every 2.5 minutes. char *VOX_LookupGlobalSeqVirtual( int type, SoundSource soundsource, char *pGroupName, int iglobal ) { int i; int ient; float curtime = g_pSoundServices->GetClientTime(); // look for ent of this type with un-expired global for (i = 0; i < CENTNAMESMAX; i++) { if (g_entnames[i].type == type) { if (curtime - g_entnames[i].timestampseq[iglobal] <= (snd_vox_seqtimeout.GetInt()/2)) { // if first ent found has an un-expired global sequence set, // get next value in sequence, otherwise break ient = i; goto Pick_next; } else { // global has expired - reset sequence ient = i; g_entnames[ient].iseq[iglobal] = 0; goto Pick_next; } } } // if not found, construct a new sequential global for this ent ient = VOX_LookupEntIndex( type, soundsource, true ); // pick next word from groupname Pick_next: int ipick = g_entnames[ient].iseq[iglobal]; int ipicknext = 0; char *psz = VOX_LookupSentenceByIndex( pGroupName, ipick, &ipicknext ); g_entnames[ient].iseq[iglobal] = ipicknext; // get existing ent index, or index to new slot g_entnames[ient].timestampseq[iglobal] = curtime; g_entnames[ient].pszglobalseq[iglobal] = psz; return psz; } // insert new words into rgpparseword at 'ireplace' slot void VOX_InsertWords( int ireplace, int cnew, char *pszNew, char *pszNew1, char *pszNew2 ) { if ( cnew ) { // make space in rgpparseword for 'cnew - 1' new words int ccopy = cnew - 1; // number of new slots we need int j; if (ccopy) { for (j = CVOXWORDMAX-1; j > ireplace + ccopy; j--) rgpparseword[j] = rgpparseword[j - ccopy ]; } // replace rgpparseword entry(s) with the substitued name(s) rgpparseword[ireplace] = pszNew; if ( cnew == 2 || cnew == 3) rgpparseword[ireplace+1] = pszNew1; if ( cnew == 3 ) rgpparseword[ireplace+2] = pszNew2; } } // remove 'silent' word from rgpparseword void VOX_DeleteWord( int iword ) { if (iword < 0 || iword >= CVOXWORDMAX) return; rgpparseword[iword] = 0; // slide all words > iword up into vacated slot for (int j = iword; j < CVOXWORDMAX-1; j++) rgpparseword[j] = rgpparseword[j+1]; } // get global list of map names from sentences.txt // map names are stored in order in V_MAPNAMES group void VOX_LookupMapnames( void ) { // get group V_MAPNAMES int i; char *psz; int inext = 0; for (i = 0; i < CVOXMAPNAMESMAX; i++) { // step sequentially through group - return ptr to 1st word in each group (map name) psz = VOX_LookupSentenceByIndex( "V_MAPNAME", i, &inext ); if (!psz) return; g_rgmapnames[i] = psz; g_cmapnames++; } } // get index of current map name // return 0 as default index if not found int VOX_GetMapNameIndex( const char *pszmapname ) { for (int i = 0; i < g_cmapnames; i++) { if ( Q_strstr( pszmapname, g_rgmapnames[i] ) ) return i; } return 0; } // look for virtual 'V_' values in rgpparseword. // V_MYNAME - replace with saved name value (based on type + entity) // - if no saved name, create one and save // V_MYNUM - replace with saved number value (based on type + entity) // - if no saved num, create on and save // V_RNDNUM - grab a random number string from V_RNDNUM_ // V_RNDNAME - grab a random name string from V_RNDNAME_ // replace any 'V_' values with actual string names in rgpparseword extern ConVar host_map; inline bool IsVirtualName( const char *pName ) { return (pName[0] == 'V' && pName[1] == '_'); } void VOX_ReplaceVirtualNames( channel_t *pchan ) { // for each word in the sentence, check for V_, if found // replace virtual word with saved word or rnd word int i = 0; char *pszNew = NULL; char *pszNew1 = NULL; char *pszNew2 = NULL; int iname = -1; int cnew = 0; bool fbymap; char *pszmaptoken; SoundSource soundsource = pchan ? pchan->soundsource : 0; const char *pszmap = host_map.GetString(); // get global list of map names from sentences.txt while (rgpparseword[i]) { if ( IsVirtualName( rgpparseword[i] ) ) { iname = -1; cnew = 0; pszNew = NULL; pszNew1 = NULL; pszNew2 = NULL; char szparseword[256]; int slen = Q_strlen(rgpparseword[i]); char chtype = rgpparseword[i][slen-1]; // copy word to temp location so we can perform in-place substitutions V_strcpy_safe(szparseword, rgpparseword[i]); // fbymap is true if lookup is performed via mapname instead of via ordinal pszmaptoken = ( Q_strstr(szparseword, "_MAP__") ); fbymap = (pszmaptoken == NULL ? false : true); if (fbymap) { int imap = VOX_GetMapNameIndex( pszmap ); imap = clamp (imap, 0, 99); // replace last 2 characters in _MAP__ substring // with imap - this effectively makes all // '_map_' lookups relative to the mapname if ( imap >= 10 ) { pszmaptoken[4] = (imap/10) + '0'; pszmaptoken[5] = (imap%10) + '0'; } else { pszmaptoken[4] = '0'; pszmaptoken[5] = imap + '0'; } } if ( Q_strstr(szparseword, "V_MYNAME") ) { iname = 1; } else if ( Q_strstr(szparseword, "V_MYNUM") ) { iname = 0; } if ( iname >= 0 ) { // lookup ent & type, return static, null terminated string // if no saved string, create one pszNew = VOX_LookupMyVirtual( iname, szparseword, chtype, soundsource); cnew = 1; } else { if ( Q_strstr(szparseword, "V_RND") ) { // lookup random first word from this named group, // return static, null terminated string pszNew = VOX_LookupRndVirtual( szparseword ); cnew = 1; } else if ( Q_strstr(szparseword, "V_DIST") ) { // get range from ent to player, return pointers to new words VOX_LookupRangeHeadingOrGrid( 0, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); } else if ( Q_strstr(szparseword, "V_DIR") ) { // get heading from ent to player, return pointers to new words VOX_LookupRangeHeadingOrGrid( 1, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, false); } else if ( Q_strstr(szparseword, "V_IDIED") ) { // SILENT MARKER - this ent died - mark as dead and timestamp int ient = VOX_LookupEntIndex( chtype, soundsource, false); if (ient < 0) { // if not found, allocate new ent, give him a name & number, mark as dead char szgroup1[32]; char szgroup2[32]; V_strcpy_safe(szgroup1, "V_MYNAME"); szgroup1[8] = chtype; szgroup1[9] = 0; V_strcpy_safe(szgroup2, "V_MYNUM"); szgroup2[7] = chtype; szgroup2[8] = 0; ient = VOX_LookupEntIndex( chtype, soundsource, true); g_entnames[ient].pszname = VOX_LookupRndVirtual( szgroup1 ); g_entnames[ient].psznum = VOX_LookupRndVirtual( szgroup2 ); } g_entnames[ient].fdied = true; g_entnames[ient].timedied = g_pSoundServices->GetClientTime(); // clear this 'silent' word from rgpparseword VOX_DeleteWord(i); } else if ( Q_strstr(szparseword, "V_WHODIED") ) { // get last dead unit of this type int ient = VOX_LookupLastDeadIndex( chtype ); // get name and number if (ient >= 0) { cnew = 1; pszNew = g_entnames[ient].pszname; pszNew1 = g_entnames[ient].psznum; if (pszNew1) cnew++; } else { // no dead units, just clear V_WHODIED VOX_DeleteWord(i); } } else if ( Q_strstr(szparseword, "V_SECTOR") ) { // sectors are fictional - they simply // increase sequentially and expire every 5 minutes pszNew = VOX_LookupSectorVirtual( szparseword ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_GRIDX") ) { // player x position in 10 meter increments VOX_LookupRangeHeadingOrGrid( 2, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); } else if ( Q_strstr(szparseword, "V_GRIDY") ) { // player y position in 10 meter increments VOX_LookupRangeHeadingOrGrid( 3, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); } else if ( Q_strstr(szparseword, "V_G0_") ) { // 4 rnd globals per type, globals expire after 5 minutes // used for target designation, master sector code name etc. pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 0 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_G1_") ) { // 4 rnd globals per type, globals expire after 5 minutes // used for target designation, master sector code name etc. pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 1 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_G2_") ) { // 4 rnd globals per type, globals expire after 5 minutes // used for target designation, master sector code name etc. pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 2 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_G3_") ) { // 4 rnd globals per type, globals expire after 5 minutes // used for target designation, master sector code name etc. pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 3 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_SEQG0_") ) { // 4 sequential globals per type, selected sequentially in list // used for total target hit count etc. pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 0 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_SEQG1_") ) { // 4 sequential globals per type, selected sequentially in list // used for total target hit count etc. pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 1 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_SEQG2_") ) { // 4 sequential globals per type, selected sequentially in list // used for total target hit count etc. pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 2 ); if (pszNew) cnew = 1; } else if ( Q_strstr(szparseword, "V_SEQG3_") ) { // 4 sequential globals per type, selected sequentially in list // used for total target hit count etc. pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 3 ); if (pszNew) cnew = 1; } } // insert up to 3 new words into rgpparseword at 'i' location VOX_InsertWords( i, cnew, pszNew, pszNew1, pszNew2 ); } i++; } } void VOX_Precache( IEngineSound *pSoundSystem, int sentenceIndex, const char *pPathOverride = NULL ) { voxword_t rgvoxword[CVOXWORDMAX]; char buffer[512]; char szpath[MAX_PATH]; char pathbuffer[MAX_PATH]; char *pWords[CVOXWORDMAX]; // array of pointers to parsed words if ( !IsVirtualName(g_Sentences[sentenceIndex].pName)) { g_Sentences[sentenceIndex].isPrecached = true; } memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); char *psz = (char *)(g_Sentences[sentenceIndex].pName + Q_strlen(g_Sentences[sentenceIndex].pName) + 1); // get directory from string, advance psz psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); Q_strncpy(buffer, psz, sizeof( buffer ) ); psz = buffer; if ( pPathOverride ) { Q_strncpy(szpath, pPathOverride, sizeof(szpath)); } // parse sentence (also inserts null terminators between words) VOX_ParseString(psz); int i = 0, count = 0; // copy the parsed words out of the globals for ( i = 0; rgpparseword[i]; i++ ) { pWords[i] = rgpparseword[i]; count++; } int cword = 0; for ( i = 0; i < count; i++ ) { if ( IsVirtualName(pWords[i]) ) { CUtlVector< WordBuf > list; VOX_BuildVirtualNameList( pWords[i], list ); int c = list.Count(); for ( int j = 0 ; j < c; ++j ) { Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, list[j].word ); pSoundSystem->PrecacheSound( pathbuffer, false ); } } else { // Get any pitch, volume, start, end params into voxword if (VOX_ParseWordParams(pWords[i], &rgvoxword[cword], i == 0)) { // this is a valid word (as opposed to a parameter block) Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, pWords[i] ); // find name, if already in cache, mark voxword // so we don't discard when word is done playing pSoundSystem->PrecacheSound( pathbuffer, false ); cword++; } } } } void VOX_PrecacheSentenceGroup( IEngineSound *pSoundSystem, const char *pGroupName, const char *pPathOverride ) { int i; int len = Q_strlen( pGroupName ); for ( i = 0; i < g_Sentences.Count(); i++ ) { if ( !g_Sentences[i].isPrecached && !Q_strncasecmp( g_Sentences[i].pName, pGroupName, len ) ) { VOX_Precache( pSoundSystem, i, pPathOverride ); } } } // link all sounds in sentence, start playing first word. // return number of words loaded void VOX_LoadSound( channel_t *pchan, const char *pszin ) { #ifndef SWDS char buffer[512]; int i, cword; char pathbuffer[MAX_PATH]; char szpath[MAX_PATH]; voxword_t rgvoxword[CVOXWORDMAX]; char *psz; bool emitcaption = false; CUtlSymbol captionSymbol = UTL_INVAL_SYMBOL; float duration = 0.0f; if (!pszin) return; memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); memset(buffer, 0, sizeof(buffer)); // lookup actual string in g_Sentences, // set pointer to string data psz = VOX_LookupString(pszin, NULL, &emitcaption, &captionSymbol, &duration ); if (!psz) { DevMsg ("VOX_LoadSound: no sentence named %s\n",pszin); return; } // get directory from string, advance psz psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); if ( Q_strlen(psz) > sizeof(buffer) - 1 ) { DevMsg ("VOX_LoadSound: sentence is too long %s\n",psz); return; } // copy into buffer Q_strncpy(buffer, psz, sizeof( buffer ) ); psz = buffer; // parse sentence (also inserts null terminators between words) VOX_ParseString(psz); // replace any 'V_' values with actual string names in rgpparseword VOX_ReplaceVirtualNames( pchan ); // for each word in the sentence, construct the filename, // lookup the sfx and save each pointer in a temp array i = 0; cword = 0; char captionstream[ 1024 ]; char groupname[ 512 ]; Q_strncpy( groupname, pszin, sizeof( groupname ) ); int len = Q_strlen( groupname ); while ( len > 0 && V_isdigit( groupname[ len - 1 ] ) ) { groupname[ len - 1 ] = 0; --len; } Q_snprintf( captionstream, sizeof( captionstream ), "%s ", groupname ); while (rgpparseword[i]) { // Get any pitch, volume, start, end params into voxword if (VOX_ParseWordParams(rgpparseword[i], &rgvoxword[cword], i == 0)) { // this is a valid word (as opposed to a parameter block) Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] ); // find name, if already in cache, mark voxword // so we don't discard when word is done playing rgvoxword[cword].sfx = S_FindName(pathbuffer, &(rgvoxword[cword].fKeepCached)); // JAY: HACKHACK: Keep all sentences cached for now rgvoxword[cword].fKeepCached = 1; char captiontoken[ 128 ]; Q_snprintf( captiontoken, sizeof( captiontoken ), "S(%s%s) ", szpath, rgpparseword[i] ); Q_strncat( captionstream, captiontoken, sizeof( captionstream ), COPY_ALL_CHARACTERS ); cword++; } i++; } pchan->pMixer = NULL; if (cword) { // some 'virtual' sentences can end up with 0 words // if no words, then pchan->pMixer is null; chan will be released right away. pchan->pMixer = CreateSentenceMixer( rgvoxword ); if ( !pchan->pMixer ) return; pchan->flags.isSentence = true; pchan->sfx = rgvoxword[0].sfx; Assert(pchan->sfx); if ( g_pSoundServices ) { if ( emitcaption ) { if ( captionSymbol != UTL_INVAL_SYMBOL ) { g_pSoundServices->EmitCloseCaption( captionSymbol.String(), duration ); if ( snd_vox_captiontrace.GetBool() ) { Msg( "Vox: caption '%s'\n", captionSymbol.String() ); } } else { g_pSoundServices->EmitSentenceCloseCaption( captionstream ); if ( snd_vox_captiontrace.GetBool() ) { Msg( "Vox: captionstream '%s'\n", captionstream ); } } } else { if ( snd_vox_captiontrace.GetBool() ) { Msg( "Vox: No caption for '%s'\n", pszin ? pszin : "NULL" ); } } } } #endif } static bool CCPairLessFunc( const ccpair& lhs, const ccpair& rhs ) { return Q_stricmp( lhs.token.word, rhs.token.word ) < 0; } void VOX_AddNumbers( char *pGroupName, CUtlVector< WordBuf >& list ) { // construct group name from V_NUMBERS + TYPE for ( int i = 0; i <= 30; ++i ) { char sznumbers[16]; int glen = Q_strlen(pGroupName); int slen = Q_strlen("V_NUMBERS"); V_strcpy_safe(sznumbers, "V_NUMBERS"); // insert type character sznumbers[slen] = pGroupName[glen-1]; sznumbers[slen+1] = 0; WordBuf w; // w.Set( VOX_LookupString( VOX_LookupSentenceByIndex( sznumbers, i, NULL ), NULL ) ); w.Set( VOX_LookupSentenceByIndex( sznumbers, i, NULL ) ); list.AddToTail( w ); } } void VOX_AddRndVirtual( char *pGroupName, CUtlVector< WordBuf >& list ) { // get group index int isentenceg = VOX_GroupIndexFromName( pGroupName ); if ( isentenceg < 0) return; char szsentencename[32]; char const *szgroupname = g_SentenceGroups[ isentenceg ].GroupName(); // get pointer to sentence name within group, using lru for ( int snum = 0; snum < g_SentenceGroups[ isentenceg ].count; ++snum ) { Q_snprintf( szsentencename, sizeof( szsentencename ), "%s%d", szgroupname, snum ); char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); if ( psz ) { WordBuf w; w.Set( psz ); list.AddToTail( w ); } } } void VOX_AddMyVirtualWords( int iname, char *pGroupName, char chtype, CUtlVector< WordBuf >& list ) { VOX_AddRndVirtual( pGroupName, list ); } void VOX_BuildVirtualNameList( char *word, CUtlVector< WordBuf >& list ) { // for each word in the sentence, check for V_, if found // replace virtual word with saved word or rnd word int iname = -1; bool fbymap; char *pszmaptoken; char szparseword[256]; int slen = Q_strlen(word); char chtype = word[slen-1]; // copy word to temp location so we can perform in-place substitutions Q_strncpy( szparseword, word, sizeof( szparseword ) ); // fbymap is true if lookup is performed via mapname instead of via ordinal pszmaptoken = ( Q_strstr(szparseword, "_MAP__") ); fbymap = (pszmaptoken == NULL ? false : true); if (fbymap) { for ( int imap = 0; imap < g_cmapnames; ++imap ) { // replace last 2 characters in _MAP__ substring // with imap - this effectively makes all // '_map_' lookups relative to the mapname pszmaptoken[4] = '0'; if (imap < 10) Q_snprintf( &(pszmaptoken[5]), 1, "%1d", imap ); else Q_snprintf( &(pszmaptoken[4]), 2, "%d", imap ); // Recurse... VOX_BuildVirtualNameList( szparseword, list ); } return; } if ( Q_strstr(szparseword, "V_MYNAME") ) { iname = 1; } else if ( Q_strstr(szparseword, "V_MYNUM") ) { iname = 0; } if ( iname >= 0 ) { // lookup ent & type, return static, null terminated string // if no saved string, create one VOX_AddMyVirtualWords( iname, szparseword, chtype, list ); } else { if ( Q_strstr(szparseword, "V_RND") ) { // lookup random first word from this named group, // return static, null terminated string VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_DIST") ) { VOX_AddNumbers( szparseword, list ); } else if ( Q_strstr(szparseword, "V_DIR") ) { VOX_AddNumbers( szparseword, list ); } else if ( Q_strstr(szparseword, "V_IDIED") ) { // SILENT MARKER - this ent died - mark as dead and timestamp // if not found, allocate new ent, give him a name & number, mark as dead char szgroup1[32]; char szgroup2[32]; V_strcpy_safe(szgroup1, "V_MYNAME"); szgroup1[8] = chtype; szgroup1[9] = 0; V_strcpy_safe(szgroup2, "V_MYNUM"); szgroup2[7] = chtype; szgroup2[8] = 0; VOX_BuildVirtualNameList( szgroup1, list ); VOX_BuildVirtualNameList( szgroup2, list ); return; } else if ( Q_strstr(szparseword, "V_WHODIED") ) { // get last dead unit of this type /* int ient = VOX_LookupLastDeadIndex( chtype ); // get name and number if (ient >= 0) { cnew = 1; pszNew = g_entnames[ient].pszname; pszNew1 = g_entnames[ient].psznum; if (pszNew1) cnew++; } else { // no dead units, just clear V_WHODIED VOX_DeleteWord(i); } */ } else if ( Q_strstr(szparseword, "V_SECTOR") ) { VOX_AddNumbers( szparseword, list ); } else if ( Q_strstr(szparseword, "V_GRIDX") ) { VOX_AddNumbers( szparseword, list ); } else if ( Q_strstr(szparseword, "V_GRIDY") ) { VOX_AddNumbers( szparseword, list ); } else if ( Q_strstr(szparseword, "V_G0_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_G1_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_G2_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_G3_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_SEQG0_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_SEQG1_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_SEQG2_") ) { VOX_AddRndVirtual( szparseword, list ); } else if ( Q_strstr(szparseword, "V_SEQG3_") ) { VOX_AddRndVirtual( szparseword, list ); } } if ( Q_strnicmp( szparseword, "V_", 2 ) ) { WordBuf w; w.Set( szparseword ); list.AddToTail( w ); } } //----------------------------------------------------------------------------- // Purpose: For generating reslists, adds the wavefile to the dictionary // Input : *fn - //----------------------------------------------------------------------------- void VOX_Touch( char const *fn, CUtlDict< int, int >& list ) { if ( list.Find( fn ) == list.InvalidIndex() ) { list.Insert( fn ); } } //----------------------------------------------------------------------------- // Purpose: Iterates the touch list and touches all referenced .wav files. // Input : int - // list - //----------------------------------------------------------------------------- void VOX_TouchSounds( CUtlDict< int, int >& list, CUtlRBTree< ccpair, int >& ccpairs, bool spewsentences ) { int i; for ( i = list.First(); i != list.InvalidIndex(); i = list.Next( i ) ) { char const *fn = list.GetElementName( i ); // Msg( "touch %s\n", fn ); char expanded[ 512 ]; Q_snprintf( expanded, sizeof( expanded ), "sound/%s", fn ); FileHandle_t fh = g_pFileSystem->Open( expanded, "rb" ); if ( FILESYSTEM_INVALID_HANDLE != fh ) { g_pFileSystem->Close( fh ); } } if ( spewsentences ) { for ( i = ccpairs.FirstInorder() ; i != ccpairs.InvalidIndex(); i = ccpairs.NextInorder( i ) ) { ccpair& pair = ccpairs[ i ]; Msg( "\"%s\"\t\"%s\"\n", pair.token.word, pair.value.word ); } FileHandle_t fh = g_pFileSystem->Open( "sentences.m3u", "wt", "GAME" ); if ( FILESYSTEM_INVALID_HANDLE != fh ) { for ( i = ccpairs.FirstInorder() ; i != ccpairs.InvalidIndex(); i = ccpairs.NextInorder( i ) ) { ccpair& pair = ccpairs[ i ]; char outline[ 512 ]; Q_snprintf( outline, sizeof( outline ), "%s\n", pair.fullpath.word ); g_pFileSystem->Write( outline, Q_strlen(outline), fh ); } g_pFileSystem->Close( fh ); } } } // link all sounds in sentence, start playing first word. // return number of words loaded void VOX_TouchSound( const char *pszin, CUtlDict< int, int >& filelist, CUtlRBTree< ccpair, int >& ccpairs, bool spewsentences ) { #ifndef SWDS char buffer[512]; int i, cword; char pathbuffer[MAX_PATH]; char szpath[MAX_PATH]; voxword_t rgvoxword[CVOXWORDMAX]; char *psz; if (!pszin) return; memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); memset(buffer, 0, sizeof(buffer)); // lookup actual string in g_Sentences, // set pointer to string data psz = VOX_LookupString(pszin, NULL); if (!psz) { DevMsg ("VOX_TouchSound: no sentence named %s\n",pszin); return; } // get directory from string, advance psz psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); if ( Q_strlen(psz) > sizeof(buffer) - 1 ) { DevMsg ("VOX_TouchSound: sentence is too long %s\n",psz); return; } // copy into buffer Q_strncpy(buffer, psz, sizeof( buffer ) ); psz = buffer; // parse sentence (also inserts null terminators between words) VOX_ParseString(psz); // for each word in the sentence, construct the filename, // lookup the sfx and save each pointer in a temp array i = 0; cword = 0; CUtlVector< WordBuf > rep; while (rgpparseword[i]) { // Get any pitch, volume, start, end params into voxword if ( VOX_ParseWordParams(rgpparseword[i], &rgvoxword[cword], i == 0 ) ) { // Iterate all virtuals here... if ( !Q_strnicmp( rgpparseword[i], "V_", 2 ) ) { CUtlVector< WordBuf > list; VOX_BuildVirtualNameList( rgpparseword[i], list ); int c = list.Count(); for ( int j = 0 ; j < c; ++j ) { char name[ 256 ]; Q_snprintf( name, sizeof( name ), "%s", list[ j ].word ); if ( !Q_strnicmp( name, "V_", 2 ) ) { Warning( "VOX_TouchSound didn't resolve virtual token %s!\n", name ); } Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, name ); VOX_Touch( pathbuffer, filelist ); WordBuf w; if ( j == 0 ) { w.Set( name ); rep.AddToTail( w ); } ccpair pair; Q_snprintf( pair.token.word, sizeof( pair.token.word ), "S(%s%s)", szpath, name ); pair.value.Set( name ); Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s/sound/%s%s.wav", g_pSoundServices->GetGameDir(), szpath, name ); Q_FixSlashes( pathbuffer, '\\' ); pair.fullpath.Set( pathbuffer ); if ( ccpairs.Find( pair ) == ccpairs.InvalidIndex() ) { ccpairs.Insert( pair ); } } } else { // this is a valid word (as opposed to a parameter block) Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] ); VOX_Touch( pathbuffer, filelist ); WordBuf w; w.Set( rgpparseword[ i ] ); rep.AddToTail( w ); ccpair pair; Q_snprintf( pair.token.word, sizeof( pair.token.word ), "S(%s%s)", szpath, rgpparseword[i] ); pair.value.Set( rgpparseword[i] ); Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s/sound/%s%s.wav", g_pSoundServices->GetGameDir(), szpath, rgpparseword[ i ] ); Q_FixSlashes( pathbuffer, CORRECT_PATH_SEPARATOR ); pair.fullpath.Set( pathbuffer ); if ( ccpairs.Find( pair ) == ccpairs.InvalidIndex() ) { ccpairs.Insert( pair ); } } } i++; } if ( spewsentences ) { char outbuf[ 1024 ]; // Build representative text outbuf[ 0 ] = 0; for ( i = 0; i < rep.Count(); ++i ) { /* if ( !Q_stricmp( rep[ i ].word, "_comma" ) ) { if ( i != 0 && Q_strlen( outbuf ) >= 1 ) { outbuf[ Q_strlen( outbuf ) - 1 ] =0; } // Don't end sentence with comma.. if ( i != rep.Count() - 1 ) { Q_strncat( outbuf, ", ", sizeof( outbuf ), COPY_ALL_CHARACTERS ); } continue; } */ Q_strncat( outbuf, rep[ i ].word, sizeof( outbuf ), COPY_ALL_CHARACTERS ); if ( i != rep.Count() - 1 ) { Q_strncat( outbuf, " ", sizeof( outbuf ), COPY_ALL_CHARACTERS ); } } Msg( " %s\n", outbuf ); } #endif } //----------------------------------------------------------------------------- // Purpose: Take a NULL terminated sentence, and parse any commands contained in // {}. The string is rewritten in place with those commands removed. // // Input : *pSentenceData - sentence data to be modified in place // sentenceIndex - global sentence table index for any data that is // parsed out //----------------------------------------------------------------------------- void VOX_ParseLineCommands( char *pSentenceData, int sentenceIndex ) { char tempBuffer[512]; char *pNext, *pStart; int length, tempBufferPos = 0; if ( !pSentenceData ) return; pStart = pSentenceData; while ( *pSentenceData ) { pNext = ScanForwardUntil( pSentenceData, '{' ); // Find length of "good" portion of the string (not a {} command) length = pNext - pSentenceData; if ( tempBufferPos + length > sizeof(tempBuffer) ) { DevMsg("Error! sentence too long!\n" ); return; } // Copy good string to temp buffer memcpy( tempBuffer + tempBufferPos, pSentenceData, length ); // Move the copy position tempBufferPos += length; pSentenceData = pNext; // Skip ahead of the opening brace if ( *pSentenceData ) { pSentenceData++; } while ( 1 ) { // Skip whitespace while ( *pSentenceData && *pSentenceData <= 32 ) { pSentenceData++; } // Simple comparison of string commands: switch( tolower( *pSentenceData ) ) { case 'l': // All commands starting with the letter 'l' here if ( !Q_strnicmp( pSentenceData, "len", 3 ) ) { g_Sentences[sentenceIndex].length = atof( pSentenceData + 3 ) ; // "len " len + space pSentenceData += 4; // Skip until next } or whitespace character while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) pSentenceData++; } break; case 'c': // This sentence should emit a close caption if ( !Q_strnicmp( pSentenceData, "closecaption", 12 ) ) { g_Sentences[sentenceIndex].closecaption = true; pSentenceData += 12; pSentenceData = (char *)COM_Parse( pSentenceData ); // Skip until next } or whitespace character while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) pSentenceData++; if ( Q_strlen( com_token ) > 0 ) { g_Sentences[sentenceIndex].caption = com_token; } else { g_Sentences[sentenceIndex].caption = UTL_INVAL_SYMBOL; } } break; case 0: default: { // Skip until next } or whitespace character while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) pSentenceData++; } break; } // Done? if ( !*pSentenceData || *pSentenceData == '}' ) { break; } } // pSentenceData = ScanForwardUntil( pSentenceData, '}' ); // Skip the closing brace if ( *pSentenceData ) pSentenceData++; // Skip trailing whitespace while ( *pSentenceData && *pSentenceData <= 32 ) pSentenceData++; } if ( tempBufferPos < sizeof(tempBuffer) ) { // terminate cleaned up copy tempBuffer[ tempBufferPos ] = 0; // Copy it over the original data Q_strcpy( pStart, tempBuffer ); } } //----------------------------------------------------------------------------- // Purpose: Add a new group or increment count of the existing one // Input : *pSentenceName - text of the sentence name //----------------------------------------------------------------------------- int VOX_GroupAdd( const char *pSentenceName ) { int len = strlen( pSentenceName ) - 1; // group members end in a number if ( len <= 0 || !V_isdigit(pSentenceName[len]) ) return -1; // truncate away the index while ( len > 0 && V_isdigit(pSentenceName[len]) ) { len--; } // make a copy of the actual group name char *groupName = (char *)stackalloc( len + 2 ); Q_strncpy( groupName, pSentenceName, len+2 ); // check for it in the list int i; sentencegroup_t *pGroup; CUtlSymbol symGroupName = sentencegroup_t::GetSymbol( groupName ); int groupCount = g_SentenceGroups.Size(); for ( i = 0; i < groupCount; i++ ) { int groupIndex = (i + groupCount-1) % groupCount; // Start at the last group a loop around pGroup = &g_SentenceGroups[groupIndex]; if ( symGroupName == pGroup->GroupNameSymbol() ) { // Matches previous group, bump count pGroup->count++; return i; } } // new group int addIndex = g_SentenceGroups.AddToTail(); sentencegroup_t *group = &g_SentenceGroups[addIndex]; group->SetGroupName( groupName ); group->count = 1; return addIndex; } #if DEAD //----------------------------------------------------------------------------- // Purpose: clear the sentence groups //----------------------------------------------------------------------------- void VOX_GroupClear( void ) { g_SentenceGroups.RemoveAll(); } #endif void VOX_LRUInit( sentencegroup_t *pGroup ) { int i, n1, n2, temp; if ( pGroup->count ) { unsigned char *pLRU = &g_GroupLRU[pGroup->lru]; for (i = 0; i < pGroup->count; i++) pLRU[i] = (unsigned char) i; // randomize array by swapping random elements for (i = 0; i < (pGroup->count * 4); i++) { // FIXME: This should probably call through g_pSoundServices // or some other such call? n1 = RandomInt(0,pGroup->count-1); n2 = RandomInt(0,pGroup->count-1); temp = pLRU[n1]; pLRU[n1] = pLRU[n2]; pLRU[n2] = temp; } } } //----------------------------------------------------------------------------- // Purpose: Init the LRU for each sentence group //----------------------------------------------------------------------------- void VOX_GroupInitAllLRUs( void ) { int i; int totalCount = 0; for ( i = 0; i < g_SentenceGroups.Size(); i++ ) { g_SentenceGroups[i].lru = totalCount; totalCount += g_SentenceGroups[i].count; } g_GroupLRU.Purge(); g_GroupLRU.EnsureCount( totalCount ); for ( i = 0; i < g_SentenceGroups.Size(); i++ ) { VOX_LRUInit( &g_SentenceGroups[i] ); } } //----------------------------------------------------------------------------- // Purpose: Only during reslist generation //----------------------------------------------------------------------------- void VOX_AddSentenceWavesToResList( void ) { if ( !CommandLine()->FindParm( "-makereslists" ) && !CommandLine()->FindParm( "-spewsentences" ) ) { return; } bool spewsentences = CommandLine()->FindParm( "-spewsentences" ) != 0 ? true : false; CUtlDict< int, int > list; CUtlRBTree< ccpair, int > ccpairs( 0, 0, CCPairLessFunc ); int i; int sentencecount = g_Sentences.Count(); for ( i = 0; i < sentencecount; i++ ) { // Walk through all nonvirtual sentences and touch the referenced sounds... sentence_t *pSentence = &g_Sentences[i]; if ( !Q_strnicmp( pSentence->pName, "V_", 2 ) ) { continue; } if ( spewsentences ) { const char *psz = VOX_LookupString(pSentence->pName, NULL); if ( psz ) { Msg( "%s : %s\n", pSentence->pName, psz ); } } VOX_TouchSound( pSentence->pName, list, ccpairs, spewsentences ); } VOX_TouchSounds( list, ccpairs, spewsentences ); list.RemoveAll(); } //----------------------------------------------------------------------------- // Purpose: Given a group name, return that group's index // Input : *pGroupName - name of the group // Output : int - index in group table, returns -1 if no matching group is found //----------------------------------------------------------------------------- int VOX_GroupIndexFromName( const char *pGroupName ) { int i; if ( pGroupName ) { // search rgsentenceg for match on szgroupname CUtlSymbol symGroupName = sentencegroup_t::GetSymbol( pGroupName ); for ( i = 0; i < g_SentenceGroups.Size(); i++ ) { if ( symGroupName == g_SentenceGroups[i].GroupNameSymbol() ) return i; } } return -1; } //----------------------------------------------------------------------------- // Purpose: return the group's name // Input : groupIndex - index of the group // Output : const char * - name pointer //----------------------------------------------------------------------------- const char *VOX_GroupNameFromIndex( int groupIndex ) { if ( groupIndex >= 0 && groupIndex < g_SentenceGroups.Size() ) return g_SentenceGroups[groupIndex].GroupName(); return NULL; } // ignore lru. pick next sentence from sentence group. Go in order until we hit the last sentence, // then repeat list if freset is true. If freset is false, then repeat last sentence. // ipick is passed in as the requested sentence ordinal. // ipick 'next' is returned. // return of -1 indicates an error. int VOX_GroupPickSequential( int isentenceg, char *szfound, int szfoundLen, int ipick, int freset ) { const char *szgroupname; unsigned char count; if (isentenceg < 0 || isentenceg > g_SentenceGroups.Size()) return -1; szgroupname = g_SentenceGroups[isentenceg].GroupName(); count = g_SentenceGroups[isentenceg].count; if (count == 0) return -1; if (ipick >= count) ipick = count-1; Q_snprintf( szfound, szfoundLen, "!%s%d", szgroupname, ipick ); if (ipick >= count) { if (freset) // reset at end of list return 0; else return count; } return ipick + 1; } // pick a random sentence from rootname0 to rootnameX. // picks from the rgsentenceg[isentenceg] least // recently used, modifies lru array. returns the sentencename. // note, lru must be seeded with 0-n randomized sentence numbers, with the // rest of the lru filled with -1. The first integer in the lru is // actually the size of the list. Returns ipick, the ordinal // of the picked sentence within the group. int VOX_GroupPick( int isentenceg, char *szfound, int strLen ) { const char *szgroupname; unsigned char *plru; unsigned char i; unsigned char count; unsigned char ipick=0; int ffound = FALSE; if (isentenceg < 0 || isentenceg > g_SentenceGroups.Size()) return -1; szgroupname = g_SentenceGroups[isentenceg].GroupName(); count = g_SentenceGroups[isentenceg].count; plru = &g_GroupLRU[g_SentenceGroups[isentenceg].lru]; while (!ffound) { for (i = 0; i < count; i++) if (plru[i] != 0xFF) { ipick = plru[i]; plru[i] = 0xFF; ffound = TRUE; break; } if (!ffound) { VOX_LRUInit( &g_SentenceGroups[isentenceg] ); } else { Q_snprintf( szfound, strLen, "!%s%d", szgroupname, ipick ); return ipick; } } return -1; } struct filelist_t { const char *pFileName; filelist_t *pNext; }; static filelist_t *g_pSentenceFileList = NULL; //----------------------------------------------------------------------------- // Purpose: clear / reinitialize the vox list //----------------------------------------------------------------------------- void VOX_ListClear( void ) { filelist_t *pList, *pNext; pList = g_pSentenceFileList; while ( pList ) { pNext = pList->pNext; free( pList ); pList = pNext; } g_pSentenceFileList = NULL; } //----------------------------------------------------------------------------- // Purpose: Check to see if this file is in the list // Input : *psentenceFileName - // Output : int, true if the file is in the list, false if not //----------------------------------------------------------------------------- int VOX_ListFileIsLoaded( const char *psentenceFileName ) { filelist_t *pList = g_pSentenceFileList; while ( pList ) { if ( !strcmp( psentenceFileName, pList->pFileName ) ) return true; pList = pList->pNext; } return false; } //----------------------------------------------------------------------------- // Purpose: Add this file name to the sentence list // Input : *psentenceFileName - //----------------------------------------------------------------------------- void VOX_ListMarkFileLoaded( const char *psentenceFileName ) { filelist_t *pEntry; char *pName; pEntry = (filelist_t *)malloc( sizeof(filelist_t) + strlen( psentenceFileName ) + 1); if ( pEntry ) { pName = (char *)(pEntry+1); Q_strcpy( pName, psentenceFileName ); pEntry->pFileName = pName; pEntry->pNext = g_pSentenceFileList; g_pSentenceFileList = pEntry; } } // This creates a compact copy of the sentence file in memory with only the necessary data void VOX_CompactSentenceFile() { int totalMem = 0; int i; for ( i = 0; i < g_Sentences.Count(); i++ ) { int len = Q_strlen( g_Sentences[i].pName ) + 1; const char *pData = g_Sentences[i].pName + len; int dataLen = Q_strlen( pData ) + 1; totalMem += len + dataLen; } g_SentenceFile.EnsureCount( totalMem ); totalMem = 0; for ( i = 0; i < g_Sentences.Count(); i++ ) { int len = Q_strlen( g_Sentences[i].pName ) + 1; const char *pData = g_Sentences[i].pName + len; int dataLen = Q_strlen( pData ) + 1; char *pDest = &g_SentenceFile[totalMem]; memcpy( pDest, g_Sentences[i].pName, len + dataLen ); g_Sentences[i].pName = pDest; totalMem += len + dataLen; } } // Load sentence file into memory, insert null terminators to // delimit sentence name/sentence pairs. Keep pointer to each // sentence name so we can search later. void VOX_ReadSentenceFile( const char *psentenceFileName ) { char *pch; byte *pFileData; int fileSize; char c; char *pchlast, *pSentenceData; characterset_t whitespace; // Have we already loaded this file? if ( VOX_ListFileIsLoaded( psentenceFileName ) ) { // must touch any sentence wavs again to ensure the map's init path gets the results if ( MapReslistGenerator().IsLoggingToMap() ) { VOX_AddSentenceWavesToResList(); } return; } // load file FileHandle_t file; file = g_pFileSystem->Open( psentenceFileName, "rb" ); if ( FILESYSTEM_INVALID_HANDLE == file ) { DevMsg ("Couldn't load %s\n", psentenceFileName); return; } fileSize = g_pFileSystem->Size( file ); if ( fileSize <= 0 ) { DevMsg ("VOX_ReadSentenceFile: %s has invalid size %i\n", psentenceFileName, fileSize ); g_pFileSystem->Close( file ); return; } pFileData = (byte *)g_pFileSystem->AllocOptimalReadBuffer( file, fileSize + 1 ); if ( !pFileData ) { DevMsg ("VOX_ReadSentenceFile: %s couldn't allocate %i bytes for data\n", psentenceFileName, fileSize ); g_pFileSystem->Close( file ); return; } // Read the data and close the file g_pFileSystem->ReadEx( pFileData, g_pFileSystem->GetOptimalReadSize( file, fileSize ), fileSize, file ); g_pFileSystem->Close( file ); // Make sure we end with a null terminator pFileData[ fileSize ] = 0; pch = (char *)pFileData; pchlast = pch + fileSize; CharacterSetBuild( &whitespace, "\n\r\t " ); const char *pName = 0; while (pch < pchlast) { // Only process this pass on sentences pSentenceData = NULL; // skip newline, cr, tab, space c = *pch; while (pch < pchlast && IN_CHARACTERSET( whitespace, c )) c = *(++pch); // YWB: Fix possible crashes reading past end of file if the last line has only whitespace on it... if ( !*pch ) break; // skip entire line if first char is / if (*pch != '/') { int addIndex = g_Sentences.AddToTail(); sentence_t *pSentence = &g_Sentences[addIndex]; pName = pch; pSentence->pName = pch; pSentence->length = 0; pSentence->closecaption = false; pSentence->isPrecached = false; pSentence->caption = UTL_INVAL_SYMBOL; // scan forward to first space, insert null terminator // after sentence name c = *pch; while (pch < pchlast && c != ' ') c = *(++pch); if (pch < pchlast) *pch++ = 0; // A sentence may have some line commands, make an extra pass pSentenceData = pch; } // scan forward to end of sentence or eof while (pch < pchlast && pch[0] != '\n' && pch[0] != '\r') pch++; // insert null terminator if (pch < pchlast) *pch++ = 0; // If we have some sentence data, parse out any line commands if ( pSentenceData && pSentenceData < pchlast ) { // Add a new group or increment count of the existing one VOX_GroupAdd( pName ); int index = g_Sentences.Size()-1; // The current sentence has an index of count-1 VOX_ParseLineCommands( pSentenceData, index ); } } // now compact the file data in memory VOX_CompactSentenceFile(); g_pFileSystem->FreeOptimalReadBuffer( pFileData ); VOX_GroupInitAllLRUs(); // This only does stuff during reslist generation... VOX_AddSentenceWavesToResList(); VOX_ListMarkFileLoaded( psentenceFileName ); } //----------------------------------------------------------------------------- // Purpose: Get the current number of sentences in the database // Output : int //----------------------------------------------------------------------------- int VOX_SentenceCount( void ) { return g_Sentences.Size(); } float VOX_SentenceLength( int sentence_num ) { if ( sentence_num < 0 || sentence_num > g_Sentences.Size()-1 ) return 0.0f; return g_Sentences[ sentence_num ].length; } // scan g_Sentences, looking for pszin sentence name // return pointer to sentence data if found, null if not // CONSIDER: if we have a large number of sentences, should // CONSIDER: sort strings in g_Sentences and do binary search. char *VOX_LookupString(const char *pSentenceName, int *psentencenum, bool *pbEmitCaption /*=NULL*/, CUtlSymbol *pCaptionSymbol /*=NULL*/, float *pflDuration /*= NULL*/ ) { if ( pbEmitCaption ) { *pbEmitCaption = false; } if ( pCaptionSymbol ) { *pCaptionSymbol = UTL_INVAL_SYMBOL; } if ( pflDuration ) { *pflDuration = 0.0f; } int i; int c = g_Sentences.Size(); for (i = 0; i < c; i++) { char const *name = g_Sentences[i].pName; if (!stricmp(pSentenceName, name)) { if (psentencenum) { *psentencenum = i; } if ( pbEmitCaption ) { *pbEmitCaption = g_Sentences[ i ].closecaption; } if ( pCaptionSymbol ) { *pCaptionSymbol = g_Sentences[ i ].caption; } if ( pflDuration ) { *pflDuration = g_Sentences[ i ].length; } return (char *)(name + Q_strlen(name) + 1); } } return NULL; } // Abstraction for sentence name array const char *VOX_SentenceNameFromIndex( int sentencenum ) { if ( sentencenum < g_Sentences.Size() ) return g_Sentences[sentencenum].pName; return NULL; }