//========= 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;
}