source-engine/fgdlib/gamedata.cpp

887 lines
23 KiB
C++
Raw Normal View History

2020-04-22 16:56:21 +00:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
//=============================================================================
#include <windows.h>
#include <tier0/dbg.h>
#include <io.h>
#include <WorldSize.h>
#include "fgdlib/GameData.h"
#include "fgdlib/HelperInfo.h"
#include "KeyValues.h"
#include "filesystem_tools.h"
#include "tier1/strtools.h"
#include "utlmap.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
#pragma warning(disable:4244)
const int MAX_ERRORS = 5;
static GameDataMessageFunc_t g_pMsgFunc = NULL;
//-----------------------------------------------------------------------------
// Sets the function used for emitting error messages while loading gamedata files.
//-----------------------------------------------------------------------------
void GDSetMessageFunc(GameDataMessageFunc_t pFunc)
{
g_pMsgFunc = pFunc;
}
//-----------------------------------------------------------------------------
// Purpose: Fetches the next token from the file.
// Input : tr -
// ppszStore - Destination buffer, one of the following:
// pointer to NULL - token will be placed in an allocated buffer
// pointer to non-NULL buffer - token will be placed in buffer
// ttexpecting -
// pszExpecting -
// Output :
//-----------------------------------------------------------------------------
static bool DoGetToken(TokenReader &tr, char **ppszStore, int nSize, trtoken_t ttexpecting, const char *pszExpecting)
{
trtoken_t ttype;
if (*ppszStore != NULL)
{
// Reads the token into the given buffer.
ttype = tr.NextToken(*ppszStore, nSize);
}
else
{
// Allocates a buffer to hold the token.
ttype = tr.NextTokenDynamic(ppszStore);
}
if (ttype == TOKENSTRINGTOOLONG)
{
GDError(tr, "unterminated string or string too long");
return false;
}
//
// Check for a bad token type.
//
char *pszStore = *ppszStore;
bool bBadTokenType = false;
if ((ttype != ttexpecting) && (ttexpecting != TOKENNONE))
{
//
// If we were expecting a string and got an integer, don't worry about it.
// We can translate from integer to string.
//
if (!((ttexpecting == STRING) && (ttype == INTEGER)))
{
bBadTokenType = true;
}
}
if (bBadTokenType && (pszExpecting == NULL))
{
//
// We didn't get the expected token type but no expected
// string was specified.
//
char *pszTokenName;
switch (ttexpecting)
{
case IDENT:
{
pszTokenName = "identifier";
break;
}
case INTEGER:
{
pszTokenName = "integer";
break;
}
case STRING:
{
pszTokenName = "string";
break;
}
case OPERATOR:
default:
{
pszTokenName = "symbol";
break;
}
}
GDError(tr, "expecting %s", pszTokenName);
return false;
}
else if (bBadTokenType || ((pszExpecting != NULL) && !IsToken(pszStore, pszExpecting)))
{
//
// An expected string was specified, and we got either the wrong type or
// the right type but the wrong string,
//
GDError(tr, "expecting '%s', but found '%s'", pszExpecting, pszStore);
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : tr -
// error -
// Output :
//-----------------------------------------------------------------------------
bool GDError(TokenReader &tr, const char *error, ...)
{
char szBuf[128];
va_list vl;
va_start(vl, error);
vsprintf(szBuf, error, vl);
va_end(vl);
if (g_pMsgFunc)
{
// HACK: should use an enumeration for error level
g_pMsgFunc(1, tr.Error(szBuf));
}
if (tr.GetErrorCount() >= MAX_ERRORS)
{
if (g_pMsgFunc)
{
// HACK: should use an enumeration for error level
g_pMsgFunc(1, " - too many errors; aborting.");
}
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Fetches the next token from the file.
// Input : tr - The token reader object with which to fetch the token.
// pszStore - Buffer in which to place the token, NULL to discard the token.
// ttexpecting - The token type that we are expecting. If this is not TOKENNONE
// and token type read is different, the operation will fail.
// pszExpecting - The token string that we are expecting. If this string
// is not NULL and the token string read is different, the operation will fail.
// Output : Returns TRUE if the operation succeeded, FALSE if there was an error.
// If there was an error, the error will be reported in the message window.
//-----------------------------------------------------------------------------
bool GDGetToken(TokenReader &tr, char *pszStore, int nSize, trtoken_t ttexpecting, const char *pszExpecting)
{
Assert(pszStore != NULL);
if (pszStore != NULL)
{
return DoGetToken(tr, &pszStore, nSize, ttexpecting, pszExpecting);
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Fetches the next token from the file.
// Input : tr - The token reader object with which to fetch the token.
// pszStore - Buffer in which to place the token, NULL to discard the token.
// ttexpecting - The token type that we are expecting. If this is not TOKENNONE
// and token type read is different, the operation will fail.
// pszExpecting - The token string that we are expecting. If this string
// is not NULL and the token string read is different, the operation will fail.
// Output : Returns TRUE if the operation succeeded, FALSE if there was an error.
// If there was an error, the error will be reported in the message window.
//-----------------------------------------------------------------------------
bool GDSkipToken(TokenReader &tr, trtoken_t ttexpecting, const char *pszExpecting)
{
//
// Read the next token into a buffer and discard it.
//
char szDiscardBuf[MAX_TOKEN];
char *pszDiscardBuf = szDiscardBuf;
return DoGetToken(tr, &pszDiscardBuf, sizeof(szDiscardBuf), ttexpecting, pszExpecting);
}
//-----------------------------------------------------------------------------
// Purpose: Fetches the next token from the file, allocating a buffer exactly
// large enough to hold the token.
// Input : tr -
// ppszStore -
// ttexpecting -
// pszExpecting -
// Output :
//-----------------------------------------------------------------------------
bool GDGetTokenDynamic(TokenReader &tr, char **ppszStore, trtoken_t ttexpecting, const char *pszExpecting)
{
if (ppszStore == NULL)
{
return false;
}
*ppszStore = NULL;
return DoGetToken(tr, ppszStore, -1, ttexpecting, pszExpecting);
}
//-----------------------------------------------------------------------------
// Purpose: Constructor.
//-----------------------------------------------------------------------------
GameData::GameData(void)
{
m_nMaxMapCoord = 8192;
m_nMinMapCoord = -8192;
m_InstanceClass = NULL;
}
//-----------------------------------------------------------------------------
// Purpose: Destructor.
//-----------------------------------------------------------------------------
GameData::~GameData(void)
{
ClearData();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void GameData::ClearData(void)
{
// delete classes.
int nCount = m_Classes.Count();
for (int i = 0; i < nCount; i++)
{
GDclass *pm = m_Classes.Element(i);
delete pm;
}
m_Classes.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose: Loads a gamedata (FGD) file into this object.
// Input : pszFilename -
// Output : Returns TRUE on success, FALSE on failure.
//-----------------------------------------------------------------------------
BOOL GameData::Load(const char *pszFilename)
{
TokenReader tr;
if(GetFileAttributes(pszFilename) == 0xffffffff)
return FALSE;
if(!tr.Open(pszFilename))
return FALSE;
trtoken_t ttype;
char szToken[128];
while (1)
{
if (tr.GetErrorCount() >= MAX_ERRORS)
{
break;
}
ttype = tr.NextToken(szToken, sizeof(szToken));
if(ttype == TOKENEOF)
break;
if(ttype != OPERATOR || !IsToken(szToken, "@"))
{
if(!GDError(tr, "expected @"))
return FALSE;
}
// check what kind it is, and parse a new object
if (tr.NextToken(szToken, sizeof(szToken)) != IDENT)
{
if(!GDError(tr, "expected identifier after @"))
return FALSE;
}
if (IsToken(szToken, "baseclass") || IsToken(szToken, "pointclass") || IsToken(szToken, "solidclass") || IsToken(szToken, "keyframeclass") ||
IsToken(szToken, "moveclass") || IsToken(szToken, "npcclass") || IsToken(szToken, "filterclass"))
{
//
// New class.
//
GDclass *pNewClass = new GDclass;
if (!pNewClass->InitFromTokens(tr, this))
{
tr.IgnoreTill(OPERATOR, "@"); // go to next section
delete pNewClass;
}
else
{
if (IsToken(szToken, "baseclass")) // Not directly available to user.
{
pNewClass->SetBaseClass(true);
}
else if (IsToken(szToken, "pointclass")) // Generic point class.
{
pNewClass->SetPointClass(true);
}
else if (IsToken(szToken, "solidclass")) // Tied to solids.
{
pNewClass->SetSolidClass(true);
}
else if (IsToken(szToken, "npcclass")) // NPC class - can be spawned by npc_maker.
{
pNewClass->SetPointClass(true);
pNewClass->SetNPCClass(true);
}
else if (IsToken(szToken, "filterclass")) // Filter class - can be used as a filter
{
pNewClass->SetPointClass(true);
pNewClass->SetFilterClass(true);
}
else if (IsToken(szToken, "moveclass")) // Animating
{
pNewClass->SetMoveClass(true);
pNewClass->SetPointClass(true);
}
else if (IsToken(szToken, "keyframeclass")) // Animation keyframes
{
pNewClass->SetKeyFrameClass(true);
pNewClass->SetPointClass(true);
}
// Check and see if this new class matches an existing one. If so we will override the previous definition.
int nExistingClassIndex = 0;
GDclass *pExistingClass = ClassForName(pNewClass->GetName(), &nExistingClassIndex);
if (NULL != pExistingClass)
{
m_Classes.InsertAfter(nExistingClassIndex, pNewClass);
m_Classes.Remove(nExistingClassIndex);
}
else
{
m_Classes.AddToTail(pNewClass);
}
}
}
else if (IsToken(szToken, "include"))
{
if (GDGetToken(tr, szToken, sizeof(szToken), STRING))
{
// Let's assume it's in the same directory.
char justPath[MAX_PATH], loadFilename[MAX_PATH];
if ( Q_ExtractFilePath( pszFilename, justPath, sizeof( justPath ) ) )
{
Q_snprintf( loadFilename, sizeof( loadFilename ), "%s%s", justPath, szToken );
}
else
{
Q_strncpy( loadFilename, szToken, sizeof( loadFilename ) );
}
// First try our fully specified directory
if (!Load(loadFilename))
{
// Failing that, try our start directory
if (!Load(szToken))
{
GDError(tr, "error including file: %s", szToken);
}
}
}
}
else if (IsToken(szToken, "mapsize"))
{
if (!ParseMapSize(tr))
{
// Error in map size specifier, skip to next @ sign.
tr.IgnoreTill(OPERATOR, "@");
}
}
else if ( IsToken( szToken, "materialexclusion" ) )
{
if ( !LoadFGDMaterialExclusions( tr ) )
{
// FGD exclusions not defined; skip to next @ sign.
tr.IgnoreTill(OPERATOR, "@");
}
}
else if ( IsToken( szToken, "autovisgroup" ) )
{
if ( !LoadFGDAutoVisGroups( tr ) )
{
// FGD AutoVisGroups not defined; skip to next @ sign.
tr.IgnoreTill(OPERATOR, "@");
}
}
else
{
GDError(tr, "unrecognized section name %s", szToken);
tr.IgnoreTill(OPERATOR, "@");
}
}
if (tr.GetErrorCount() > 0)
{
return FALSE;
}
tr.Close();
return TRUE;
}
//-----------------------------------------------------------------------------
// Purpose: Parses the "mapsize" specifier, which should be of the form:
//
// mapsize(min, max)
//
// ex: mapsize(-8192, 8192)
//
// Input : tr -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool GameData::ParseMapSize(TokenReader &tr)
{
if (!GDSkipToken(tr, OPERATOR, "("))
{
return false;
}
char szToken[128];
if (!GDGetToken(tr, szToken, sizeof(szToken), INTEGER))
{
return false;
}
int nMin = atoi(szToken);
if (!GDSkipToken(tr, OPERATOR, ","))
{
return false;
}
if (!GDGetToken(tr, szToken, sizeof(szToken), INTEGER))
{
return false;
}
int nMax = atoi(szToken);
if (nMin != nMax)
{
m_nMinMapCoord = min(nMin, nMax);
m_nMaxMapCoord = max(nMin, nMax);
}
if (!GDSkipToken(tr, OPERATOR, ")"))
{
return false;
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : pszName -
// piIndex -
// Output :
//-----------------------------------------------------------------------------
GDclass *GameData::ClassForName(const char *pszName, int *piIndex)
{
int nCount = m_Classes.Count();
for (int i = 0; i < nCount; i++)
{
GDclass *mp = m_Classes.Element(i);
if(!strcmp(mp->GetName(), pszName))
{
if(piIndex)
piIndex[0] = i;
return mp;
}
}
return NULL;
}
// These are 'standard' keys that every entity uses, but they aren't specified that way in the .fgd
static const char *RequiredKeys[] =
{
"Origin",
"Angles",
NULL
};
//-----------------------------------------------------------------------------
// Purpose: this function will set up the initial class about to be instanced
// Input : pszClassName - the class name of the entity to be instanced
// pszInstancePrefix - the prefix to be used for all name fields
// Origin - the origin offset of the instance
// Angles - the angle rotation of the instance
// Output : if successful, will return the game data class of the class name
//-----------------------------------------------------------------------------
GDclass *GameData::BeginInstanceRemap( const char *pszClassName, const char *pszInstancePrefix, Vector &Origin, QAngle &Angle )
{
m_InstanceOrigin = Origin;
m_InstanceAngle = Angle;
AngleMatrix( m_InstanceAngle, m_InstanceOrigin, m_InstanceMat );
strcpy( m_InstancePrefix, pszInstancePrefix );
if ( m_InstanceClass )
{
delete m_InstanceClass;
m_InstanceClass = NULL;
}
if ( strcmpi( pszClassName, "info_overlay_accessor" ) == 0 )
{ // yucky hack for a made up entity in the bsp process
pszClassName = "info_overlay";
}
GDclass *BaseClass = ClassForName( pszClassName );
if ( BaseClass )
{
m_InstanceClass = new GDclass();
m_InstanceClass->Parent = this;
m_InstanceClass->AddBase( BaseClass );
for( int i = 0; RequiredKeys[ i ]; i++ )
{
if ( m_InstanceClass->VarForName( RequiredKeys[ i ] ) == NULL )
{
BaseClass = ClassForName( RequiredKeys[ i ] );
if ( BaseClass )
{
m_InstanceClass->AddBase( BaseClass );
}
}
}
}
else
{
m_InstanceClass = NULL;
}
return m_InstanceClass;
}
enum tRemapOperation
{
REMAP_NAME = 0,
REMAP_POSITION,
REMAP_ANGLE,
REMAP_ANGLE_NEGATIVE_PITCH,
};
static CUtlMap< GDIV_TYPE, tRemapOperation > RemapOperation;
//-----------------------------------------------------------------------------
// Purpose: function to sort the class type for the RemapOperations map
// Input : type1 - the first type to compare against
// type2 - the second type to compare against
// Output : returns true if the first type is less than the second one
//-----------------------------------------------------------------------------
static bool CUtlType_LessThan( const GDIV_TYPE &type1, const GDIV_TYPE &type2 )
{
return ( type1 < type2 );
}
//-----------------------------------------------------------------------------
// Purpose: this function will attempt to remap a key's value
// Input : pszKey - the name of the key
// pszInvalue - the original value
// AllowNameRemapping - only do name remapping if this parameter is true.
// this is generally only false on the instance level.
// Output : returns true if the value changed
// pszOutValue - the new value if changed
//-----------------------------------------------------------------------------
bool GameData::RemapKeyValue( const char *pszKey, const char *pszInValue, char *pszOutValue, TNameFixup NameFixup )
{
if ( RemapOperation.Count() == 0 )
{
RemapOperation.SetLessFunc( &CUtlType_LessThan );
RemapOperation.Insert( ivAngle, REMAP_ANGLE );
RemapOperation.Insert( ivTargetDest, REMAP_NAME );
RemapOperation.Insert( ivTargetSrc, REMAP_NAME );
RemapOperation.Insert( ivOrigin, REMAP_POSITION );
RemapOperation.Insert( ivAxis, REMAP_ANGLE );
RemapOperation.Insert( ivAngleNegativePitch, REMAP_ANGLE_NEGATIVE_PITCH );
}
if ( !m_InstanceClass )
{
return false;
}
GDinputvariable *KVVar = m_InstanceClass->VarForName( pszKey );
if ( !KVVar )
{
return false;
}
GDIV_TYPE KVType = KVVar->GetType();
int KVRemapIndex = RemapOperation.Find( KVType );
if ( KVRemapIndex == RemapOperation.InvalidIndex() )
{
return false;
}
strcpy( pszOutValue, pszInValue );
switch( RemapOperation[ KVRemapIndex ] )
{
case REMAP_NAME:
if ( KVType != ivInstanceVariable )
{
RemapNameField( pszInValue, pszOutValue, NameFixup );
}
break;
case REMAP_POSITION:
{
Vector inPoint( 0.0f, 0.0f, 0.0f ), outPoint;
sscanf ( pszInValue, "%f %f %f", &inPoint.x, &inPoint.y, &inPoint.z );
VectorTransform( inPoint, m_InstanceMat, outPoint );
sprintf( pszOutValue, "%g %g %g", outPoint.x, outPoint.y, outPoint.z );
}
break;
case REMAP_ANGLE:
if ( m_InstanceAngle.x != 0.0f || m_InstanceAngle.y != 0.0f || m_InstanceAngle.z != 0.0f )
{
QAngle inAngles( 0.0f, 0.0f, 0.0f ), outAngles;
matrix3x4_t angToWorld, localMatrix;
sscanf ( pszInValue, "%f %f %f", &inAngles.x, &inAngles.y, &inAngles.z );
AngleMatrix( inAngles, angToWorld );
MatrixMultiply( m_InstanceMat, angToWorld, localMatrix );
MatrixAngles( localMatrix, outAngles );
sprintf( pszOutValue, "%g %g %g", outAngles.x, outAngles.y, outAngles.z );
}
break;
case REMAP_ANGLE_NEGATIVE_PITCH:
if ( m_InstanceAngle.x != 0.0f || m_InstanceAngle.y != 0.0f || m_InstanceAngle.z != 0.0f )
{
QAngle inAngles( 0.0f, 0.0f, 0.0f ), outAngles;
matrix3x4_t angToWorld, localMatrix;
sscanf ( pszInValue, "%f", &inAngles.x ); // just the pitch
inAngles.x = -inAngles.x;
AngleMatrix( inAngles, angToWorld );
MatrixMultiply( m_InstanceMat, angToWorld, localMatrix );
MatrixAngles( localMatrix, outAngles );
sprintf( pszOutValue, "%g", -outAngles.x ); // just the pitch
}
break;
}
return ( strcmpi( pszInValue, pszOutValue ) != 0 );
}
//-----------------------------------------------------------------------------
// Purpose: this function will attempt to remap a name field.
// Input : pszInvalue - the original value
// AllowNameRemapping - only do name remapping if this parameter is true.
// this is generally only false on the instance level.
// Output : returns true if the value changed
// pszOutValue - the new value if changed
//-----------------------------------------------------------------------------
bool GameData::RemapNameField( const char *pszInValue, char *pszOutValue, TNameFixup NameFixup )
{
strcpy( pszOutValue, pszInValue );
if ( pszInValue[ 0 ] && pszInValue[ 0 ] != '@' )
{ // ! at the start of a value means it is global and should not be remaped
switch( NameFixup )
{
case NAME_FIXUP_PREFIX:
sprintf( pszOutValue, "%s-%s", m_InstancePrefix, pszInValue );
break;
case NAME_FIXUP_POSTFIX:
sprintf( pszOutValue, "%s-%s", pszInValue, m_InstancePrefix );
break;
}
}
return ( strcmpi( pszInValue, pszOutValue ) != 0 );
}
//-----------------------------------------------------------------------------
// Purpose: Gathers any FGD-defined material directory exclusions
// Input :
// Output :
//-----------------------------------------------------------------------------
bool GameData::LoadFGDMaterialExclusions( TokenReader &tr )
{
if ( !GDSkipToken( tr, OPERATOR, "[" ) )
{
return false;
}
while ( 1 )
{
char szToken[128];
bool bMatchFound = false;
if ( tr.PeekTokenType( szToken, sizeof( szToken ) ) == OPERATOR )
{
break;
}
else if ( GDGetToken( tr, szToken, sizeof( szToken ), STRING ) )
{
// Make sure we haven't loaded this from another FGD
for ( int i = 0; i < m_FGDMaterialExclusions.Count(); i++ )
{
if ( !stricmp( szToken, m_FGDMaterialExclusions[i].szDirectory ) )
{
bMatchFound = true;
break;
}
}
// Parse the string
if ( bMatchFound == false )
{
int index = m_FGDMaterialExclusions.AddToTail();
Q_strncpy( m_FGDMaterialExclusions[index].szDirectory, szToken, sizeof( m_FGDMaterialExclusions[index].szDirectory ) );
m_FGDMaterialExclusions[index].bUserGenerated = false;
}
}
}
//
// Closing square brace.
//
if ( !GDSkipToken( tr, OPERATOR, "]" ) )
{
return( FALSE );
}
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Gathers any FGD-defined Auto VisGroups
// Input :
// Output :
//-----------------------------------------------------------------------------
bool GameData::LoadFGDAutoVisGroups( TokenReader &tr )
{
int gindex = 0; // Index of AutoVisGroups
int cindex = 0; // Index of Classes
char szToken[128];
// Handle the Parent -- World Geometry, Entities, World Detail
if ( GDSkipToken( tr, OPERATOR, "=" ) )
{
// We expect a name
if ( !GDGetToken( tr, szToken, sizeof( szToken ), STRING ) )
{
return( FALSE );
}
gindex = m_FGDAutoVisGroups.AddToTail();
Q_strncpy( m_FGDAutoVisGroups[gindex].szParent, szToken, sizeof( m_FGDAutoVisGroups[gindex].szParent ) );
// We expect a Class
if ( !GDSkipToken( tr, OPERATOR, "[" ) )
{
return( FALSE );
}
}
// Handle the Class(es) -- Brush Entities, Occluders, Lights
while ( 1 )
{
if ( GDGetToken( tr, szToken, sizeof( szToken ), STRING ) )
{
cindex = m_FGDAutoVisGroups[gindex].m_Classes.AddToTail();
Q_strncpy( m_FGDAutoVisGroups[gindex].m_Classes[cindex].szClass, szToken, sizeof( m_FGDAutoVisGroups[gindex].m_Classes[cindex].szClass ) );
if ( !GDSkipToken( tr, OPERATOR, "[" ) )
{
return( FALSE );
}
// Parse objects/entities -- func_detail, point_template, light_spot
while ( 1 )
{
if ( tr.PeekTokenType( szToken, sizeof( szToken ) ) == OPERATOR )
{
break;
}
if ( !GDGetToken( tr, szToken, sizeof( szToken ), STRING ) )
{
return( FALSE );
}
m_FGDAutoVisGroups[gindex].m_Classes[cindex].szEntities.CopyAndAddToTail( szToken );
}
if ( !GDSkipToken( tr, OPERATOR, "]" ) )
{
return( FALSE );
}
// See if we have another Class coming up
if ( tr.PeekTokenType( szToken, sizeof( szToken ) ) == STRING )
{
continue;
}
// If no more Classes, we now expect a terminating ']'
if ( !GDSkipToken( tr, OPERATOR, "]" ) )
{
return( FALSE );
}
// We're done
return true;
}
// We don't have another Class; look for a terminating brace
else
{
if ( !GDSkipToken( tr, OPERATOR, "]" ) )
{
return( FALSE );
}
}
}
// Safety net
GDError( tr, "Malformed AutoVisGroup -- Last processed: %s", szToken );
return( FALSE );
}
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgoff.h"