You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2862 lines
68 KiB
2862 lines
68 KiB
//========= 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<sentence_t> 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<unsigned char> g_GroupLRU; |
|
static CUtlVector<char> 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<sentencegroup_t> 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_<type> |
|
// V_RNDNAME - grab a random name string from V_RNDNAME_<type> |
|
|
|
// 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; |
|
} |
|
|
|
|
|
|
|
|