Files
xash3d-fwgs/engine/client/titles.c

445 lines
10 KiB
C

/*
titles.c - implementation of titles.txt parser
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 "client.h"
#include "tests.h"
#define MAX_MESSAGES 2048
#define MSGFILE_NAME 0
#define MSGFILE_TEXT 1
client_textmessage_t gMessageParms;
// the string "pText" is assumed to have all whitespace from both ends cut out
static int IsComment( const char *pText )
{
if( pText )
{
int length = Q_strlen( pText );
if( length >= 2 && pText[0] == '/' && pText[1] == '/' )
return 1;
// no text?
if( length > 0 )
return 0;
}
// no text is a comment too
return 1;
}
// the string "pText" is assumed to have all whitespace from both ends cut out
static int IsStartOfText( const char *pText )
{
if( pText )
{
if( pText[0] == '{' )
return 1;
}
return 0;
}
// the string "pText" is assumed to have all whitespace from both ends cut out
static int IsEndOfText( const char *pText )
{
if( pText )
{
if( pText[0] == '}' )
return 1;
}
return 0;
}
static int IsWhiteSpace( char space )
{
if( space == ' ' || space == '\t' || space == '\r' || space == '\n' )
return 1;
return 0;
}
static const char *SkipSpace( const char *pText )
{
if( pText )
{
int pos = 0;
while( pText[pos] && IsWhiteSpace( pText[pos] ))
pos++;
return pText + pos;
}
return NULL;
}
static const char *SkipText( const char *pText )
{
if( pText )
{
int pos = 0;
while( pText[pos] && !IsWhiteSpace( pText[pos] ))
pos++;
return pText + pos;
}
return NULL;
}
static int ParseFloats( const char *pText, float *pFloat, int count )
{
const char *pTemp = pText;
int index = 0;
while( pTemp && count > 0 )
{
// skip current token / float
pTemp = SkipText( pTemp );
// skip any whitespace in between
pTemp = SkipSpace( pTemp );
if( pTemp )
{
// parse a float
pFloat[index] = Q_atof( pTemp );
count--;
index++;
}
}
if( count == 0 )
return 1;
return 0;
}
static int IsToken( const char *pText, const char *pTokenName )
{
if( !pText || !pTokenName )
return 0;
if( !Q_strnicmp( pText+1, pTokenName, Q_strlen( pTokenName )))
return 1;
return 0;
}
static int ParseDirective( const char *pText )
{
if( pText && pText[0] == '$' )
{
float tempFloat[8];
if( IsToken( pText, "position" ))
{
if( ParseFloats( pText, tempFloat, 2 ))
{
gMessageParms.x = tempFloat[0];
gMessageParms.y = tempFloat[1];
}
}
else if( IsToken( pText, "effect" ))
{
if( ParseFloats( pText, tempFloat, 1 ))
{
gMessageParms.effect = (int)tempFloat[0];
}
}
else if( IsToken( pText, "fxtime" ))
{
if( ParseFloats( pText, tempFloat, 1 ))
{
gMessageParms.fxtime = tempFloat[0];
}
}
else if( IsToken( pText, "color2" ))
{
if( ParseFloats( pText, tempFloat, 3 ))
{
gMessageParms.r2 = (int)tempFloat[0];
gMessageParms.g2 = (int)tempFloat[1];
gMessageParms.b2 = (int)tempFloat[2];
}
}
else if( IsToken( pText, "color" ))
{
if( ParseFloats( pText, tempFloat, 3 ))
{
gMessageParms.r1 = (int)tempFloat[0];
gMessageParms.g1 = (int)tempFloat[1];
gMessageParms.b1 = (int)tempFloat[2];
}
}
else if( IsToken( pText, "fadein" ))
{
if( ParseFloats( pText, tempFloat, 1 ))
{
gMessageParms.fadein = tempFloat[0];
}
}
else if( IsToken( pText, "fadeout" ))
{
if( ParseFloats( pText, tempFloat, 3 ))
{
gMessageParms.fadeout = tempFloat[0];
}
}
else if( IsToken( pText, "holdtime" ))
{
if( ParseFloats( pText, tempFloat, 3 ))
{
gMessageParms.holdtime = tempFloat[0];
}
}
else
{
Con_DPrintf( S_ERROR "unknown token: %s\n", pText );
}
return 1;
}
return 0;
}
client_textmessage_t *CL_TextMessageParse( poolhandle_t mempool, byte *pMemFile, int fileSize, int *numTitles )
{
char buf[512], trim[512], currentName[512];
char *pCurrentText = NULL, *pNameHeap;
char nameHeap[32768]; // g-cont. i will scale up heap to handle all TFC messages
int mode = MSGFILE_NAME; // searching for a message name
int lineNumber, filePos, lastLinePos;
client_textmessage_t textMessages[MAX_MESSAGES];
int i, nameHeapSize, textHeapSize, messageSize, nameOffset;
int messageCount, lastNamePos;
size_t textHeapSizeRemaining;
lastNamePos = 0;
lineNumber = 0;
filePos = 0;
lastLinePos = 0;
messageCount = 0;
while( Q_memfgets( pMemFile, fileSize, &filePos, buf, 512 ) != NULL )
{
COM_TrimSpace( trim, buf, sizeof( trim ));
switch( mode )
{
case MSGFILE_NAME:
// skip comment lines
if( IsComment( trim ))
break;
// Is this a directive "$command"?, if so parse it and break
if( ParseDirective( trim ))
break;
if( IsStartOfText( trim ))
{
mode = MSGFILE_TEXT;
pCurrentText = (char*)(pMemFile + filePos);
break;
}
if( IsEndOfText( trim ))
{
Con_Reportf( "%s: unexpected '}' found, line %d\n", __func__, lineNumber );
return NULL;
}
Q_strncpy( currentName, trim, sizeof( currentName ));
break;
case MSGFILE_TEXT:
if( IsEndOfText( trim ))
{
int length = Q_strlen( currentName );
// save name on name heap
if( lastNamePos + length > 32768 )
{
Con_Reportf( "%s: error while parsing!\n", __func__ );
return NULL;
}
Q_strncpy( nameHeap + lastNamePos, currentName, sizeof( nameHeap ) - lastNamePos );
// terminate text in-place in the memory file
// (it's temporary memory that will be deleted)
pMemFile[lastLinePos-1] = 0;
// Save name/text on heap
textMessages[messageCount] = gMessageParms;
textMessages[messageCount].pName = nameHeap + lastNamePos;
lastNamePos += length + 1;
textMessages[messageCount].pMessage = pCurrentText;
messageCount++;
// reset parser to search for names
mode = MSGFILE_NAME;
break;
}
if( IsStartOfText( trim ))
{
Con_Reportf( "%s: unexpected '{' found, line %d\n", __func__, lineNumber );
return NULL;
}
break;
}
lineNumber++;
lastLinePos = filePos;
if( messageCount >= MAX_MESSAGES )
{
Con_Printf( S_WARN "Too many messages in titles.txt, max is %d\n", MAX_MESSAGES );
break;
}
}
Con_Reportf( "%s: parsed %d text messages\n", __func__, messageCount );
nameHeapSize = lastNamePos;
textHeapSize = 0;
for( i = 0; i < messageCount; i++ )
textHeapSize += Q_strlen( textMessages[i].pMessage ) + 1;
messageSize = ( messageCount * sizeof( client_textmessage_t ));
if(( textHeapSize + nameHeapSize + messageSize ) <= 0 )
{
*numTitles = 0;
return NULL;
}
// must malloc because we need to be able to clear it after initialization
client_textmessage_t *out = Mem_Calloc( mempool, textHeapSize + nameHeapSize + messageSize );
// copy table over
memcpy( out, textMessages, messageSize );
// copy Name heap
pNameHeap = ((char *)out) + messageSize;
memcpy( pNameHeap, nameHeap, nameHeapSize );
// copy text & fixup pointers
textHeapSizeRemaining = textHeapSize;
pCurrentText = pNameHeap + nameHeapSize;
for( i = 0; i < messageCount; i++ )
{
size_t currentTextSize = Q_strlen( out[i].pMessage ) + 1;
out[i].pName = pNameHeap; // adjust name pointer (parallel buffer)
Q_strncpy( pCurrentText, out[i].pMessage, textHeapSizeRemaining ); // copy text over
out[i].pMessage = pCurrentText;
pNameHeap += Q_strlen( pNameHeap ) + 1;
pCurrentText += currentTextSize;
textHeapSizeRemaining -= currentTextSize;
}
if(( pCurrentText - (char *)out ) != ( textHeapSize + nameHeapSize + messageSize ))
Con_DPrintf( S_ERROR "%s: overflow text message buffer!\n", __func__ );
*numTitles = messageCount;
return out;
}
#if XASH_ENGINE_TESTS
void Test_RunTitles( void )
{
poolhandle_t mempool = Mem_AllocPool( __func__ );
client_textmessage_t *tmessages;
client_textmessage_t *null;
int num_titles = 0, num_null_titles = 0;
char titles[] =
"// this is a comment\n"
"$effect 2\n"
"$color 1 2 3\n"
"$color2 4 5 6\n"
"$position 7 8\n"
"$fadein 0.1\n"
"$fadeout 0.5\n"
"$holdtime 321\n"
"$fxtime 123\n"
"TITLE\n"
"{\n"
"Hello to anybody reading test data\n"
"Hope you have a good time\n"
"}\n"
"\n"
"TITLE2\n"
"{\n"
"Still reading that nonsense huh?\n"
"}\n"
"// let's override some things\n"
"$cats are\n"
"$cute\n"
"$color 10 10 10\n"
"$position -1 -1\n"
"UwU\n"
"{\n"
"OwO\n"
"}\n"
"Technically titles can have spaces and // can't have comments\n"
"{\n"
"Oh yeah!\n"
"}\n";
char broken_titles[] = "}\n";
char broken_titles2[] = "{\n{\n";
null = CL_TextMessageParse( mempool, broken_titles, Q_strlen( broken_titles ), &num_null_titles );
TASSERT_EQi( num_null_titles, 0 );
TASSERT_EQp( null, NULL );
null = CL_TextMessageParse( mempool, broken_titles2, Q_strlen( broken_titles2 ), &num_null_titles );
TASSERT_EQi( num_null_titles, 0 );
TASSERT_EQp( null, NULL );
tmessages = CL_TextMessageParse( mempool, titles, Q_strlen( titles ), &num_titles );
TASSERT_EQi( num_titles, 4 );
TASSERT_NEQp( tmessages, NULL );
for( int i = 0; i < 4; i++ )
{
TASSERT_EQi( tmessages[i].effect, 2 );
TASSERT_EQi( tmessages[i].r1, i >= 2 ? 10 : 1 );
TASSERT_EQi( tmessages[i].g1, i >= 2 ? 10 : 2 );
TASSERT_EQi( tmessages[i].b1, i >= 2 ? 10 : 3 );
TASSERT_EQi( tmessages[i].a1, 0 );
TASSERT_EQi( tmessages[i].r2, 4 );
TASSERT_EQi( tmessages[i].g2, 5 );
TASSERT_EQi( tmessages[i].b2, 6 );
TASSERT_EQi( tmessages[i].a2, 0 );
TASSERT_EQi( tmessages[i].x, i >= 2 ? -1 : 7 );
TASSERT_EQi( tmessages[i].y, i >= 2 ? -1 : 8 );
TASSERT_EQi( tmessages[i].fadein, 0.1f );
TASSERT_EQi( tmessages[i].fadeout, 0.5f );
TASSERT_EQi( tmessages[i].holdtime, 321.f );
TASSERT_EQi( tmessages[i].fxtime, 123.f );
}
TASSERT_STR( tmessages[0].pName, "TITLE" );
TASSERT_STR( tmessages[1].pName, "TITLE2" );
TASSERT_STR( tmessages[2].pName, "UwU" );
TASSERT_STR( tmessages[3].pName, "Technically titles can have spaces and // can't have comments" );
TASSERT_STR( tmessages[0].pMessage, "Hello to anybody reading test data\nHope you have a good time" );
TASSERT_STR( tmessages[1].pMessage, "Still reading that nonsense huh?" );
TASSERT_STR( tmessages[2].pMessage, "OwO" );
TASSERT_STR( tmessages[3].pMessage, "Oh yeah!" );
Mem_Free( tmessages );
Mem_FreePool( &mempool );
}
#endif // XASH_ENGINE_TESTS