//========= Copyright Valve Corporation, All rights reserved. ============// // //============================================================================= #include #include #include #include #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"