mirror of
https://github.com/YGGverse/xash3d-fwgs.git
synced 2025-01-09 14:47:59 +00:00
5e0a0765ce
The `.editorconfig` file in this repo is configured to trim all trailing whitespace regardless of whether the line is modified. Trims all trailing whitespace in the repository to make the codebase easier to work with in editors that respect `.editorconfig`. `git blame` becomes less useful on these lines but it already isn't very useful. Commands: ``` find . -type f -name '*.h' -exec sed --in-place 's/[[:space:]]\+$//' {} \+ find . -type f -name '*.c' -exec sed --in-place 's/[[:space:]]\+$//' {} \+ ```
692 lines
16 KiB
C
692 lines
16 KiB
C
/*
|
|
s_vox.c - npc sentences
|
|
Copyright (C) 2010 Uncle Mike
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "sound.h"
|
|
#include "const.h"
|
|
#include <ctype.h>
|
|
|
|
sentence_t g_Sentences[MAX_SENTENCES];
|
|
static uint g_numSentences;
|
|
static char *rgpparseword[CVOXWORDMAX]; // array of pointers to parsed words
|
|
static char voxperiod[] = "_period"; // vocal pause
|
|
static char voxcomma[] = "_comma"; // vocal pause
|
|
|
|
static int IsNextWord( const char c )
|
|
{
|
|
if( c == '.' || c == ',' || c == ' ' || c == '(' )
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int IsSkipSpace( const char c )
|
|
{
|
|
if( c == ',' || c == '.' || c == ' ' )
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int IsWhiteSpace( const char space )
|
|
{
|
|
if( space == ' ' || space == '\t' || space == '\r' || space == '\n' )
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int IsCommandChar( const char c )
|
|
{
|
|
if( c == 'v' || c == 'p' || c == 's' || c == 'e' || c == 't' )
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int IsDelimitChar( const char c )
|
|
{
|
|
if( c == '(' || c == ')' )
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static char *ScanForwardUntil( char *string, const char scan )
|
|
{
|
|
while( string[0] )
|
|
{
|
|
if( string[0] == scan )
|
|
return string;
|
|
string++;
|
|
}
|
|
return string;
|
|
}
|
|
|
|
// backwards scan psz for last '/'
|
|
// return substring in szpath null terminated
|
|
// if '/' not found, return 'vox/'
|
|
static char *VOX_GetDirectory( char *szpath, char *psz )
|
|
{
|
|
char c;
|
|
int cb = 0, len;
|
|
char *p;
|
|
|
|
len = Q_strlen( psz );
|
|
p = psz + len - 1;
|
|
|
|
// scan backwards until first '/' or start of string
|
|
c = *p;
|
|
while( p > psz && c != '/' )
|
|
{
|
|
c = *( --p );
|
|
cb++;
|
|
}
|
|
|
|
if( c != '/' )
|
|
{
|
|
// didn't find '/', return default directory
|
|
Q_strcpy( szpath, "vox/" );
|
|
return psz;
|
|
}
|
|
|
|
cb = len - cb;
|
|
memcpy( szpath, psz, cb );
|
|
szpath[cb] = 0;
|
|
|
|
return p + 1;
|
|
}
|
|
|
|
// 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 )
|
|
{
|
|
int i;
|
|
|
|
if( Q_isdigit( pSentenceName ) && (i = Q_atoi( pSentenceName )) < g_numSentences )
|
|
{
|
|
if( psentencenum ) *psentencenum = i;
|
|
return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 );
|
|
}
|
|
|
|
for( i = 0; i < g_numSentences; i++ )
|
|
{
|
|
if( !Q_stricmp( pSentenceName, g_Sentences[i].pName ))
|
|
{
|
|
if( psentencenum ) *psentencenum = i;
|
|
return (g_Sentences[i].pName + Q_strlen( g_Sentences[i].pName ) + 1 );
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// 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, fdone = 0;
|
|
char c, *p = psz;
|
|
|
|
memset( rgpparseword, 0, sizeof( char* ) * CVOXWORDMAX );
|
|
|
|
if( !psz ) return NULL;
|
|
|
|
i = 0;
|
|
rgpparseword[i++] = psz;
|
|
|
|
while( !fdone && i < CVOXWORDMAX )
|
|
{
|
|
// scan up to next word
|
|
c = *p;
|
|
while( c && !IsNextWord( c ))
|
|
c = *(++p);
|
|
|
|
// if '(' then scan for matching ')'
|
|
if( c == '(' )
|
|
{
|
|
p = ScanForwardUntil( p, ')' );
|
|
c = *(++p);
|
|
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 == ',' ) && *(p+1) != '\n' && *(p+1) != '\r' && *(p+1) != 0 )
|
|
{
|
|
if( c == '.' ) rgpparseword[i++] = voxperiod;
|
|
else rgpparseword[i++] = voxcomma;
|
|
|
|
if( i >= CVOXWORDMAX )
|
|
break;
|
|
}
|
|
|
|
// null terminate substring
|
|
*p++ = 0;
|
|
|
|
// skip whitespace
|
|
c = *p;
|
|
while( c && IsSkipSpace( c ))
|
|
c = *(++p);
|
|
|
|
if( !c ) fdone = 1;
|
|
else rgpparseword[i++] = p;
|
|
}
|
|
}
|
|
|
|
return rgpparseword;
|
|
}
|
|
|
|
float VOX_GetVolumeScale( channel_t *pchan )
|
|
{
|
|
if( pchan->currentWord )
|
|
{
|
|
if ( pchan->words[pchan->wordIndex].volume )
|
|
{
|
|
float volume = pchan->words[pchan->wordIndex].volume * 0.01f;
|
|
if( volume < 1.0f ) return volume;
|
|
}
|
|
}
|
|
|
|
return 1.0f;
|
|
}
|
|
|
|
void VOX_SetChanVol( channel_t *ch )
|
|
{
|
|
float scale;
|
|
|
|
if( !ch->currentWord )
|
|
return;
|
|
|
|
scale = VOX_GetVolumeScale( ch );
|
|
if( scale == 1.0f ) return;
|
|
|
|
ch->rightvol = (int)(ch->rightvol * scale);
|
|
ch->leftvol = (int)(ch->leftvol * scale);
|
|
}
|
|
|
|
float VOX_ModifyPitch( channel_t *ch, float pitch )
|
|
{
|
|
if( ch->currentWord )
|
|
{
|
|
if( ch->words[ch->wordIndex].pitch > 0 )
|
|
{
|
|
pitch += ( ch->words[ch->wordIndex].pitch - PITCH_NORM ) * 0.01f;
|
|
}
|
|
}
|
|
|
|
return pitch;
|
|
}
|
|
|
|
//===============================================================================
|
|
// 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, ct, sznum[8];
|
|
static voxword_t voxwordDefault;
|
|
int i;
|
|
|
|
// 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 + Q_strlen( psz ) - 1 );
|
|
|
|
// no formatting, return
|
|
if( c != ')' ) return 1;
|
|
|
|
// scan forward to first '('
|
|
c = *psz;
|
|
while( !IsDelimitChar( c ))
|
|
c = *(++psz);
|
|
|
|
// bogus formatting
|
|
if( c == ')' ) return 0;
|
|
|
|
// null terminate
|
|
*psz = 0;
|
|
ct = *(++psz);
|
|
|
|
while( 1 )
|
|
{
|
|
// scan until we hit a character in the commandSet
|
|
while( ct && !IsCommandChar( ct ))
|
|
ct = *(++psz);
|
|
|
|
if( ct == ')' )
|
|
break;
|
|
|
|
memset( sznum, 0, sizeof( sznum ));
|
|
i = 0;
|
|
|
|
c = *(++psz);
|
|
|
|
if( !isdigit( c ))
|
|
break;
|
|
|
|
// read number
|
|
while( isdigit( c ) && i < sizeof( sznum ) - 1 )
|
|
{
|
|
sznum[i++] = c;
|
|
c = *(++psz);
|
|
}
|
|
|
|
// get value of number
|
|
i = Q_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( Q_strlen( pszsave ) == 0 )
|
|
{
|
|
voxwordDefault = *pvoxword;
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
void VOX_LoadWord( channel_t *pchan )
|
|
{
|
|
if( pchan->words[pchan->wordIndex].sfx )
|
|
{
|
|
wavdata_t *pSource = S_LoadSound( pchan->words[pchan->wordIndex].sfx );
|
|
|
|
if( pSource )
|
|
{
|
|
int start = pchan->words[pchan->wordIndex].start;
|
|
int end = pchan->words[pchan->wordIndex].end;
|
|
|
|
// apply mixer
|
|
pchan->currentWord = &pchan->pMixer;
|
|
pchan->currentWord->pData = pSource;
|
|
|
|
// don't allow overlapped ranges
|
|
if( end <= start ) end = 0;
|
|
|
|
if( start || end )
|
|
{
|
|
int sampleCount = pSource->samples;
|
|
|
|
if( start )
|
|
{
|
|
S_SetSampleStart( pchan, pSource, (int)(sampleCount * 0.01f * start));
|
|
}
|
|
|
|
if( end )
|
|
{
|
|
S_SetSampleEnd( pchan, pSource, (int)(sampleCount * 0.01f * end));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void VOX_FreeWord( channel_t *pchan )
|
|
{
|
|
pchan->currentWord = NULL; // sentence is finished
|
|
memset( &pchan->pMixer, 0, sizeof( pchan->pMixer ));
|
|
|
|
// release unused sounds
|
|
if( pchan->words[pchan->wordIndex].sfx )
|
|
{
|
|
// If this wave wasn't precached by the game code
|
|
if( !pchan->words[pchan->wordIndex].fKeepCached )
|
|
{
|
|
FS_FreeSound( pchan->words[pchan->wordIndex].sfx->cache );
|
|
pchan->words[pchan->wordIndex].sfx->cache = NULL;
|
|
pchan->words[pchan->wordIndex].sfx = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
void VOX_LoadFirstWord( channel_t *pchan, voxword_t *pwords )
|
|
{
|
|
int i = 0;
|
|
|
|
// copy each pointer in the sfx temp array into the
|
|
// sentence array, and set the channel to point to the
|
|
// sentence array
|
|
while( pwords[i].sfx != NULL )
|
|
{
|
|
pchan->words[i] = pwords[i];
|
|
i++;
|
|
}
|
|
pchan->words[i].sfx = NULL;
|
|
|
|
pchan->wordIndex = 0;
|
|
VOX_LoadWord( pchan );
|
|
}
|
|
|
|
// return number of samples mixed
|
|
int VOX_MixDataToDevice( channel_t *pchan, int sampleCount, int outputRate, int outputOffset )
|
|
{
|
|
// save this to compute total output
|
|
int startingOffset = outputOffset;
|
|
|
|
if( !pchan->currentWord )
|
|
return 0;
|
|
|
|
while( sampleCount > 0 && pchan->currentWord )
|
|
{
|
|
int timeCompress = pchan->words[pchan->wordIndex].timecompress;
|
|
int outputCount = S_MixDataToDevice( pchan, sampleCount, outputRate, outputOffset, timeCompress );
|
|
|
|
outputOffset += outputCount;
|
|
sampleCount -= outputCount;
|
|
|
|
// if we finished load a next word
|
|
if( pchan->currentWord->finished )
|
|
{
|
|
VOX_FreeWord( pchan );
|
|
pchan->wordIndex++;
|
|
VOX_LoadWord( pchan );
|
|
|
|
if( pchan->currentWord )
|
|
{
|
|
pchan->sfx = pchan->words[pchan->wordIndex].sfx;
|
|
}
|
|
}
|
|
}
|
|
return outputOffset - startingOffset;
|
|
}
|
|
|
|
// link all sounds in sentence, start playing first word.
|
|
void VOX_LoadSound( channel_t *pchan, const char *pszin )
|
|
{
|
|
char buffer[512];
|
|
int i, cword;
|
|
char pathbuffer[64];
|
|
char szpath[32];
|
|
voxword_t rgvoxword[CVOXWORDMAX];
|
|
char *psz;
|
|
|
|
if( !pszin || !*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 )
|
|
{
|
|
Con_DPrintf( S_ERROR "VOX_LoadSound: no such sentence %s\n", pszin );
|
|
return;
|
|
}
|
|
|
|
// get directory from string, advance psz
|
|
psz = VOX_GetDirectory( szpath, psz );
|
|
|
|
if( Q_strlen( psz ) > sizeof( buffer ) - 1 )
|
|
{
|
|
Con_Printf( S_ERROR "VOX_LoadSound: sentence is too long %s\n", psz );
|
|
return;
|
|
}
|
|
|
|
// copy into buffer
|
|
Q_strcpy( buffer, psz );
|
|
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;
|
|
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_strcpy( pathbuffer, szpath );
|
|
Q_strncat( pathbuffer, rgpparseword[i], sizeof( pathbuffer ));
|
|
Q_strncat( pathbuffer, ".wav", sizeof( pathbuffer ));
|
|
|
|
// 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 ));
|
|
cword++;
|
|
}
|
|
i++;
|
|
}
|
|
|
|
VOX_LoadFirstWord( pchan, rgvoxword );
|
|
|
|
pchan->isSentence = true;
|
|
pchan->sfx = rgvoxword[0].sfx;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// 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 ))
|
|
{
|
|
Con_Printf( S_ERROR "Sentence too long (max length %lu characters)\n", sizeof(tempBuffer) - 1 );
|
|
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++;
|
|
|
|
// skip whitespace
|
|
while( *pSentenceData && *pSentenceData <= 32 )
|
|
pSentenceData++;
|
|
|
|
// simple comparison of string commands:
|
|
switch( Q_tolower( *pSentenceData ))
|
|
{
|
|
case 'l':
|
|
// all commands starting with the letter 'l' here
|
|
if( !Q_strnicmp( pSentenceData, "len", 3 ))
|
|
{
|
|
g_Sentences[sentenceIndex].length = Q_atof( pSentenceData + 3 );
|
|
}
|
|
break;
|
|
case 0:
|
|
default:
|
|
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 );
|
|
}
|
|
}
|
|
|
|
// 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 c, *pch, *pFileData;
|
|
char *pchlast, *pSentenceData;
|
|
fs_offset_t fileSize;
|
|
|
|
// load file
|
|
pFileData = (char *)FS_LoadFile( psentenceFileName, &fileSize, false );
|
|
if( !pFileData ) return; // this game just doesn't used vox sound system
|
|
|
|
pch = pFileData;
|
|
pchlast = pch + fileSize;
|
|
|
|
while( pch < pchlast )
|
|
{
|
|
if( g_numSentences >= MAX_SENTENCES )
|
|
{
|
|
Con_Printf( S_ERROR "VOX_Init: too many sentences specified, max is %d\n", MAX_SENTENCES );
|
|
break;
|
|
}
|
|
|
|
// only process this pass on sentences
|
|
pSentenceData = NULL;
|
|
|
|
// skip newline, cr, tab, space
|
|
|
|
c = *pch;
|
|
while( pch < pchlast && IsWhiteSpace( c ))
|
|
c = *(++pch);
|
|
|
|
// skip entire line if first char is /
|
|
if( *pch != '/' )
|
|
{
|
|
sentence_t *pSentence = &g_Sentences[g_numSentences++];
|
|
|
|
pSentence->pName = pch;
|
|
pSentence->length = 0;
|
|
|
|
// 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 )
|
|
{
|
|
int index = g_numSentences - 1;
|
|
|
|
// the current sentence has an index of count-1
|
|
VOX_ParseLineCommands( pSentenceData, index );
|
|
}
|
|
}
|
|
}
|
|
|
|
void VOX_Init( void )
|
|
{
|
|
memset( g_Sentences, 0, sizeof( g_Sentences ));
|
|
g_numSentences = 0;
|
|
|
|
VOX_ReadSentenceFile( DEFAULT_SOUNDPATH "sentences.txt" );
|
|
}
|
|
|
|
|
|
void VOX_Shutdown( void )
|
|
{
|
|
g_numSentences = 0;
|
|
}
|