mirror of
https://github.com/shawns-valve/halflife.git
synced 2024-11-24 13:11:06 +00:00
704 lines
18 KiB
C++
704 lines
18 KiB
C++
//========= Copyright © 1996-2002, Valve LLC, All rights reserved. ============
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//=============================================================================
|
|
|
|
// Author: Michael S. Booth (mike@turtlerockstudios.com), 2003
|
|
|
|
#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 "simple_checksum.h"
|
|
|
|
BotProfileManager *TheBotProfiles = NULL;
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
// A little explanation is in order here. This file is in game_shared. It is supposed to be able to be compiled
|
|
// into multiple dlls. However, all the basic utility functions (loading files, parsing strings, etc) are
|
|
// accessed different ways in different dlls. To combat this, we need to redirect some calls depending on
|
|
// the dll we're in.
|
|
#ifdef GAMEUI_EXPORTS
|
|
#include "../GameUI/EngineInterface.h"
|
|
#include "../cstrike/dlls/weapontype.h"
|
|
#include "../cstrike/dlls/weapontype.cpp" /// @TODO: remove this CStrike peculiarity from a game_shared file!!!
|
|
void Career_Printf(const char *fmt, ...);
|
|
#define CONSOLE_ECHO Career_Printf
|
|
#define LOAD_FILE_FOR_ME(name, len) (engine->COM_LoadFile( (name), 5, (len) ))
|
|
#define FREE_FILE (engine->COM_FreeFile)
|
|
#define UTIL_IsGame( x ) 0
|
|
#else
|
|
#include "extdll.h"
|
|
#include "util.h"
|
|
#include "cbase.h"
|
|
#include "weapons.h"
|
|
#include "soundent.h"
|
|
#include "gamerules.h"
|
|
#include "player.h"
|
|
#include "client.h"
|
|
#include "cmd.h"
|
|
#include "pm_shared.h"
|
|
#include "bot.h"
|
|
#include "bot_util.h"
|
|
#endif
|
|
|
|
//--------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Generates a filename-decorated skin name
|
|
*/
|
|
static const char * GetDecoratedSkinName( const char *name, const char *filename )
|
|
{
|
|
#ifdef _WIN32
|
|
const int BufLen = _MAX_PATH + 64;
|
|
#else
|
|
const int BufLen = MAX_PATH + 64;
|
|
#endif
|
|
static char buf[BufLen];
|
|
snprintf( buf, BufLen, "%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 )
|
|
{
|
|
int weaponClass = AliasToWeaponClass( WeaponIDToAlias( m_weaponPreference[i] ) );
|
|
|
|
if (weaponClass == WEAPONCLASS_SUBMACHINEGUN ||
|
|
weaponClass == WEAPONCLASS_SHOTGUN ||
|
|
weaponClass == WEAPONCLASS_MACHINEGUN ||
|
|
weaponClass == WEAPONCLASS_RIFLE ||
|
|
weaponClass == WEAPONCLASS_SNIPERRIFLE)
|
|
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 (AliasToWeaponClass( WeaponIDToAlias( m_weaponPreference[i] ) ) == WEAPONCLASS_PISTOL)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Return true if this profile is valid for the specified team
|
|
*/
|
|
bool BotProfile::IsValidForTeam( BotProfileTeamType team ) const
|
|
{
|
|
return ( team == BOT_TEAM_ANY || m_teams == BOT_TEAM_ANY || team == m_teams );
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* 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 )
|
|
{
|
|
int dataLength;
|
|
char *dataPointer = (char *)LOAD_FILE_FOR_ME( const_cast<char *>( filename ), &dataLength );
|
|
const char *dataFile = dataPointer;
|
|
|
|
if (dataFile == NULL)
|
|
{
|
|
if ( UTIL_IsGame( "czero" ) )
|
|
{
|
|
CONSOLE_ECHO( "WARNING: Cannot access bot profile database '%s'\n", filename );
|
|
}
|
|
return;
|
|
}
|
|
|
|
// compute simple checksum
|
|
if (checksum)
|
|
{
|
|
*checksum = ComputeSimpleChecksum( (const unsigned char *)dataPointer, dataLength );
|
|
}
|
|
|
|
// keep list of templates used for inheritance
|
|
BotProfileList templateList;
|
|
|
|
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 );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
snprintf( skinName, BufLen, "%s", token );
|
|
|
|
// get attribute name
|
|
dataFile = SharedParse( dataFile );
|
|
if (!dataFile)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
if (stricmp( "Model", token ))
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected 'Model'\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
|
|
// eat '='
|
|
dataFile = SharedParse( dataFile );
|
|
if (!dataFile)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
if (strcmp( "=", token ))
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
|
|
// get attribute value
|
|
dataFile = SharedParse( dataFile );
|
|
if (!dataFile)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename );
|
|
FREE_FILE( 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 ];
|
|
sprintf( 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 );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
if (strcmp( "End", token ))
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected 'End'\n", filename );
|
|
FREE_FILE( 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( BotProfileList::iterator iter = templateList.begin(); iter != templateList.end(); ++iter )
|
|
{
|
|
if (!stricmp( (*iter)->GetName(), token ))
|
|
{
|
|
inherit = *iter;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (inherit == NULL)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing '%s' - invalid template reference '%s'\n", filename, token );
|
|
FREE_FILE( 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 );
|
|
FREE_FILE( 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 );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
|
|
// check for End delimiter
|
|
if (!stricmp( token, "End" ))
|
|
break;
|
|
|
|
// found attribute name - keep it
|
|
char attributeName[64];
|
|
strncpy( attributeName, token, sizeof( attributeName ) - 1 );
|
|
attributeName[ sizeof( attributeName ) - 1 ] = '\0';
|
|
|
|
// eat '='
|
|
dataFile = SharedParse( dataFile );
|
|
if (!dataFile)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
|
|
token = SharedGetToken();
|
|
if (strcmp( "=", token ))
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected '='\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
|
|
// get attribute value
|
|
dataFile = SharedParse( dataFile );
|
|
if (!dataFile)
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - expected attribute value\n", filename );
|
|
FREE_FILE( dataPointer );
|
|
return;
|
|
}
|
|
token = SharedGetToken();
|
|
|
|
// store value in appropriate attribute
|
|
if (!stricmp( "Aggression", attributeName ))
|
|
{
|
|
profile->m_aggression = atof(token) / 100.0f;
|
|
}
|
|
else if (!stricmp( "Skill", attributeName ))
|
|
{
|
|
profile->m_skill = 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 = 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 = atof(token);
|
|
|
|
#ifndef GAMEUI_EXPORTS
|
|
// subtract off latency due to "think" update rate.
|
|
// In GameUI, we don't really care.
|
|
profile->m_reactionTime -= g_flBotFullThinkInterval;
|
|
#endif
|
|
|
|
}
|
|
else if (!stricmp( "AttackDelay", attributeName ))
|
|
{
|
|
profile->m_attackDelay = 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 = BOT_TEAM_T;
|
|
}
|
|
else if ( !stricmp( token, "CT" ) )
|
|
{
|
|
profile->m_teams = BOT_TEAM_CT;
|
|
}
|
|
else
|
|
{
|
|
profile->m_teams = BOT_TEAM_ANY;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CONSOLE_ECHO( "Error parsing %s - unknown attribute '%s'\n", filename, attributeName );
|
|
}
|
|
}
|
|
|
|
if (!isDefault)
|
|
{
|
|
if (isTemplate)
|
|
{
|
|
// add to template list
|
|
templateList.push_back( profile );
|
|
}
|
|
else
|
|
{
|
|
// add profile to the master list
|
|
m_profileList.push_back( profile );
|
|
}
|
|
}
|
|
}
|
|
|
|
FREE_FILE( dataPointer );
|
|
|
|
// free the templates
|
|
for( BotProfileList::iterator iter = templateList.begin(); iter != templateList.end(); ++iter )
|
|
delete *iter;
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
BotProfileManager::~BotProfileManager( void )
|
|
{
|
|
Reset();
|
|
|
|
VoiceBankList::iterator it;
|
|
for ( it = m_voiceBanks.begin(); it != m_voiceBanks.end(); ++it )
|
|
{
|
|
delete[] *it;
|
|
}
|
|
m_voiceBanks.clear();
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Free all bot profiles
|
|
*/
|
|
void BotProfileManager::Reset( void )
|
|
{
|
|
for( BotProfileList::iterator iter = m_profileList.begin(); iter != m_profileList.end(); ++iter )
|
|
delete *iter;
|
|
|
|
m_profileList.clear();
|
|
|
|
for (int 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* 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;
|
|
|
|
VoiceBankList::const_iterator it;
|
|
for ( it = m_voiceBanks.begin(); it != m_voiceBanks.end(); ++it, ++index )
|
|
{
|
|
if ( !stricmp( filename, *it ) )
|
|
{
|
|
return index;
|
|
}
|
|
}
|
|
|
|
m_voiceBanks.push_back( CloneString( filename ) );
|
|
return index;
|
|
}
|
|
|
|
|
|
//--------------------------------------------------------------------------------------------------------------
|
|
/**
|
|
* Return random unused profile that matches the given difficulty level
|
|
*/
|
|
const BotProfile *BotProfileManager::GetRandomProfile( BotDifficultyType difficulty, BotProfileTeamType team ) const
|
|
{
|
|
#ifndef RANDOM_LONG
|
|
return NULL; // we don't need random profiles when we're not in the game dll
|
|
#else
|
|
BotProfileList::const_iterator iter;
|
|
|
|
// count up valid profiles
|
|
int validCount = 0;
|
|
for( iter = m_profileList.begin(); iter != m_profileList.end(); ++iter )
|
|
{
|
|
const BotProfile *profile = *iter;
|
|
|
|
if (profile->IsDifficulty( difficulty ) && !UTIL_IsNameTaken( profile->GetName() ) && profile->IsValidForTeam( team ))
|
|
++validCount;
|
|
}
|
|
|
|
if (validCount == 0)
|
|
return NULL;
|
|
|
|
// select one at random
|
|
int which = RANDOM_LONG( 0, validCount-1 );
|
|
for( iter = m_profileList.begin(); iter != m_profileList.end(); ++iter )
|
|
{
|
|
const BotProfile *profile = *iter;
|
|
|
|
if (profile->IsDifficulty( difficulty ) && !UTIL_IsNameTaken( profile->GetName() ) && profile->IsValidForTeam( team ))
|
|
if (which-- == 0)
|
|
return profile;
|
|
}
|
|
|
|
return NULL;
|
|
#endif;
|
|
}
|
|
|