You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
7.8 KiB
264 lines
7.8 KiB
1 year ago
|
/** Defines an "objective specification" object and the classes used to read and load it.
|
||
|
|
||
|
*/
|
||
|
|
||
|
#include "convar.h"
|
||
|
#include "asw_mission_text_database.h"
|
||
|
#include "KeyValues.h"
|
||
|
#include "filesystem.h"
|
||
|
#include "vstdlib/random.h"
|
||
|
#include "vprof.h"
|
||
|
|
||
|
// memdbgon must be the last include file in a .cpp file!!!
|
||
|
#include "tier0/memdbgon.h"
|
||
|
|
||
|
|
||
|
|
||
|
CUtlSymbolTable CASW_MissionTextDB::s_SymTab( 128, 128, true );
|
||
|
|
||
|
CUtlSymbolMsnTxt CASW_MissionTextSpec::SymbolForMissionFilename( const char *pMissionFilename, bool bCreateIfNotFound )
|
||
|
{
|
||
|
if ( pMissionFilename == NULL || pMissionFilename[0] == 0 )
|
||
|
{
|
||
|
return UTL_INVAL_SYMBOL;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
char buf[256];
|
||
|
V_FileBase( pMissionFilename, buf, 255 );
|
||
|
return bCreateIfNotFound ? SymTab().AddString( pMissionFilename ) : SymTab().Find( pMissionFilename ) ;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
inline const char *FindStrSubkey( KeyValues * RESTRICT pKV, const char *pKeyName )
|
||
|
{
|
||
|
KeyValues * RESTRICT pSubKey = pKV->FindKey( pKeyName );
|
||
|
return pSubKey ? pSubKey->GetString() : NULL;
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
"objectivetext"
|
||
|
{
|
||
|
"entityname" "foo"
|
||
|
"missionfile" "mission_frotz.txt"
|
||
|
"shortdesc" "Recover the alien sandwich."
|
||
|
"longdesc" "Delicious, tender and moist: a perfect balance between starch and protein.
|
||
|
Somewhere within the base lies this paragon among lunchmeats, and it is your sacred task to retrieve it."
|
||
|
}
|
||
|
*/
|
||
|
void CASW_MissionTextDB::LoadKeyValuesFile( const char *pFilename )
|
||
|
{
|
||
|
KeyValues * RESTRICT pKeyValuesSource = new KeyValues( pFilename );
|
||
|
KeyValues::AutoDelete raii( pKeyValuesSource ); // deletes the keyvalues at end of scope
|
||
|
|
||
|
if ( !pKeyValuesSource->LoadFromFile( g_pFullFileSystem, pFilename, "GAME" ) )
|
||
|
{
|
||
|
//AssertMsg1( false, "Could not open key values file %s", pFilename );
|
||
|
Warning( "Could not open key values file %s\n", pFilename );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
int numAdded = 0 ;
|
||
|
for ( KeyValues *RESTRICT pEntry = pKeyValuesSource ; pEntry != NULL ; pEntry = pEntry->GetNextKey() )
|
||
|
{
|
||
|
if ( Q_stricmp( pEntry->GetName(), "objectivetext" ) == 0 )
|
||
|
{
|
||
|
const char *pEntName = FindStrSubkey( pEntry, "entityname" );
|
||
|
if ( !pEntName )
|
||
|
{
|
||
|
Warning( "A mission text block did not specify an entityname!\n%s\n",
|
||
|
pEntry->GetString() );
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const char *pShortDesc = FindStrSubkey( pEntry, "shortdesc" );
|
||
|
if ( !pShortDesc )
|
||
|
{
|
||
|
Warning( "A mission text block did not specify a short description!\n%s\n",
|
||
|
pEntry->GetString() );
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const char *pLongDesc = FindStrSubkey( pEntry, "longdesc" );
|
||
|
if ( !pLongDesc )
|
||
|
{
|
||
|
Warning( "A mission text block did not specify a long description!\n%s\n",
|
||
|
pEntry->GetString() );
|
||
|
// substitute the short desc
|
||
|
pLongDesc = pShortDesc;
|
||
|
}
|
||
|
|
||
|
const char *pMissionFilename = FindStrSubkey( pEntry, "missionfile" );
|
||
|
|
||
|
// insert
|
||
|
AddSpec( pShortDesc, pLongDesc, pEntName, pMissionFilename );
|
||
|
++numAdded;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Warning( "Keyvalues file contained unknown key type %s\n", pEntry->GetName() );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Msg( "%d mission descriptions loaded from %s\n", numAdded, pFilename );
|
||
|
}
|
||
|
|
||
|
void CASW_MissionTextDB::Reset()
|
||
|
{
|
||
|
m_missionSpecs.RemoveAll();
|
||
|
}
|
||
|
|
||
|
void CASW_MissionTextDB::AddSpec( const char *pszShortDescription,
|
||
|
const char *pszSLongDescription, const char *pszObjectiveEntityName, const char *pszMissionFilename )
|
||
|
{
|
||
|
// assert mandatory fields
|
||
|
Assert( pszShortDescription && pszSLongDescription );
|
||
|
Assert( pszObjectiveEntityName );
|
||
|
int nextInsertId = m_missionSpecs.AddToTail();
|
||
|
m_missionSpecs[ nextInsertId ].Init( pszShortDescription, pszSLongDescription, pszObjectiveEntityName, pszMissionFilename, nextInsertId );
|
||
|
};
|
||
|
|
||
|
const CASW_MissionTextSpec * CASW_MissionTextDB::Find( const char *pObjectiveEntityName, const char *pMissionFilename /*= NULL */ )
|
||
|
{
|
||
|
// simple algorithm. walk through building a vector of possible matches. then sort by match quality. return the best.
|
||
|
// in the future this will actually use a data structure.
|
||
|
if ( pObjectiveEntityName == NULL )
|
||
|
return NULL;
|
||
|
|
||
|
// the type for our local vector below
|
||
|
struct MatchTuple
|
||
|
{
|
||
|
const CASW_MissionTextSpec *pSpec;
|
||
|
int score;
|
||
|
MatchTuple( const CASW_MissionTextSpec * pSpec_, int score_ ) : pSpec(pSpec_), score(score_) {};
|
||
|
};
|
||
|
|
||
|
CUtlVector<MatchTuple> matches( Count() >> 2, Count() >> 2 );
|
||
|
|
||
|
CUtlSymbolMsnTxt nEntName = SymTab().Find( pObjectiveEntityName );
|
||
|
CUtlSymbolMsnTxt nMissionName = CASW_MissionTextSpec::SymbolForMissionFilename( pMissionFilename, false );
|
||
|
if ( !nEntName.IsValid() )
|
||
|
{
|
||
|
Warning( "No mission text specifier mentions an entity named %s\n", pObjectiveEntityName );
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
// march forward looking for matches. compute a score for each match.
|
||
|
const int N = m_missionSpecs.Count();
|
||
|
for ( int i = 0 ; i < N ; ++i )
|
||
|
{
|
||
|
const CASW_MissionTextSpec *pSpec = &m_missionSpecs[i];
|
||
|
int matchscore = 0;
|
||
|
// test mandatory field
|
||
|
if ( pSpec->m_nObjectiveEntityName == nEntName )
|
||
|
{
|
||
|
// score optional fields.
|
||
|
// compare each field in the spec against the query.
|
||
|
// if the field in the spec is defined, then a match adds one to
|
||
|
// score, but a mismatch disqualifies the entry.
|
||
|
// if the field in the spec is undefined, then it matches everything,
|
||
|
// but does not add to score.
|
||
|
if ( pSpec->m_nMissionFilename.IsValid() )
|
||
|
{
|
||
|
matchscore = pSpec->m_nMissionFilename == nMissionName ? 2 : 0;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
matchscore = 1;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( matchscore > 0 )
|
||
|
{
|
||
|
// best possible score, return immediately
|
||
|
if ( matchscore == 2 )
|
||
|
{
|
||
|
// (refactor this block if we want to choose randomly from many possibilities)
|
||
|
return pSpec;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
matches.AddToTail( MatchTuple(pSpec, matchscore) );
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( matches.Count() == 0 )
|
||
|
return NULL; // nothing found
|
||
|
|
||
|
// in theory we should sort this list to push the best matches to the beginning,
|
||
|
// but right now the only possible score is 1, so return an arbitrary element
|
||
|
return matches[0].pSpec;
|
||
|
}
|
||
|
|
||
|
CASW_MissionTextSpec * CASW_MissionTextDB::GetSpecById( CASW_MissionTextSpec::ID_t nId )
|
||
|
{
|
||
|
AssertMsg2( nId < m_missionSpecs.Count(), "Tried to look up invalid mission text ID %d / %d\n",
|
||
|
nId, m_missionSpecs.Count() );
|
||
|
|
||
|
return nId < m_missionSpecs.Count() ? &m_missionSpecs[ nId ] : NULL;
|
||
|
}
|
||
|
|
||
|
const char * CASW_MissionTextDB::GetShortDescriptionByID( unsigned int id )
|
||
|
{
|
||
|
CASW_MissionTextSpec * RESTRICT pSpec = GetSpecById( id );
|
||
|
return pSpec ? pSpec->GetShortDescription() : NULL;
|
||
|
}
|
||
|
|
||
|
const char * CASW_MissionTextDB::GetLongDescriptionByID( unsigned int id )
|
||
|
{
|
||
|
CASW_MissionTextSpec * RESTRICT pSpec = GetSpecById( id );
|
||
|
return pSpec ? pSpec->GetLongDescription() : NULL;
|
||
|
}
|
||
|
|
||
|
IASW_Mission_Text_Database::ID_t CASW_MissionTextDB::FindMissionTextID( const char *pEntityName, const char *pMissionName )
|
||
|
{
|
||
|
const CASW_MissionTextSpec *pSpec = Find( pEntityName, pMissionName );
|
||
|
return pSpec ? pSpec->m_nId : IASW_Mission_Text_Database::INVALID_INDEX ;
|
||
|
}
|
||
|
|
||
|
|
||
|
#ifdef ENABLE_MISSION_TEXT_DB_DEBUG_FUNCTIONS
|
||
|
CASW_MissionTextDB g_MissionTextDB;
|
||
|
|
||
|
void CC_ASW_TestMissionText_f( const CCommand &args )
|
||
|
{
|
||
|
if ( args.ArgC() < 2 )
|
||
|
{
|
||
|
Msg("Usage: asw_test_mission_text_q <entity name> [mission name]");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const char *pEntName = args[1];
|
||
|
const char *pMissName = args.ArgC() >= 3 ? args[2] : NULL;
|
||
|
const CASW_MissionTextSpec *pSpec = g_MissionTextDB.Find( pEntName, pMissName );
|
||
|
if ( pSpec )
|
||
|
{
|
||
|
Msg( "\"%s\"\n", pSpec->GetShortDescription() );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Warning( "Not found.\n" );
|
||
|
}
|
||
|
}
|
||
|
static ConCommand asw_test_mission_text_q("asw_test_mission_text_q", CC_ASW_TestMissionText_f, 0 );
|
||
|
|
||
|
|
||
|
void CC_ASW_TestMissionTextLoad_f( const CCommand &args )
|
||
|
{
|
||
|
if ( args.ArgC() < 2 )
|
||
|
{
|
||
|
Msg("Usage: asw_test_mission_text_load [filename]");
|
||
|
return;
|
||
|
}
|
||
|
g_MissionTextDB.LoadKeyValuesFile( args[1] );
|
||
|
}
|
||
|
static ConCommand asw_test_mission_text_load("asw_test_mission_text_load", CC_ASW_TestMissionTextLoad_f, 0 );
|
||
|
|
||
|
void CC_ASW_TestMissionTextReset_f( const CCommand &args )
|
||
|
{
|
||
|
g_MissionTextDB.Reset();
|
||
|
}
|
||
|
static ConCommand asw_test_mission_text_reset("asw_test_mission_text_reset", CC_ASW_TestMissionTextReset_f, 0 );
|
||
|
|
||
|
#endif
|