#include "precompiled.h" BotProfileManager *TheBotProfiles = nullptr; // Generates a filename-decorated skin name const char *GetDecoratedSkinName(const char *name, const char *filename) { const int BufLen = MAX_PATH + 64; static char buf[BufLen]; Q_snprintf(buf, BufLen, "%s/%s", filename, name); return buf; } const char *BotProfile::GetWeaponPreferenceAsString(int i) const { if (i < 0 || i >= m_weaponPreferenceCount) return nullptr; return WeaponIDToAlias(m_weaponPreference[i]); } // Return true if this profile has a primary weapon preference bool BotProfile::HasPrimaryPreference() 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() 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); } BotProfileManager::BotProfileManager() { m_nextSkin = 0; for (int i = 0; i < NumCustomSkins; i++) { m_skins[i] = nullptr; m_skinFilenames[i] = nullptr; m_skinModelnames[i] = nullptr; } } // Load the bot profile database void BotProfileManager::Init(const char *filename, unsigned int *checksum) { static const char *BotDifficultyName[] = { "EASY", "NORMAL", "HARD", "EXPERT", nullptr }; int dataLength; char *dataPointer = (char *)LOAD_FILE_FOR_ME(const_cast(filename), &dataLength); char *dataFile = dataPointer; if (!dataFile) { if (AreBotsAllowed()) { 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 = (!Q_stricmp(token, "Default")); bool isTemplate = (!Q_stricmp(token, "Template")); bool isCustomSkin = (!Q_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(); Q_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 (Q_stricmp(token, "Model") != 0) { 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 (Q_strcmp(token, "=") != 0) { 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[Q_strlen(token) * 2 + Q_strlen("models/player//.mdl") + 1]; Q_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 (Q_strcmp(token, "End") != 0) { CONSOLE_ECHO("Error parsing %s - expected 'End'\n", filename); FREE_FILE(dataPointer); return; } // it's just a custom skin - no need to do inheritance on a bot profile, etc. continue; } // 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 = nullptr; // template names are separated by "+" while (true) { char *c = Q_strchr(token, '+'); if (c) *c = '\0'; // find the given template name for (auto templates : templateList) { if (!Q_stricmp(templates->GetName(), token)) { inherit = templates; break; } } if (!inherit) { 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 == nullptr) 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()); #ifdef REGAMEDLL_FIXES if (RANDOM_LONG(0, 2) == 2) #else // 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) #endif { 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 (!Q_stricmp(token, "End")) break; // found attribute name - keep it char attributeName[64]; Q_strcpy(attributeName, token); // eat '=' dataFile = SharedParse(dataFile); if (!dataFile) { CONSOLE_ECHO("Error parsing %s - expected '='\n", filename); FREE_FILE(dataPointer); return; } token = SharedGetToken(); if (Q_strcmp(token, "=") != 0) { 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 (!Q_stricmp("Aggression", attributeName)) { profile->m_aggression = Q_atof(token) / 100.0f; } else if (!Q_stricmp("Skill", attributeName)) { profile->m_skill = Q_atof(token) / 100.0f; } else if (!Q_stricmp("Skin", attributeName)) { profile->m_skin = Q_atoi(token); if (profile->m_skin == 0) { // Q_atoi() failed - try to look up a custom skin by name profile->m_skin = GetCustomSkinIndex(token, filename); } } else if (!Q_stricmp("Teamwork", attributeName)) { profile->m_teamwork = Q_atof(token) / 100.0f; } else if (!Q_stricmp("Cost", attributeName)) { profile->m_cost = Q_atoi(token); } else if (!Q_stricmp("VoicePitch", attributeName)) { profile->m_voicePitch = Q_atoi(token); } else if (!Q_stricmp("VoiceBank", attributeName)) { profile->m_voiceBank = FindVoiceBankIndex(token); } else if (!Q_stricmp("WeaponPreference", attributeName)) { // weapon preferences override parent prefs if (isFirstWeaponPref) { isFirstWeaponPref = false; profile->m_weaponPreferenceCount = 0; } if (!Q_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 (!Q_stricmp("ReactionTime", attributeName)) { profile->m_reactionTime = Q_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 (!Q_stricmp("AttackDelay", attributeName)) { profile->m_attackDelay = Q_atof(token); } else if (!Q_stricmp("Difficulty", attributeName)) { // override inheritance profile->m_difficultyFlags = 0; // parse bit flags while (true) { char *c = Q_strchr(token, '+'); if (c) *c = '\0'; for (int i = 0; i < NUM_DIFFICULTY_LEVELS; i++) { if (!Q_stricmp(BotDifficultyName[i], token)) profile->m_difficultyFlags |= (1<m_teams = BOT_TEAM_T; } else if (!Q_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 (auto templates : templateList) delete templates; templateList.clear(); } BotProfileManager::~BotProfileManager() { Reset(); for (auto phrase : m_voiceBanks) delete[] phrase; m_voiceBanks.clear(); } // Free all bot profiles void BotProfileManager::Reset() { for (auto profile : m_profileList) delete profile; m_profileList.clear(); for (int i = 0; i < NumCustomSkins; i++) { if (m_skins[i]) { delete[] m_skins[i]; m_skins[i] = nullptr; } if (m_skinFilenames[i]) { delete[] m_skinFilenames[i]; m_skinFilenames[i] = nullptr; } if (m_skinModelnames[i]) { delete[] m_skinModelnames[i]; m_skinModelnames[i] = nullptr; } } } // Returns custom skin name at a particular index const char *BotProfileManager::GetCustomSkin(int index) { if (index < FirstCustomSkin || index > LastCustomSkin) { return nullptr; } 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 nullptr; } 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 nullptr; } 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 (!Q_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 (auto phrase : m_voiceBanks) { if (!Q_stricmp(filename, phrase)) return index; 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 { #ifdef RANDOM_LONG BotProfileList::const_iterator iter; // count up valid profiles int validCount = 0; for (auto profile : m_profileList) { if (profile->IsDifficulty(difficulty) && !UTIL_IsNameTaken(profile->GetName()) && profile->IsValidForTeam(team)) validCount++; } if (validCount == 0) return nullptr; // select one at random int which = RANDOM_LONG(0, validCount - 1); for (auto profile : m_profileList) { if (profile->IsDifficulty(difficulty) && !UTIL_IsNameTaken(profile->GetName()) && profile->IsValidForTeam(team)) { if (which-- == 0) return profile; } } return nullptr; #else // we don't need random profiles when we're not in the game dll return nullptr; #endif // RANDOM_LONG }