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.
704 lines
17 KiB
704 lines
17 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
//=============================================================================// |
|
|
|
// Author: Michael S. Booth (mike@turtlerockstudios.com), 2003 |
|
|
|
#include "cbase.h" |
|
|
|
#pragma warning( disable : 4530 ) // STL uses exceptions, but we are not compiling with them - ignore warning |
|
|
|
#define DEFINE_DIFFICULTY_NAMES |
|
#include "bot_profile.h" |
|
#include "shared_util.h" |
|
|
|
#include "bot.h" |
|
#include "bot_util.h" |
|
#include "cs_bot.h" // BOTPORT: Remove this CS dependency |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
|
|
BotProfileManager *TheBotProfiles = NULL; |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Generates a filename-decorated skin name |
|
*/ |
|
static const char * GetDecoratedSkinName( const char *name, const char *filename ) |
|
{ |
|
const int BufLen = _MAX_PATH + 64; |
|
static char buf[BufLen]; |
|
Q_snprintf( buf, sizeof( buf ), "%s/%s", filename, name ); |
|
return buf; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
const char* BotProfile::GetWeaponPreferenceAsString( int i ) const |
|
{ |
|
if ( i < 0 || i >= m_weaponPreferenceCount ) |
|
return NULL; |
|
|
|
return WeaponIDToAlias( m_weaponPreference[ i ] ); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if this profile has a primary weapon preference |
|
*/ |
|
bool BotProfile::HasPrimaryPreference( void ) const |
|
{ |
|
for( int i=0; i<m_weaponPreferenceCount; ++i ) |
|
{ |
|
if (IsPrimaryWeapon( m_weaponPreference[i] )) |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if this profile has a pistol weapon preference |
|
*/ |
|
bool BotProfile::HasPistolPreference( void ) const |
|
{ |
|
for( int i=0; i<m_weaponPreferenceCount; ++i ) |
|
if (IsSecondaryWeapon( m_weaponPreference[i] )) |
|
return true; |
|
|
|
return false; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if this profile is valid for the specified team |
|
*/ |
|
bool BotProfile::IsValidForTeam( int team ) const |
|
{ |
|
return ( team == TEAM_UNASSIGNED || m_teams == TEAM_UNASSIGNED || team == m_teams ); |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return true if this profile inherits from the specified template |
|
*/ |
|
bool BotProfile::InheritsFrom( const char *name ) const |
|
{ |
|
if ( WildcardMatch( name, GetName() ) ) |
|
return true; |
|
|
|
for ( int i=0; i<m_templates.Count(); ++i ) |
|
{ |
|
const BotProfile *queryTemplate = m_templates[i]; |
|
if ( queryTemplate->InheritsFrom( name ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Constructor |
|
*/ |
|
BotProfileManager::BotProfileManager( void ) |
|
{ |
|
m_nextSkin = 0; |
|
for (int i=0; i<NumCustomSkins; ++i) |
|
{ |
|
m_skins[i] = NULL; |
|
m_skinFilenames[i] = NULL; |
|
m_skinModelnames[i] = NULL; |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Load the bot profile database |
|
*/ |
|
void BotProfileManager::Init( const char *filename, unsigned int *checksum ) |
|
{ |
|
FileHandle_t file = filesystem->Open( filename, "r" ); |
|
|
|
if (!file) |
|
{ |
|
if ( true ) // UTIL_IsGame( "czero" ) ) |
|
{ |
|
CONSOLE_ECHO( "WARNING: Cannot access bot profile database '%s'\n", filename ); |
|
} |
|
return; |
|
} |
|
|
|
int dataLength = filesystem->Size( filename ); |
|
char *dataPointer = new char[ dataLength ]; |
|
int dataReadLength = filesystem->Read( dataPointer, dataLength, file ); |
|
filesystem->Close( file ); |
|
if ( dataReadLength > 0 ) |
|
{ |
|
// NULL-terminate based on the length read in, since Read() can transform \r\n to \n and |
|
// return fewer bytes than we were expecting. |
|
dataPointer[ dataReadLength - 1 ] = 0; |
|
} |
|
|
|
const char *dataFile = dataPointer; |
|
|
|
// compute simple checksum |
|
if (checksum) |
|
{ |
|
*checksum = 0; // ComputeSimpleChecksum( (const unsigned char *)dataPointer, dataLength ); |
|
} |
|
|
|
BotProfile defaultProfile; |
|
|
|
// |
|
// Parse the BotProfile.db into BotProfile instances |
|
// |
|
while( true ) |
|
{ |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
break; |
|
|
|
char *token = SharedGetToken(); |
|
|
|
bool isDefault = (!stricmp( token, "Default" )); |
|
bool isTemplate = (!stricmp( token, "Template" )); |
|
bool isCustomSkin = (!stricmp( token, "Skin" )); |
|
|
|
if ( isCustomSkin ) |
|
{ |
|
const int BufLen = 64; |
|
char skinName[BufLen]; |
|
|
|
// get skin name |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected skin name\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
Q_snprintf( skinName, sizeof( skinName ), "%s", token ); |
|
|
|
// get attribute name |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
if (stricmp( "Model", token )) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
// eat '=' |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
if (strcmp( "=", token )) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
// get attribute value |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
|
|
const char *decoratedName = GetDecoratedSkinName( skinName, filename ); |
|
bool skinExists = GetCustomSkinIndex( decoratedName ) > 0; |
|
if ( m_nextSkin < NumCustomSkins && !skinExists ) |
|
{ |
|
// decorate the name |
|
m_skins[ m_nextSkin ] = CloneString( decoratedName ); |
|
|
|
// construct the model filename |
|
m_skinModelnames[ m_nextSkin ] = CloneString( token ); |
|
m_skinFilenames[ m_nextSkin ] = new char[ strlen(token)*2 + strlen("models/player//.mdl") + 1 ]; |
|
Q_snprintf( m_skinFilenames[ m_nextSkin ], sizeof( m_skinFilenames[ m_nextSkin ] ), "models/player/%s/%s.mdl", token, token ); |
|
++m_nextSkin; |
|
} |
|
|
|
// eat 'End' |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
if (strcmp( "End", token )) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
continue; // it's just a custom skin - no need to do inheritance on a bot profile, etc. |
|
} |
|
|
|
// encountered a new profile |
|
BotProfile *profile; |
|
|
|
if (isDefault) |
|
{ |
|
profile = &defaultProfile; |
|
} |
|
else |
|
{ |
|
profile = new BotProfile; |
|
|
|
// always inherit from Default |
|
*profile = defaultProfile; |
|
} |
|
|
|
// do inheritance in order of appearance |
|
if (!isTemplate && !isDefault) |
|
{ |
|
const BotProfile *inherit = NULL; |
|
|
|
// template names are separated by "+" |
|
while(true) |
|
{ |
|
char *c = strchr( token, '+' ); |
|
if (c) |
|
*c = '\000'; |
|
|
|
// find the given template name |
|
FOR_EACH_LL( m_templateList, it ) |
|
{ |
|
BotProfile *profile = m_templateList[ it ]; |
|
if (!stricmp( profile->GetName(), token )) |
|
{ |
|
inherit = profile; |
|
break; |
|
} |
|
} |
|
|
|
if (inherit == NULL) |
|
{ |
|
CONSOLE_ECHO( "Error parsing '%s' - invalid template reference '%s'\n", filename, token ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
// inherit the data |
|
profile->Inherit( inherit, &defaultProfile ); |
|
|
|
if (c == NULL) |
|
break; |
|
|
|
token = c+1; |
|
} |
|
} |
|
|
|
|
|
// get name of this profile |
|
if (!isDefault) |
|
{ |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing '%s' - expected name\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
profile->m_name = CloneString( SharedGetToken() ); |
|
|
|
/** |
|
* HACK HACK |
|
* Until we have a generalized means of storing bot preferences, we're going to hardcode the bot's |
|
* preference towards silencers based on his name. |
|
*/ |
|
if ( profile->m_name[0] % 2 ) |
|
{ |
|
profile->m_prefersSilencer = true; |
|
} |
|
} |
|
|
|
// read attributes for this profile |
|
bool isFirstWeaponPref = true; |
|
while( true ) |
|
{ |
|
// get next token |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
|
|
// check for End delimiter |
|
if (!stricmp( token, "End" )) |
|
break; |
|
|
|
// found attribute name - keep it |
|
char attributeName[64]; |
|
strcpy( attributeName, token ); |
|
|
|
// eat '=' |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
token = SharedGetToken(); |
|
if (strcmp( "=", token )) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
|
|
// get attribute value |
|
dataFile = SharedParse( dataFile ); |
|
if (!dataFile) |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename ); |
|
delete [] dataPointer; |
|
return; |
|
} |
|
token = SharedGetToken(); |
|
|
|
// store value in appropriate attribute |
|
if (!stricmp( "Aggression", attributeName )) |
|
{ |
|
profile->m_aggression = (float)atof(token) / 100.0f; |
|
} |
|
else if (!stricmp( "Skill", attributeName )) |
|
{ |
|
profile->m_skill = (float)atof(token) / 100.0f; |
|
} |
|
else if (!stricmp( "Skin", attributeName )) |
|
{ |
|
profile->m_skin = atoi(token); |
|
if ( profile->m_skin == 0 ) |
|
{ |
|
// atoi() failed - try to look up a custom skin by name |
|
profile->m_skin = GetCustomSkinIndex( token, filename ); |
|
} |
|
} |
|
else if (!stricmp( "Teamwork", attributeName )) |
|
{ |
|
profile->m_teamwork = (float)atof(token) / 100.0f; |
|
} |
|
else if (!stricmp( "Cost", attributeName )) |
|
{ |
|
profile->m_cost = atoi(token); |
|
} |
|
else if (!stricmp( "VoicePitch", attributeName )) |
|
{ |
|
profile->m_voicePitch = atoi(token); |
|
} |
|
else if (!stricmp( "VoiceBank", attributeName )) |
|
{ |
|
profile->m_voiceBank = FindVoiceBankIndex( token ); |
|
} |
|
else if (!stricmp( "WeaponPreference", attributeName )) |
|
{ |
|
// weapon preferences override parent prefs |
|
if (isFirstWeaponPref) |
|
{ |
|
isFirstWeaponPref = false; |
|
profile->m_weaponPreferenceCount = 0; |
|
} |
|
|
|
if (!stricmp( token, "none" )) |
|
{ |
|
profile->m_weaponPreferenceCount = 0; |
|
} |
|
else |
|
{ |
|
if (profile->m_weaponPreferenceCount < BotProfile::MAX_WEAPON_PREFS) |
|
{ |
|
profile->m_weaponPreference[ profile->m_weaponPreferenceCount++ ] = AliasToWeaponID( token ); |
|
} |
|
} |
|
} |
|
else if (!stricmp( "ReactionTime", attributeName )) |
|
{ |
|
profile->m_reactionTime = (float)atof(token); |
|
|
|
#ifndef GAMEUI_EXPORTS |
|
// subtract off latency due to "think" update rate. |
|
// In GameUI, we don't really care. |
|
//profile->m_reactionTime -= g_BotUpdateInterval; |
|
#endif |
|
|
|
} |
|
else if (!stricmp( "AttackDelay", attributeName )) |
|
{ |
|
profile->m_attackDelay = (float)atof(token); |
|
} |
|
else if (!stricmp( "Difficulty", attributeName )) |
|
{ |
|
// override inheritance |
|
profile->m_difficultyFlags = 0; |
|
|
|
// parse bit flags |
|
while(true) |
|
{ |
|
char *c = strchr( token, '+' ); |
|
if (c) |
|
*c = '\000'; |
|
|
|
for( int i=0; i<NUM_DIFFICULTY_LEVELS; ++i ) |
|
if (!stricmp( BotDifficultyName[i], token )) |
|
profile->m_difficultyFlags |= (1 << i); |
|
|
|
if (c == NULL) |
|
break; |
|
|
|
token = c+1; |
|
} |
|
} |
|
else if (!stricmp( "Team", attributeName )) |
|
{ |
|
if ( !stricmp( token, "T" ) ) |
|
{ |
|
profile->m_teams = TEAM_TERRORIST; |
|
} |
|
else if ( !stricmp( token, "CT" ) ) |
|
{ |
|
profile->m_teams = TEAM_CT; |
|
} |
|
else |
|
{ |
|
profile->m_teams = TEAM_UNASSIGNED; |
|
} |
|
} |
|
else |
|
{ |
|
CONSOLE_ECHO( "Error parsing %s - unknown attribute '%s'\n", filename, attributeName ); |
|
} |
|
} |
|
|
|
if (!isDefault) |
|
{ |
|
if (isTemplate) |
|
{ |
|
// add to template list |
|
m_templateList.AddToTail( profile ); |
|
} |
|
else |
|
{ |
|
// add profile to the master list |
|
m_profileList.AddToTail( profile ); |
|
} |
|
} |
|
} |
|
|
|
delete [] dataPointer; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
BotProfileManager::~BotProfileManager( void ) |
|
{ |
|
Reset(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Free all bot profiles |
|
*/ |
|
void BotProfileManager::Reset( void ) |
|
{ |
|
m_profileList.PurgeAndDeleteElements(); |
|
m_templateList.PurgeAndDeleteElements(); |
|
|
|
int i; |
|
|
|
for (i=0; i<NumCustomSkins; ++i) |
|
{ |
|
if ( m_skins[i] ) |
|
{ |
|
delete[] m_skins[i]; |
|
m_skins[i] = NULL; |
|
} |
|
if ( m_skinFilenames[i] ) |
|
{ |
|
delete[] m_skinFilenames[i]; |
|
m_skinFilenames[i] = NULL; |
|
} |
|
if ( m_skinModelnames[i] ) |
|
{ |
|
delete[] m_skinModelnames[i]; |
|
m_skinModelnames[i] = NULL; |
|
} |
|
} |
|
|
|
for ( i=0; i<m_voiceBanks.Count(); ++i ) |
|
{ |
|
delete[] m_voiceBanks[i]; |
|
} |
|
m_voiceBanks.RemoveAll(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Returns custom skin name at a particular index |
|
*/ |
|
const char * BotProfileManager::GetCustomSkin( int index ) |
|
{ |
|
if ( index < FirstCustomSkin || index > LastCustomSkin ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
return m_skins[ index - FirstCustomSkin ]; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Returns custom skin filename at a particular index |
|
*/ |
|
const char * BotProfileManager::GetCustomSkinFname( int index ) |
|
{ |
|
if ( index < FirstCustomSkin || index > LastCustomSkin ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
return m_skinFilenames[ index - FirstCustomSkin ]; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Returns custom skin modelname at a particular index |
|
*/ |
|
const char * BotProfileManager::GetCustomSkinModelname( int index ) |
|
{ |
|
if ( index < FirstCustomSkin || index > LastCustomSkin ) |
|
{ |
|
return NULL; |
|
} |
|
|
|
return m_skinModelnames[ index - FirstCustomSkin ]; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Looks up a custom skin index by filename-decorated name (will decorate the name if filename is given) |
|
*/ |
|
int BotProfileManager::GetCustomSkinIndex( const char *name, const char *filename ) |
|
{ |
|
const char * skinName = name; |
|
if ( filename ) |
|
{ |
|
skinName = GetDecoratedSkinName( name, filename ); |
|
} |
|
|
|
for (int i=0; i<NumCustomSkins; ++i) |
|
{ |
|
if ( m_skins[i] ) |
|
{ |
|
if ( !stricmp( skinName, m_skins[i] ) ) |
|
{ |
|
return FirstCustomSkin + i; |
|
} |
|
} |
|
} |
|
return 0; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------- |
|
/** |
|
* return index of the (custom) bot phrase db, inserting it if needed |
|
*/ |
|
int BotProfileManager::FindVoiceBankIndex( const char *filename ) |
|
{ |
|
int index = 0; |
|
|
|
for ( int i=0; i<m_voiceBanks.Count(); ++i ) |
|
{ |
|
if ( !stricmp( filename, m_voiceBanks[i] ) ) |
|
{ |
|
return index; |
|
} |
|
} |
|
|
|
m_voiceBanks.AddToTail( CloneString( filename ) ); |
|
return index; |
|
} |
|
|
|
|
|
//-------------------------------------------------------------------------------------------------------------- |
|
/** |
|
* Return random unused profile that matches the given difficulty level |
|
*/ |
|
const BotProfile *BotProfileManager::GetRandomProfile( BotDifficultyType difficulty, int team, CSWeaponType weaponType ) const |
|
{ |
|
// count up valid profiles |
|
CUtlVector< const BotProfile * > profiles; |
|
FOR_EACH_LL( m_profileList, it ) |
|
{ |
|
const BotProfile *profile = m_profileList[ it ]; |
|
|
|
// Match difficulty |
|
if ( !profile->IsDifficulty( difficulty ) ) |
|
continue; |
|
|
|
// Prevent duplicate names |
|
if ( UTIL_IsNameTaken( profile->GetName() ) ) |
|
continue; |
|
|
|
// Match team choice |
|
if ( !profile->IsValidForTeam( team ) ) |
|
continue; |
|
|
|
// Match desired weapon |
|
if ( weaponType != WEAPONTYPE_UNKNOWN ) |
|
{ |
|
if ( !profile->GetWeaponPreferenceCount() ) |
|
continue; |
|
|
|
if ( weaponType != WeaponClassFromWeaponID( (CSWeaponID)profile->GetWeaponPreference( 0 ) ) ) |
|
continue; |
|
} |
|
|
|
profiles.AddToTail( profile ); |
|
} |
|
|
|
if ( !profiles.Count() ) |
|
return NULL; |
|
|
|
// select one at random |
|
int which = RandomInt( 0, profiles.Count()-1 ); |
|
return profiles[which]; |
|
} |
|
|
|
|