/* * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * In addition, as a special exception, the author gives permission to * link the code of this program with the Half-Life Game Engine ("HL * Engine") and Modified Game Libraries ("MODs") developed by Valve, * L.L.C ("Valve"). You must obey the GNU General Public License in all * respects for all of the code used other than the HL Engine and MODs * from Valve. If you modify this file, you may extend this exception * to your version of the file, but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. * */ #include "precompiled.h" CBotManager *TheBots = nullptr; float CCSBotManager::m_flNextCVarCheck = 0.0f; bool CCSBotManager::m_isMapDataLoaded = false; bool CCSBotManager::m_isLearningMap = false; bool CCSBotManager::m_isAnalysisRequested = false; NavEditCmdType CCSBotManager::m_editCmd = EDIT_NONE; CCSBotManager::CCSBotManager() { m_flNextCVarCheck = 0.0f; m_zoneCount = 0; SetLooseBomb(nullptr); m_isBombPlanted = false; m_bombDefuser = nullptr; m_isLearningMap = false; m_isAnalysisRequested = false; m_editCmd = EDIT_NONE; m_navPlace = 0; m_roundStartTimestamp = 0.0f; m_bServerActive = false; TheBotPhrases = new BotPhraseManager; // load the database of bot radio chatter TheBotPhrases->Initialize("BotChatter.db", 0); TheBotProfiles = new BotProfileManager; // make sure default voice bank is first TheBotProfiles->FindVoiceBankIndex("BotChatter.db"); const char *filename; if (IS_CAREER_MATCH()) { filename = "MissionPacks/BotPackList.db"; } else { filename = "BotPackList.db"; } // read in the list of bot profile DBs int dataLength; char *dataPointer = (char *)LOAD_FILE_FOR_ME((char *)filename, &dataLength); if (!dataPointer) { TheBotProfiles->Init(cv_bot_profile_db.string); } else { char *dataFile = SharedParse(dataPointer); char *token; while (dataFile) { token = SharedGetToken(); char *clone = CloneString(token); TheBotProfiles->Init(clone); delete[] clone; dataFile = SharedParse(dataFile); } FREE_FILE(dataPointer); } // Now that we've parsed all the profiles, we have a list of the voice banks they're using. // Go back and parse the custom voice speakables. const BotProfileManager::VoiceBankList *pVoiceBanks = TheBotProfiles->GetVoiceBanks(); for (uint32 i = 1; i < pVoiceBanks->size(); i++) { TheBotPhrases->Initialize((*pVoiceBanks)[i], i); } #ifdef REGAMEDLL_FIXES AddServerCommands(); #endif } // Invoked when a new round begins void CCSBotManager::RestartRound() { // extend CBotManager::RestartRound(); SetLooseBomb(nullptr); m_isBombPlanted = false; m_earliestBombPlantTimestamp = gpGlobals->time + RANDOM_FLOAT(10.0f, 30.0f); m_bombDefuser = nullptr; m_editCmd = EDIT_NONE; ResetRadioMessageTimestamps(); m_lastSeenEnemyTimestamp = -9999.9f; m_roundStartTimestamp = gpGlobals->time + freezetime.value; // randomly decide if defensive team wants to "rush" as a whole const float defenseRushChance = 33.3f; // 25.0f; m_isDefenseRushing = (RANDOM_FLOAT(0.0f, 100.0f) <= defenseRushChance) ? true : false; TheBotPhrases->OnRoundRestart(); m_isRoundOver = false; m_isRespawnStarted = false; m_canRespawn = true; } // Called each frame void CCSBotManager::StartFrame() { // EXTEND CBotManager::StartFrame(); MonitorBotCVars(); // debug zone extent visualization if (cv_bot_debug.value == 5.0f) { for (int z = 0; z < m_zoneCount; z++) { Zone *zone = &m_zone[z]; UTIL_DrawBox(&zone->m_extent, 1, 255, 100, 0); } } } // Return true if the bot can use this weapon bool CCSBotManager::IsWeaponUseable(CBasePlayerItem *item) const { if (!item) { return false; } if (item->m_iId == WEAPON_C4) return true; WeaponClassType weaponClass = WeaponIDToWeaponClass(item->m_iId); if ((!AllowShotguns() && weaponClass == WEAPONCLASS_SHOTGUN) || (!AllowMachineGuns() && weaponClass == WEAPONCLASS_MACHINEGUN) || (!AllowRifles() && weaponClass == WEAPONCLASS_RIFLE) || (!AllowSnipers() && weaponClass == WEAPONCLASS_SNIPERRIFLE) || (!AllowSubMachineGuns() && weaponClass == WEAPONCLASS_SUBMACHINEGUN) || (!AllowTacticalShield() && item->m_iId == WEAPON_SHIELDGUN) || (!AllowPistols() && weaponClass == WEAPONCLASS_PISTOL) || (!AllowGrenades() && weaponClass == WEAPONCLASS_GRENADE)) { return false; } return true; } bool CCSBotManager::IsWeaponUseable(ArmouryItemPack item) const { WeaponClassType weaponClass = WeaponIDToWeaponClass(item); if ((!AllowShotguns() && weaponClass == WEAPONCLASS_SHOTGUN) || (!AllowMachineGuns() && weaponClass == WEAPONCLASS_MACHINEGUN) || (!AllowRifles() && weaponClass == WEAPONCLASS_RIFLE) || (!AllowSnipers() && weaponClass == WEAPONCLASS_SNIPERRIFLE) || (!AllowSubMachineGuns() && weaponClass == WEAPONCLASS_SUBMACHINEGUN) || (!AllowTacticalShield() && item == ARMOURY_SHIELD) || (!AllowPistols() && weaponClass == WEAPONCLASS_PISTOL) || (!AllowGrenades() && weaponClass == WEAPONCLASS_GRENADE)) { return false; } return true; } // Return true if this player is on "defense" bool CCSBotManager::IsOnDefense(CBasePlayer *pPlayer) const { switch (GetScenario()) { case SCENARIO_DEFUSE_BOMB: return (pPlayer->m_iTeam == CT); case SCENARIO_RESCUE_HOSTAGES: return (pPlayer->m_iTeam == TERRORIST); case SCENARIO_ESCORT_VIP: return (pPlayer->m_iTeam == TERRORIST); } return false; } // Return true if this player is on "offense" bool CCSBotManager::IsOnOffense(CBasePlayer *pPlayer) const { return !IsOnDefense(pPlayer); } // Invoked when a map has just been loaded void CCSBotManager::ServerActivate() { DestroyNavigationMap(); m_isMapDataLoaded = false; m_zoneCount = 0; m_gameScenario = SCENARIO_DEATHMATCH; ValidateMapData(); RestartRound(); m_isLearningMap = false; m_isAnalysisRequested = false; m_bServerActive = true; #ifndef REGAMEDLL_FIXES AddServerCommands(); #endif TheBotPhrases->OnMapChange(); } void CCSBotManager::AddServerCommand(const char *cmd) { ADD_SERVER_COMMAND((char *)cmd, Bot_ServerCommand); } void CCSBotManager::AddServerCommands() { static bool fFirstTime = true; if (!fFirstTime) return; fFirstTime = false; if (!AreBotsAllowed()) return; AddServerCommand("bot_about"); AddServerCommand("bot_add"); AddServerCommand("bot_add_t"); AddServerCommand("bot_add_ct"); AddServerCommand("bot_kill"); AddServerCommand("bot_kick"); AddServerCommand("bot_knives_only"); AddServerCommand("bot_pistols_only"); AddServerCommand("bot_snipers_only"); AddServerCommand("bot_all_weapons"); AddServerCommand("entity_dump"); AddServerCommand("bot_nav_delete"); AddServerCommand("bot_nav_split"); AddServerCommand("bot_nav_merge"); AddServerCommand("bot_nav_mark"); AddServerCommand("bot_nav_begin_area"); AddServerCommand("bot_nav_end_area"); AddServerCommand("bot_nav_connect"); AddServerCommand("bot_nav_disconnect"); AddServerCommand("bot_nav_splice"); AddServerCommand("bot_nav_crouch"); AddServerCommand("bot_nav_jump"); AddServerCommand("bot_nav_precise"); AddServerCommand("bot_nav_no_jump"); AddServerCommand("bot_nav_analyze"); AddServerCommand("bot_nav_strip"); AddServerCommand("bot_nav_save"); AddServerCommand("bot_nav_load"); AddServerCommand("bot_nav_use_place"); AddServerCommand("bot_nav_place_floodfill"); AddServerCommand("bot_nav_place_pick"); AddServerCommand("bot_nav_toggle_place_mode"); AddServerCommand("bot_nav_toggle_place_painting"); AddServerCommand("bot_goto_mark"); AddServerCommand("bot_memory_usage"); AddServerCommand("bot_nav_mark_unnamed"); AddServerCommand("bot_nav_warp"); AddServerCommand("bot_nav_corner_select"); AddServerCommand("bot_nav_corner_raise"); AddServerCommand("bot_nav_corner_lower"); AddServerCommand("bot_nav_check_consistency"); } void CCSBotManager::ServerDeactivate() { m_bServerActive = false; } void CCSBotManager::ClientDisconnect(CBasePlayer *pPlayer) { if (!pPlayer || !pPlayer->IsBot()) return; auto pevTemp = VARS(pPlayer->edict()); CCSBot *pBot = static_cast(pPlayer); pBot->Disconnect(); if (!FStringNull(pPlayer->pev->classname)) { RemoveEntityHashValue(pPlayer->pev, STRING(pPlayer->pev->classname), CLASSNAME); } FREE_PRIVATE(pPlayer->edict()); pPlayer = GetClassPtr((CBasePlayer *)pevTemp); AddEntityHashValue(pPlayer->pev, STRING(pPlayer->pev->classname), CLASSNAME); pPlayer->pev->flags = FL_DORMANT; } void PrintAllEntities() { for (int i = 1; i < gpGlobals->maxEntities; i++) { edict_t *edict = INDEXENT(i); if (!edict || FStringNull(edict->v.classname)) continue; CONSOLE_ECHO(" %s\n", STRING(edict->v.classname)); } } void CCSBotManager::ServerCommand(const char *pcmd) { if (!m_bServerActive || !AreBotsAllowed()) return; char buffer[400]; const char *msg = CMD_ARGV(1); if (FStrEq(pcmd, "bot_about")) { Q_snprintf(buffer, sizeof(buffer), "\n--------------------------------------------------------------------------\n" "The Official Counter-Strike Bot V%d.%02d\n" "Created by Michael S. Booth\n" "Web: www.turtlerockstudios.com\\csbot\n" "E-mail: csbot@turtlerockstudios.com\n" "--------------------------------------------------------------------------\n\n", BOT_VERSION_MAJOR, BOT_VERSION_MINOR); CONSOLE_ECHO(buffer); HintMessageToAllPlayers(buffer); } else if (FStrEq(pcmd, "bot_add")) { BotAddCommand(BOT_TEAM_ANY, FROM_CONSOLE); } else if (FStrEq(pcmd, "bot_add_t")) { BotAddCommand(BOT_TEAM_T, FROM_CONSOLE); } else if (FStrEq(pcmd, "bot_add_ct")) { BotAddCommand(BOT_TEAM_CT, FROM_CONSOLE); } else if (FStrEq(pcmd, "bot_kill")) { bool killThemAll; if (CMD_ARGC() == 1 || FStrEq(msg, "all")) killThemAll = true; else killThemAll = false; for (int i = 1; i <= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); if (!pPlayer) continue; if (FNullEnt(pPlayer->pev)) continue; const char *name = STRING(pPlayer->pev->netname); if (FStrEq(name, "")) continue; if (pPlayer->IsBot()) { if (killThemAll || FStrEq(name, msg)) { pPlayer->Kill(); } } } } else if (FStrEq(pcmd, "bot_kick")) { bool kickThemAll; if (CMD_ARGC() == 1 || FStrEq(msg, "all")) kickThemAll = true; else kickThemAll = false; for (int i = 1; i <= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); if (!pPlayer) continue; if (FNullEnt(pPlayer->pev)) continue; const char *name = STRING(pPlayer->pev->netname); if (FStrEq(name, "")) continue; if (pPlayer->IsBot()) { if (kickThemAll || FStrEq(name, msg)) { // adjust bot quota so kicked bot is not immediately added back in int newQuota = cv_bot_quota.value - 1; SERVER_COMMAND(UTIL_VarArgs("kick \"%s\"\n", name)); CVAR_SET_FLOAT("bot_quota", clamp(newQuota, 0, int(cv_bot_quota.value))); } } } if (kickThemAll || cv_bot_quota.value < 0.0f) { CVAR_SET_FLOAT("bot_quota", 0); } } else if (FStrEq(pcmd, "bot_knives_only")) { CVAR_SET_FLOAT("bot_allow_pistols", 0); CVAR_SET_FLOAT("bot_allow_shotguns", 0); CVAR_SET_FLOAT("bot_allow_sub_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_rifles", 0); CVAR_SET_FLOAT("bot_allow_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_grenades", 0); CVAR_SET_FLOAT("bot_allow_snipers", 0); CVAR_SET_FLOAT("bot_allow_shield", 0); } else if (FStrEq(pcmd, "bot_pistols_only")) { CVAR_SET_FLOAT("bot_allow_pistols", 1); CVAR_SET_FLOAT("bot_allow_shotguns", 0); CVAR_SET_FLOAT("bot_allow_sub_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_rifles", 0); CVAR_SET_FLOAT("bot_allow_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_grenades", 0); CVAR_SET_FLOAT("bot_allow_snipers", 0); CVAR_SET_FLOAT("bot_allow_shield", 0); } else if (FStrEq(pcmd, "bot_snipers_only")) { CVAR_SET_FLOAT("bot_allow_pistols", 1); CVAR_SET_FLOAT("bot_allow_shotguns", 0); CVAR_SET_FLOAT("bot_allow_sub_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_rifles", 0); CVAR_SET_FLOAT("bot_allow_machine_guns", 0); CVAR_SET_FLOAT("bot_allow_grenades", 0); CVAR_SET_FLOAT("bot_allow_snipers", 1); CVAR_SET_FLOAT("bot_allow_shield", 0); } else if (FStrEq(pcmd, "bot_all_weapons")) { CVAR_SET_FLOAT("bot_allow_pistols", 1); CVAR_SET_FLOAT("bot_allow_shotguns", 1); CVAR_SET_FLOAT("bot_allow_sub_machine_guns", 1); CVAR_SET_FLOAT("bot_allow_rifles", 1); CVAR_SET_FLOAT("bot_allow_machine_guns", 1); CVAR_SET_FLOAT("bot_allow_grenades", 1); CVAR_SET_FLOAT("bot_allow_snipers", 1); CVAR_SET_FLOAT("bot_allow_shield", 1); } else if (FStrEq(pcmd, "entity_dump")) { PrintAllEntities(); } else if (FStrEq(pcmd, "bot_nav_delete")) { m_editCmd = EDIT_DELETE; } else if (FStrEq(pcmd, "bot_nav_split")) { m_editCmd = EDIT_SPLIT; } else if (FStrEq(pcmd, "bot_nav_merge")) { m_editCmd = EDIT_MERGE; } else if (FStrEq(pcmd, "bot_nav_mark")) { m_editCmd = EDIT_MARK; } else if (FStrEq(pcmd, "bot_nav_begin_area")) { m_editCmd = EDIT_BEGIN_AREA; } else if (FStrEq(pcmd, "bot_nav_end_area")) { m_editCmd = EDIT_END_AREA; } else if (FStrEq(pcmd, "bot_nav_connect")) { m_editCmd = EDIT_CONNECT; } else if (FStrEq(pcmd, "bot_nav_disconnect")) { m_editCmd = EDIT_DISCONNECT; } else if (FStrEq(pcmd, "bot_nav_splice")) { m_editCmd = EDIT_SPLICE; } else if (FStrEq(pcmd, "bot_nav_crouch")) { m_editCmd = EDIT_ATTRIB_CROUCH; } else if (FStrEq(pcmd, "bot_nav_jump")) { m_editCmd = EDIT_ATTRIB_JUMP; } else if (FStrEq(pcmd, "bot_nav_precise")) { m_editCmd = EDIT_ATTRIB_PRECISE; } else if (FStrEq(pcmd, "bot_nav_no_jump")) { m_editCmd = EDIT_ATTRIB_NO_JUMP; } else if (FStrEq(pcmd, "bot_nav_analyze")) { m_isAnalysisRequested = true; } else if (FStrEq(pcmd, "bot_nav_strip")) { StripNavigationAreas(); } else if (FStrEq(pcmd, "bot_nav_save")) { GET_GAME_DIR(buffer); Q_strcat(buffer, "\\"); Q_strcat(buffer, CBotManager::GetNavMapFilename()); if (SaveNavigationMap(buffer)) CONSOLE_ECHO("Navigation map '%s' saved.\n", buffer); else CONSOLE_ECHO("ERROR: Cannot save navigation map '%s'.\n", buffer); } else if (FStrEq(pcmd, "bot_nav_load")) { ValidateMapData(); } else if (FStrEq(pcmd, "bot_nav_use_place")) { if (CMD_ARGC() == 1) { // no arguments = list all available places int i = 0; const BotPhraseList *placeList = TheBotPhrases->GetPlaceList(); for (auto phrase : *placeList) { if (phrase->GetID() == GetNavPlace()) CONSOLE_ECHO("--> %-26s", phrase->GetName()); else CONSOLE_ECHO("%-30s", phrase->GetName()); if (!(i % 3)) CONSOLE_ECHO("\n"); i++; } CONSOLE_ECHO("\n"); } else { // single argument = set current place const BotPhraseList *placeList = TheBotPhrases->GetPlaceList(); const BotPhrase *found = nullptr; bool isAmbiguous = false; for (auto phrase : *placeList) { if (!Q_strnicmp(phrase->GetName(), msg, Q_strlen(msg))) { // check for exact match in case of subsets of other strings if (!Q_strcmp(phrase->GetName(), msg)) { found = phrase; isAmbiguous = false; break; } if (found) { isAmbiguous = true; } else { found = phrase; } } } if (isAmbiguous) { CONSOLE_ECHO("Ambiguous\n"); } else { CONSOLE_ECHO("Current place set to '%s'\n", found->GetName()); SetNavPlace(found->GetID()); } } } else if (FStrEq(pcmd, "bot_nav_toggle_place_mode")) { m_editCmd = EDIT_TOGGLE_PLACE_MODE; } else if (FStrEq(pcmd, "bot_nav_place_floodfill")) { m_editCmd = EDIT_PLACE_FLOODFILL; } else if (FStrEq(pcmd, "bot_nav_place_pick")) { m_editCmd = EDIT_PLACE_PICK; } else if (FStrEq(pcmd, "bot_nav_toggle_place_painting")) { m_editCmd = EDIT_TOGGLE_PLACE_PAINTING; } else if (FStrEq(pcmd, "bot_goto_mark")) { // tell the first bot we find to go to our marked area CNavArea *area = GetMarkedArea(); if (area) { CBaseEntity *pEntity = nullptr; while ((pEntity = UTIL_FindEntityByClassname(pEntity, "player"))) { if (!pEntity->IsPlayer()) continue; if (pEntity->IsDormant()) continue; CBasePlayer *playerOrBot = GetClassPtr((CBasePlayer *)pEntity->pev); if (playerOrBot->IsBot()) { CCSBot *pBot = static_cast(playerOrBot); if (pBot) { pBot->MoveTo(&area->m_center, FASTEST_ROUTE); } break; } } } } else if (FStrEq(pcmd, "bot_memory_usage")) { CONSOLE_ECHO("Memory usage:\n"); CONSOLE_ECHO(" %d bytes per bot\b", sizeof(CCSBot)); CONSOLE_ECHO(" %d Navigation Areas @ %d bytes each = %d bytes\n", TheNavAreaGrid.GetNavAreaCount(), sizeof(CNavArea), TheNavAreaGrid.GetNavAreaCount() * sizeof(CNavArea)); CONSOLE_ECHO(" %d Hiding Spots @ %d bytes each = %d bytes\n", TheHidingSpotList.size(), sizeof(HidingSpot), sizeof(HidingSpot) * TheHidingSpotList.size()); unsigned int encounterMem = 0; for (auto area : TheNavAreaList) { for (auto se : area->m_spotEncounterList) { encounterMem += sizeof(SpotEncounter); encounterMem += sizeof(SpotOrder) * se.spotList.size(); } } CONSOLE_ECHO(" Encounter Spot data = %d bytes\n", encounterMem); } else if (FStrEq(pcmd, "bot_nav_mark_unnamed")) { m_editCmd = EDIT_MARK_UNNAMED; } else if (FStrEq(pcmd, "bot_nav_warp")) { m_editCmd = EDIT_WARP_TO_MARK; } else if (FStrEq(pcmd, "bot_nav_corner_select")) { m_editCmd = EDIT_SELECT_CORNER; } else if (FStrEq(pcmd, "bot_nav_corner_raise")) { m_editCmd = EDIT_RAISE_CORNER; } else if (FStrEq(pcmd, "bot_nav_corner_lower")) { m_editCmd = EDIT_LOWER_CORNER; } else if (FStrEq(pcmd, "bot_nav_check_consistency")) { if (CMD_ARGC() != 2) { CONSOLE_ECHO("usage: bot_nav_check_consistency \n"); return; } SanityCheckNavigationMap(msg); } } BOOL CCSBotManager::ClientCommand(CBasePlayer *pPlayer, const char *pcmd) { return FALSE; } // Process the "bot_add" console command bool CCSBotManager::BotAddCommand(BotProfileTeamType team, bool isFromConsole) { // dont allow bots to join if the Navigation Area is being generated if (m_isLearningMap) return false; const BotProfile *profile = nullptr; if (!isFromConsole || CMD_ARGC() < 2) { // if team not specified, check cv_bot_join_team cvar for preference if (team == BOT_TEAM_ANY) { if (!Q_stricmp(cv_bot_join_team.string, "T")) team = BOT_TEAM_T; else if (!Q_stricmp(cv_bot_join_team.string, "CT")) team = BOT_TEAM_CT; else { TeamName defaultTeam = CSGameRules()->SelectDefaultTeam(); if (defaultTeam == TERRORIST) team = BOT_TEAM_T; else if (defaultTeam == CT) team = BOT_TEAM_CT; } } // try to add a bot by name profile = TheBotProfiles->GetRandomProfile(GetDifficultyLevel(), team); if (!profile) { CONSOLE_ECHO("All bot profiles at this difficulty level are in use.\n"); #ifdef REGAMEDLL_FIXES // decrease the bot quota if (!isFromConsole) { CVAR_SET_FLOAT("bot_quota", cv_bot_quota.value - 1); } #endif return true; } } else { // in career, ignore humans bool ignoreHumans = false; if (CSGameRules() && CSGameRules()->IsCareer()) ignoreHumans = true; if (UTIL_IsNameTaken(CMD_ARGV(1), ignoreHumans)) { CONSOLE_ECHO("Error - %s is already in the game.\n", CMD_ARGV(1)); return true; } profile = TheBotProfiles->GetProfile(CMD_ARGV(1), team); if (!profile) { CONSOLE_ECHO("Error - no profile for '%s' exists.\n", CMD_ARGV(1)); return true; } } // create the bot if (AddBot(profile, team)) { if (isFromConsole) { // increase the bot quota to account for manually added bot CVAR_SET_FLOAT("bot_quota", cv_bot_quota.value + 1); } } #ifdef REGAMEDLL_FIXES else { // decrease the bot quota if (!isFromConsole) { CVAR_SET_FLOAT("bot_quota", cv_bot_quota.value - 1); } } #endif return true; } // Keep a minimum quota of bots in the game void CCSBotManager::MaintainBotQuota() { #ifdef REGAMEDLL_FIXES if (!AreBotsAllowed()) return; #endif if (m_isLearningMap) return; int totalHumansInGame = UTIL_HumansInGame(); int humanPlayersInGame = UTIL_HumansInGame(IGNORE_SPECTATORS); int spectatorPlayersInGame = UTIL_SpectatorsInGame(); // don't add bots until local player has been registered, to make sure he's player ID #1 if (!IS_DEDICATED_SERVER() && totalHumansInGame == 0) return; int desiredBotCount = int(cv_bot_quota.value); int occupiedBotSlots = UTIL_BotsInGame(); // isRoundInProgress is true if the round has progressed far enough that new players will join as dead. bool isRoundInProgress = CSGameRules()->IsGameStarted() && !TheCSBots()->IsRoundOver() && (CSGameRules()->GetRoundElapsedTime() >= CSGameRules()->GetRoundRespawnTime()); #ifdef REGAMEDLL_ADD if (FStrEq(cv_bot_quota_mode.string, "fill")) { // If bot_quota_mode is 'fill', we want the number of bots and humans together to equal bot_quota // unless the round is already in progress, in which case we play with what we've been dealt if (!isRoundInProgress) { desiredBotCount = Q_max(0, desiredBotCount - humanPlayersInGame + spectatorPlayersInGame); } else { desiredBotCount = occupiedBotSlots; } } else if (FStrEq(cv_bot_quota_mode.string, "match")) { // If bot_quota_mode is 'match', we want the number of bots to be bot_quota * total humans // unless the round is already in progress, in which case we play with what we've been dealt if (!isRoundInProgress) { desiredBotCount = Q_max(0, cv_bot_quota.value * humanPlayersInGame); } else { desiredBotCount = occupiedBotSlots; } } #else // #ifdef REGAMEDLL_ADD if (cv_bot_quota_match.value > 0.0) { desiredBotCount = int(humanPlayersInGame * cv_bot_quota_match.value); } #endif // #ifdef REGAMEDLL_ADD // wait for a player to join, if necessary if (cv_bot_join_after_player.value > 0.0) { if (humanPlayersInGame == 0 && spectatorPlayersInGame == 0) desiredBotCount = 0; } #ifdef REGAMEDLL_ADD // wait until the map has been loaded for a bit, to allow players to transition across // the transition without missing the pistol round if (static_cast(cv_bot_join_delay.value) > CSGameRules()->GetMapElapsedTime()) { desiredBotCount = 0; } #endif // if bots will auto-vacate, we need to keep one slot open to allow players to join if (cv_bot_auto_vacate.value > 0.0) desiredBotCount = Q_min(desiredBotCount, gpGlobals->maxClients - (humanPlayersInGame + 1)); else desiredBotCount = Q_min(desiredBotCount, gpGlobals->maxClients - humanPlayersInGame + spectatorPlayersInGame); #ifdef REGAMEDLL_FIXES // Try to balance teams, if we are in the first specified seconds of a round and bots can join either team. if (occupiedBotSlots > 0 && desiredBotCount == occupiedBotSlots && CSGameRules()->IsGameStarted()) { if (CSGameRules()->GetRoundElapsedTime() < CSGameRules()->GetRoundRespawnTime()) // new bots can still spawn during this time { if (autoteambalance.value > 0.0f) { int numAliveTerrorist; int numAliveCT; int numDeadTerrorist; int numDeadCT; CSGameRules()->InitializePlayerCounts(numAliveTerrorist, numAliveCT, numDeadTerrorist, numDeadCT); if (!FStrEq(cv_bot_join_team.string, "T") && !FStrEq(cv_bot_join_team.string, "CT")) { if (numAliveTerrorist > CSGameRules()->m_iNumCT + 1) { if (UTIL_KickBotFromTeam(TERRORIST)) return; } else if (numAliveCT > CSGameRules()->m_iNumTerrorist + 1) { if (UTIL_KickBotFromTeam(CT)) return; } } } } } #endif // #ifdef REGAMEDLL_FIXES // add bots if necessary if (desiredBotCount > occupiedBotSlots) { // don't try to add a bot if all teams are full if (!CSGameRules()->TeamFull(TERRORIST) || !CSGameRules()->TeamFull(CT)) { #ifndef REGAMEDLL_FIXES if (AreBotsAllowed()) #endif { BotAddCommand(BOT_TEAM_ANY); } } } else if (desiredBotCount < occupiedBotSlots) { // kick a bot to maintain quota // first remove any unassigned bots if (UTIL_KickBotFromTeam(UNASSIGNED)) return; TeamName kickTeam; // remove from the team that has more players if (CSGameRules()->m_iNumTerrorist > CSGameRules()->m_iNumCT) { kickTeam = TERRORIST; } else if (CSGameRules()->m_iNumTerrorist < CSGameRules()->m_iNumCT) { kickTeam = CT; } // remove from the team that's winning else if (CSGameRules()->m_iNumTerroristWins > CSGameRules()->m_iNumCTWins) { kickTeam = TERRORIST; } else if (CSGameRules()->m_iNumCTWins > CSGameRules()->m_iNumTerroristWins) { kickTeam = CT; } else { // teams and scores are equal, pick a team at random kickTeam = (RANDOM_LONG(0, 1) == 0) ? CT : TERRORIST; } // attempt to kick a bot from the given team bool atLeastOneKicked = UTIL_KickBotFromTeam(kickTeam); if (!atLeastOneKicked) { // if there were no bots on the team, kick a bot from the other team if (kickTeam == TERRORIST) atLeastOneKicked = UTIL_KickBotFromTeam(CT); else atLeastOneKicked = UTIL_KickBotFromTeam(TERRORIST); } if (atLeastOneKicked) { CONSOLE_ECHO("These bots kicked to maintain quota.\n"); } } else { if (CSGameRules() && !CSGameRules()->IsCareer()) return; bool humansAreCTs = (Q_strcmp(humans_join_team.string, "CT") == 0); if (humansAreCTs) { if (CSGameRules()->m_iNumCT <= 6) return; UTIL_KickBotFromTeam(CT); } else { if (CSGameRules()->m_iNumTerrorist <= 6) return; UTIL_KickBotFromTeam(TERRORIST); } CVAR_SET_FLOAT("bot_quota", cv_bot_quota.value - 1.0f); } } void CCSBotManager::MonitorBotCVars() { if (cv_bot_nav_edit.value != 0.0f) { EditNavAreas(m_editCmd); m_editCmd = EDIT_NONE; } if (gpGlobals->time >= m_flNextCVarCheck) { if (cv_bot_show_danger.value != 0.0f) DrawDanger(); MaintainBotQuota(); m_flNextCVarCheck = gpGlobals->time + 0.3f; } } // Collect all nav areas that overlap the given zone class CollectOverlappingAreas { public: CollectOverlappingAreas(CCSBotManager::Zone *zone) { m_zone = zone; zone->m_areaCount = 0; } bool operator()(CNavArea *area) { const Extent *areaExtent = area->GetExtent(); if (areaExtent->hi.x >= m_zone->m_extent.lo.x && areaExtent->lo.x <= m_zone->m_extent.hi.x && areaExtent->hi.y >= m_zone->m_extent.lo.y && areaExtent->lo.y <= m_zone->m_extent.hi.y && areaExtent->hi.z >= m_zone->m_extent.lo.z && areaExtent->lo.z <= m_zone->m_extent.hi.z) { // area overlaps m_zone m_zone->m_area[m_zone->m_areaCount++] = area; if (m_zone->m_areaCount == CCSBotManager::MAX_ZONE_NAV_AREAS) { return false; } } return true; } private: CCSBotManager::Zone *m_zone; }; // Search the map entities to determine the game scenario and define important zones. void CCSBotManager::ValidateMapData() { if (m_isMapDataLoaded || !AreBotsAllowed()) return; m_isMapDataLoaded = true; if (LoadNavigationMap()) { CONSOLE_ECHO("Failed to load navigation map.\n"); return; } CONSOLE_ECHO("Navigation map loaded.\n"); m_zoneCount = 0; m_gameScenario = SCENARIO_DEATHMATCH; // Search all entities in the map and set the game type and store all zones (bomb target, etc). CBaseEntity *pEntity = nullptr; for (int i = 1; i < gpGlobals->maxEntities; i++) { pEntity = CBaseEntity::Instance(INDEXENT(i)); if (!pEntity) continue; bool found = false; bool isLegacy = false; if (FClassnameIs(pEntity->pev, "func_bomb_target")) { m_gameScenario = SCENARIO_DEFUSE_BOMB; found = true; isLegacy = false; } else if (FClassnameIs(pEntity->pev, "info_bomb_target")) { m_gameScenario = SCENARIO_DEFUSE_BOMB; found = true; isLegacy = true; } else if (FClassnameIs(pEntity->pev, "func_hostage_rescue")) { m_gameScenario = SCENARIO_RESCUE_HOSTAGES; found = true; isLegacy = false; } else if (FClassnameIs(pEntity->pev, "info_hostage_rescue")) { m_gameScenario = SCENARIO_RESCUE_HOSTAGES; found = true; isLegacy = true; } else if (FClassnameIs(pEntity->pev, "hostage_entity")) { // some very old maps (ie: cs_assault) use info_player_start // as rescue zones, so set the scenario if there are hostages // in the map m_gameScenario = SCENARIO_RESCUE_HOSTAGES; } else if (FClassnameIs(pEntity->pev, "func_vip_safetyzone")) { m_gameScenario = SCENARIO_ESCORT_VIP; found = true; isLegacy = false; } if (found) { if (m_zoneCount < MAX_ZONES) { m_zone[m_zoneCount].m_center = isLegacy ? pEntity->pev->origin : (pEntity->pev->absmax + pEntity->pev->absmin) / 2.0f; m_zone[m_zoneCount].m_isLegacy = isLegacy; m_zone[m_zoneCount].m_index = m_zoneCount; m_zone[m_zoneCount].m_entity = pEntity; m_zoneCount++; } else { CONSOLE_ECHO("Warning: Too many zones, some will be ignored.\n"); } } } // If there are no zones and the scenario is hostage rescue, // use the info_player_start entities as rescue zones. if (m_zoneCount == 0 && m_gameScenario == SCENARIO_RESCUE_HOSTAGES) { pEntity = nullptr; while ((pEntity = UTIL_FindEntityByClassname(pEntity, "info_player_start"))) { #ifdef REGAMEDLL_FIXES if (m_zoneCount >= MAX_ZONES) break; #endif if (FNullEnt(pEntity->edict())) break; if (m_zoneCount < MAX_ZONES) { m_zone[m_zoneCount].m_center = pEntity->pev->origin; m_zone[m_zoneCount].m_isLegacy = true; m_zone[m_zoneCount].m_index = m_zoneCount; m_zone[m_zoneCount].m_entity = pEntity; m_zoneCount++; } else { CONSOLE_ECHO("Warning: Too many zones, some will be ignored.\n"); } } } // Collect nav areas that overlap each zone for (int i = 0; i < m_zoneCount; i++) { Zone *zone = &m_zone[i]; if (zone->m_isLegacy) { const float legacyRange = 256.0f; zone->m_extent.lo.x = zone->m_center.x - legacyRange; zone->m_extent.lo.y = zone->m_center.y - legacyRange; zone->m_extent.lo.z = zone->m_center.z - legacyRange; zone->m_extent.hi.x = zone->m_center.x + legacyRange; zone->m_extent.hi.y = zone->m_center.y + legacyRange; zone->m_extent.hi.z = zone->m_center.z + legacyRange; } else { zone->m_extent.lo = zone->m_entity->pev->absmin; zone->m_extent.hi = zone->m_entity->pev->absmax; } // ensure Z overlap const float zFudge = 50.0f; zone->m_extent.lo.z -= zFudge; zone->m_extent.hi.z += zFudge; // build a list of nav areas that overlap this zone CollectOverlappingAreas collector(zone); ForAllAreas(collector); } } bool CCSBotManager::AddBot(const BotProfile *profile, BotProfileTeamType team) { if (!AreBotsAllowed()) return false; int nTeamSlot = UNASSIGNED; if (team == BOT_TEAM_ANY) { // if team not specified, check cv_bot_join_team cvar for preference if (!Q_stricmp(cv_bot_join_team.string, "T")) nTeamSlot = TERRORIST; else if (!Q_stricmp(cv_bot_join_team.string, "CT")) nTeamSlot = CT; } else if (team == BOT_TEAM_CT) nTeamSlot = CT; else if (team == BOT_TEAM_T) nTeamSlot = TERRORIST; if (nTeamSlot == UNASSIGNED) { nTeamSlot = CSGameRules()->SelectDefaultTeam(); } if (nTeamSlot == UNASSIGNED || CSGameRules()->TeamFull(nTeamSlot)) { CONSOLE_ECHO("Could not add bot to the game: Team is full\n"); return false; } if (CSGameRules()->TeamStacked(nTeamSlot, UNASSIGNED)) { CONSOLE_ECHO("Could not add bot to the game: Team is stacked (to disable this check, set mp_limitteams and mp_autoteambalance to zero and restart the round).\n"); return false; } CCSBot *pBot = CreateBot(profile); if (!pBot) { return false; } //int nJoinedTeam; ClientPutInServer(pBot->edict()); SET_CLIENT_KEY_VALUE(pBot->entindex(), GET_INFO_BUFFER(pBot->edict()), "*bot", "1"); pBot->m_iMenu = Menu_ChooseTeam; pBot->m_iJoiningState = PICKINGTEAM; if (HandleMenu_ChooseTeam(pBot, nTeamSlot)) { int skin = profile->GetSkin(); if (!skin) skin = 6; HandleMenu_ChooseAppearance(pBot, skin); if (IS_DEDICATED_SERVER()) { UTIL_DPrintf("Added bot %s to server\n", STRING(pBot->pev->netname)); } return true; } SERVER_COMMAND(UTIL_VarArgs("kick \"%s\"\n", STRING(pBot->pev->netname))); CONSOLE_ECHO("Could not add bot to the game.\n"); return false; } // Return the zone that contains the given position const CCSBotManager::Zone *CCSBotManager::GetZone(const Vector *pos) const { for (int z = 0; z < m_zoneCount; z++) { if (m_zone[z].m_extent.Contains(pos)) { return &m_zone[z]; } } return nullptr; } // Return the closest zone to the given position const CCSBotManager::Zone *CCSBotManager::GetClosestZone(const Vector *pos) const { const Zone *close = nullptr; float closeRangeSq = 1e9f; for (int z = 0; z < m_zoneCount; z++) { float rangeSq = (m_zone[z].m_center - (*pos)).LengthSquared(); if (rangeSq < closeRangeSq) { closeRangeSq = rangeSq; close = &m_zone[z]; } } return close; } // Return a random position inside the given zone const Vector *CCSBotManager::GetRandomPositionInZone(const Zone *zone) const { static Vector pos; if (!zone) return nullptr; if (zone->m_areaCount == 0) return nullptr; // pick a random overlapping area CNavArea *area = GetRandomAreaInZone(zone); // pick a location inside both the nav area and the zone // TODO: Randomize this if (zone->m_isLegacy) { // TODO: It is possible that the radius might not overlap this area at all... area->GetClosestPointOnArea(&zone->m_center, &pos); } else { const Extent &areaExtent = *area->GetExtent(); Extent overlap; overlap.lo.x = Q_max(areaExtent.lo.x, zone->m_extent.lo.x); overlap.lo.y = Q_max(areaExtent.lo.y, zone->m_extent.lo.y); overlap.hi.x = Q_min(areaExtent.hi.x, zone->m_extent.hi.x); overlap.hi.y = Q_min(areaExtent.hi.y, zone->m_extent.hi.y); pos.x = (overlap.lo.x + overlap.hi.x) / 2.0f; pos.y = (overlap.lo.y + overlap.hi.y) / 2.0f; pos.z = area->GetZ(&pos); } return &pos; } // Return a random area inside the given zone CNavArea *CCSBotManager::GetRandomAreaInZone(const Zone *zone) const { // TODO: improvement is needed if (!zone->m_areaCount) return nullptr; return zone->m_area[RANDOM_LONG(0, zone->m_areaCount - 1)]; } void CCSBotManager::OnEvent(GameEventType event, CBaseEntity *pEntity, CBaseEntity *pOther) { switch (event) { case EVENT_BOMB_PLANTED: m_isBombPlanted = true; m_bombPlantTimestamp = gpGlobals->time; break; case EVENT_BOMB_DEFUSING: m_bombDefuser = static_cast(pEntity); break; case EVENT_BOMB_DEFUSE_ABORTED: m_bombDefuser = nullptr; break; case EVENT_BOMB_DEFUSED: m_isBombPlanted = false; m_bombDefuser = nullptr; break; case EVENT_TERRORISTS_WIN: case EVENT_CTS_WIN: case EVENT_ROUND_DRAW: m_isRoundOver = true; break; case EVENT_RADIO_ENEMY_SPOTTED: m_lastSeenEnemyTimestamp = gpGlobals->time; SetLastSeenEnemyTimestamp(); break; default: break; } CBotManager::OnEvent(event, pEntity, pOther); } // Get the time remaining before the planted bomb explodes float CCSBotManager::GetBombTimeLeft() const { return (CSGameRules()->m_iC4Timer - (gpGlobals->time - m_bombPlantTimestamp)); } void CCSBotManager::SetLooseBomb(CBaseEntity *bomb) { m_looseBomb = bomb; if (bomb) { m_looseBombArea = TheNavAreaGrid.GetNearestNavArea(&bomb->pev->origin); } else { m_looseBombArea = nullptr; } } // Return true if player is important to scenario (VIP, bomb carrier, etc) bool CCSBotManager::IsImportantPlayer(CBasePlayer *pPlayer) const { switch (GetScenario()) { case SCENARIO_DEFUSE_BOMB: { if (pPlayer->m_iTeam == TERRORIST && pPlayer->IsBombGuy()) return true; // TODO: TEAM_CT's defusing the bomb are important return false; } case SCENARIO_ESCORT_VIP: { if (pPlayer->m_iTeam == CT && pPlayer->m_bIsVIP) return true; return false; } case SCENARIO_RESCUE_HOSTAGES: { // TODO: TEAM_CT's escorting hostages are important return false; } } // everyone is equally important in a deathmatch return false; } // Return priority of player (0 = max pri) unsigned int CCSBotManager::GetPlayerPriority(CBasePlayer *pPlayer) const { const unsigned int lowestPriority = 0xFFFFFFFF; if (!pPlayer->IsPlayer()) return lowestPriority; // human players have highest priority if (!pPlayer->IsBot()) return 0; CCSBot *pBot = static_cast(pPlayer); // bots doing something important for the current scenario have high priority switch (GetScenario()) { case SCENARIO_DEFUSE_BOMB: { // the bomb carrier has high priority if (pBot->m_iTeam == TERRORIST && pBot->m_bHasC4) return 1; break; } case SCENARIO_ESCORT_VIP: { // the VIP has high priority if (pBot->m_iTeam == CT && pBot->m_bIsVIP) return 1; break; } case SCENARIO_RESCUE_HOSTAGES: { // TEAM_CT's rescuing hostages have high priority if (pBot->m_iTeam == CT && pBot->GetHostageEscortCount()) return 1; break; } } // everyone else is ranked by their unique ID (which cannot be zero) return 1 + pBot->GetID(); } // Return the last time the given radio message was sent for given team // 'teamID' can be TEAM_CT or TEAM_TERRORIST float CCSBotManager::GetRadioMessageTimestamp(GameEventType event, int teamID) const { if (event <= EVENT_START_RADIO_1 || event >= EVENT_END_RADIO) return 0.0f; int i = (teamID == TERRORIST) ? 0 : 1; return m_radioMsgTimestamp[event - EVENT_START_RADIO_1][i]; } // Return the interval since the last time this message was sent float CCSBotManager::GetRadioMessageInterval(GameEventType event, int teamID) const { if (event <= EVENT_START_RADIO_1 || event >= EVENT_END_RADIO) return 99999999.9f; int i = (teamID == TERRORIST) ? 0 : 1; return gpGlobals->time - m_radioMsgTimestamp[event - EVENT_START_RADIO_1][i]; } // Set the given radio message timestamp. // 'teamID' can be TEAM_CT or TEAM_TERRORIST void CCSBotManager::SetRadioMessageTimestamp(GameEventType event, int teamID) { if (event <= EVENT_START_RADIO_1 || event >= EVENT_END_RADIO) return; int i = (teamID == TERRORIST) ? 0 : 1; m_radioMsgTimestamp[event - EVENT_START_RADIO_1][i] = gpGlobals->time; } // Reset all radio message timestamps void CCSBotManager::ResetRadioMessageTimestamps() { Q_memset(m_radioMsgTimestamp, 0, sizeof(m_radioMsgTimestamp)); } void CCSBotManager::OnFreeEntPrivateData(CBaseEntity *pEntity) { for (int i = 1; i <= gpGlobals->maxClients; i++) { CBasePlayer *pPlayer = UTIL_PlayerByIndex(i); if (!pPlayer || pPlayer->IsDormant()) continue; if (pPlayer->IsBot()) { CCSBot *pBot = static_cast(pPlayer); if (pBot->m_attacker == pEntity) pBot->m_attacker = nullptr; if (pBot->m_bomber == pEntity) pBot->m_bomber = nullptr; } } }