Added custom configs. These are used for replacing some hardcoded strings inside bot code, currently custom cvar for parachute detection is available, as well as custom c4 model names. Added editorconfig, and fixed CRLF for files (was a mix between LF & CRLF). Fixed use-after-free sanitizer error with chatlib. Fixed configs files loaded with memory-loader does not process last line in config files.
1835 lines
57 KiB
C++
1835 lines
57 KiB
C++
//
|
|
// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge.
|
|
// Copyright © 2004-2020 YaPB Project <yapb@jeefo.net>.
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
|
|
#include <yapb.h>
|
|
|
|
ConVar cv_autovacate ("yb_autovacate", "1", "Kick bots to automatically make room for human players.");
|
|
|
|
ConVar cv_quota ("yb_quota", "9", "Specifies the number bots to be added to the game.", true, 0.0f, static_cast <float> (kGameMaxPlayers));
|
|
ConVar cv_quota_mode ("yb_quota_mode", "normal", "Specifies the type of quota.\nAllowed values: 'normal', 'fill', and 'match'.\nIf 'fill', the server will adjust bots to keep N players in the game, where N is yb_quota.\nIf 'match', the server will maintain a 1:N ratio of humans to bots, where N is yb_quota_match.", false);
|
|
ConVar cv_quota_match ("yb_quota_match", "0", "Number of players to match if yb_quota_mode set to 'match'", true, 0.0f, static_cast <float> (kGameMaxPlayers));
|
|
ConVar cv_think_fps ("yb_think_fps", "30.0", "Specifies how many times per second bot code will run.", true, 30.0f, 90.0f);
|
|
ConVar cv_autokill_delay ("yb_autokill_delay", "0.0", "Specifies amount of time in seconds when bots will be killed if no humans left alive.", true, 0.0f, 90.0f);
|
|
|
|
ConVar cv_join_after_player ("yb_join_after_player", "0", "Specifies whether bots should join server, only when at least one human player in game.");
|
|
ConVar cv_join_team ("yb_join_team", "any", "Forces all bots to join team specified here.", false);
|
|
ConVar cv_join_delay ("yb_join_delay", "5.0", "Specifies after how many seconds bots should start to join the game after the changelevel.", true, 0.0f, 30.0f);
|
|
ConVar cv_name_prefix ("yb_name_prefix", "", "All the bot names will be prefixed with string specified with this cvar.", false);
|
|
|
|
ConVar cv_difficulty ("yb_difficulty", "4", "All bots difficulty level. Changing at runtime will affect already created bots.", true, 0.0f, 4.0f);
|
|
|
|
ConVar cv_difficulty_min ("yb_difficulty_min", "-1", "Lower bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f);
|
|
ConVar cv_difficulty_max ("yb_difficulty_max", "-1", "Upper bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f);
|
|
ConVar cv_difficulty_auto ("yb_difficulty_auto", "0", "Enables each bot balances own difficulty based kd-ratio of team.", true, 0.0f, 1.0f);
|
|
|
|
ConVar cv_show_avatars ("yb_show_avatars", "1", "Enables or disabels displaying bot avatars in front of their names in scoreboard. Note, that is currently you can see only avatars of your steam friends.");
|
|
ConVar cv_show_latency ("yb_show_latency", "2", "Enables latency display in scoreboard.\nAllowed values: '0', '1', '2'.\nIf '0', there is nothing displayed.\nIf '1', there is a 'BOT' is displayed.\nIf '2' fake ping is displayed.", true, 0.0f, 2.0f);
|
|
|
|
ConVar cv_botskin_t ("yb_botskin_t", "0", "Specifies the bots wanted skin for Terrorist team.", true, 0.0f, 5.0f);
|
|
ConVar cv_botskin_ct ("yb_botskin_ct", "0", "Specifies the bots wanted skin for CT team.", true, 0.0f, 5.0f);
|
|
|
|
ConVar cv_ping_base_min ("yb_ping_base_min", "7", "Lower bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f);
|
|
ConVar cv_ping_base_max ("yb_ping_base_max", "34", "Upper bound for base bot ping shown in scoreboard upon creation.", true, 0.0f, 100.0f);
|
|
|
|
ConVar cv_language ("yb_language", "en", "Specifies the language for bot messages and menus.", false);
|
|
|
|
ConVar mp_limitteams ("mp_limitteams", nullptr, Var::GameRef);
|
|
ConVar mp_autoteambalance ("mp_autoteambalance", nullptr, Var::GameRef);
|
|
ConVar mp_roundtime ("mp_roundtime", nullptr, Var::GameRef);
|
|
ConVar mp_timelimit ("mp_timelimit", nullptr, Var::GameRef);
|
|
ConVar mp_freezetime ("mp_freezetime", nullptr, Var::GameRef, true, "0");
|
|
|
|
BotManager::BotManager () {
|
|
// this is a bot manager class constructor
|
|
|
|
m_lastDifficulty = 0;
|
|
m_lastWinner = -1;
|
|
|
|
m_timeRoundStart = 0.0f;
|
|
m_timeRoundMid = 0.0f;
|
|
m_timeRoundEnd = 0.0f;
|
|
|
|
m_autoKillCheckTime = 0.0f;
|
|
m_maintainTime = 0.0f;
|
|
m_quotaMaintainTime = 0.0f;
|
|
|
|
m_bombPlanted = false;
|
|
m_botsCanPause = false;
|
|
m_roundOver = false;
|
|
|
|
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
|
|
|
|
for (int i = 0; i < kGameTeamNum; ++i) {
|
|
m_leaderChoosen[i] = false;
|
|
m_economicsGood[i] = true;
|
|
|
|
m_lastRadioTime[i] = 0.0f;
|
|
m_lastRadio[i] = -1;
|
|
}
|
|
reset ();
|
|
|
|
m_addRequests.clear ();
|
|
m_killerEntity = nullptr;
|
|
|
|
initFilters ();
|
|
}
|
|
|
|
void BotManager::createKillerEntity () {
|
|
// this function creates single trigger_hurt for using in Bot::kill, to reduce lags, when killing all the bots
|
|
|
|
m_killerEntity = engfuncs.pfnCreateNamedEntity (MAKE_STRING ("trigger_hurt"));
|
|
|
|
m_killerEntity->v.dmg = kInfiniteDistance;
|
|
m_killerEntity->v.dmg_take = 1.0f;
|
|
m_killerEntity->v.dmgtime = 2.0f;
|
|
m_killerEntity->v.effects |= EF_NODRAW;
|
|
|
|
engfuncs.pfnSetOrigin (m_killerEntity, Vector (-kInfiniteDistance, -kInfiniteDistance, -kInfiniteDistance));
|
|
MDLL_Spawn (m_killerEntity);
|
|
}
|
|
|
|
void BotManager::destroyKillerEntity () {
|
|
if (!game.isNullEntity (m_killerEntity)) {
|
|
engfuncs.pfnRemoveEntity (m_killerEntity);
|
|
m_killerEntity = nullptr;
|
|
}
|
|
}
|
|
|
|
void BotManager::touchKillerEntity (Bot *bot) {
|
|
|
|
// bot is already dead.
|
|
if (!bot->m_notKilled) {
|
|
return;
|
|
}
|
|
|
|
if (game.isNullEntity (m_killerEntity)) {
|
|
createKillerEntity ();
|
|
|
|
if (game.isNullEntity (m_killerEntity)) {
|
|
MDLL_ClientKill (bot->ent ());
|
|
return;
|
|
}
|
|
}
|
|
const auto &prop = conf.getWeaponProp (bot->m_currentWeapon);
|
|
|
|
m_killerEntity->v.classname = MAKE_STRING (prop.classname.chars ());
|
|
m_killerEntity->v.dmg_inflictor = bot->ent ();
|
|
m_killerEntity->v.dmg = (bot->pev->health + bot->pev->armorvalue) * 4.0f;
|
|
|
|
KeyValueData kv;
|
|
kv.szClassName = const_cast <char *> (prop.classname.chars ());
|
|
kv.szKeyName = "damagetype";
|
|
kv.szValue = const_cast <char *> (strings.format ("%d", cr::bit (4)));
|
|
kv.fHandled = HLFalse;
|
|
|
|
MDLL_KeyValue (m_killerEntity, &kv);
|
|
MDLL_Touch (m_killerEntity, bot->ent ());
|
|
}
|
|
|
|
void BotManager::execGameEntity (edict_t *ent) {
|
|
// this function calls gamedll player() function, in case to create player entity in game
|
|
|
|
if (game.is (GameFlags::Metamod)) {
|
|
CALL_GAME_ENTITY (PLID, "player", &ent->v);
|
|
return;
|
|
}
|
|
ents.callPlayerFunction (ent);
|
|
}
|
|
|
|
void BotManager::forEach (ForEachBot handler) {
|
|
for (const auto &bot : m_bots) {
|
|
if (handler (bot.get ())) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
BotCreateResult BotManager::create (StringRef name, int difficulty, int personality, int team, int skin) {
|
|
// this function completely prepares bot entity (edict) for creation, creates team, difficulty, sets named etc, and
|
|
// then sends result to bot constructor
|
|
|
|
edict_t *bot = nullptr;
|
|
String resultName;
|
|
|
|
// do not allow create bots when there is no graph
|
|
if (!graph.length ()) {
|
|
ctrl.msg ("There is no graph found. Cannot create bot.");
|
|
return BotCreateResult::GraphError;
|
|
}
|
|
|
|
// don't allow creating bots with changed graph (distance tables are messed up)
|
|
else if (graph.hasChanged ()) {
|
|
ctrl.msg ("Graph has been changed. Load graph again...");
|
|
return BotCreateResult::GraphError;
|
|
}
|
|
else if (team != -1 && isTeamStacked (team - 1)) {
|
|
ctrl.msg ("Desired team is stacked. Unable to proceed with bot creation.");
|
|
return BotCreateResult::TeamStacked;
|
|
}
|
|
if (difficulty < 0 || difficulty > 4) {
|
|
difficulty = cv_difficulty.int_ ();
|
|
|
|
if (difficulty < 0 || difficulty > 4) {
|
|
difficulty = rg.get (3, 4);
|
|
cv_difficulty.set (difficulty);
|
|
}
|
|
}
|
|
|
|
if (personality < Personality::Normal || personality > Personality::Careful) {
|
|
if (rg.chance (50)) {
|
|
personality = Personality::Normal;
|
|
}
|
|
else {
|
|
if (rg.chance (50)) {
|
|
personality = Personality::Rusher;
|
|
}
|
|
else {
|
|
personality = Personality::Careful;
|
|
}
|
|
}
|
|
}
|
|
BotName *botName = nullptr;
|
|
|
|
// setup name
|
|
if (name.empty ()) {
|
|
botName = conf.pickBotName ();
|
|
|
|
if (botName) {
|
|
resultName = botName->name;
|
|
}
|
|
else {
|
|
resultName.assignf ("%s_%d.%d", product.folder, rg.get (100, 10000), rg.get (100, 10000)); // just pick ugly random name
|
|
}
|
|
}
|
|
else {
|
|
resultName = name;
|
|
}
|
|
|
|
if (!strings.isEmpty (cv_name_prefix.str ())) {
|
|
String prefixed; // temp buffer for storing modified name
|
|
prefixed.assignf ("%s %s", cv_name_prefix.str (), resultName);
|
|
|
|
// buffer has been modified, copy to real name
|
|
resultName = cr::move (prefixed);
|
|
}
|
|
bot = engfuncs.pfnCreateFakeClient (resultName.chars ());
|
|
|
|
if (game.isNullEntity (bot)) {
|
|
ctrl.msg ("Maximum players reached (%d/%d). Unable to create Bot.", game.maxClients (), game.maxClients ());
|
|
return BotCreateResult::MaxPlayersReached;
|
|
}
|
|
auto object = cr::makeUnique <Bot> (bot, difficulty, personality, team, skin);
|
|
auto index = object->index ();
|
|
|
|
// assign owner of bot name
|
|
if (botName != nullptr) {
|
|
botName->usedBy = index; // save by who name is used
|
|
}
|
|
m_bots.push (cr::move (object));
|
|
|
|
ctrl.msg ("Connecting Bot...");
|
|
|
|
return BotCreateResult::Success;
|
|
}
|
|
|
|
Bot *BotManager::findBotByIndex (int index) {
|
|
// this function finds a bot specified by index, and then returns pointer to it (using own bot array)
|
|
|
|
if (index < 0 || index >= kGameMaxPlayers) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_index == index) {
|
|
return bot.get ();
|
|
}
|
|
}
|
|
return nullptr; // no bot
|
|
}
|
|
|
|
Bot *BotManager::findBotByEntity (edict_t *ent) {
|
|
// same as above, but using bot entity
|
|
|
|
return findBotByIndex (game.indexOfPlayer (ent));
|
|
}
|
|
|
|
Bot *BotManager::findAliveBot () {
|
|
// this function finds one bot, alive bot :)
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_notKilled) {
|
|
return bot.get ();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void BotManager::frame () {
|
|
// this function calls showframe function for all available at call moment bots
|
|
|
|
for (const auto &bot : m_bots) {
|
|
bot->frame ();
|
|
}
|
|
}
|
|
|
|
void BotManager::addbot (StringRef name, int difficulty, int personality, int team, int skin, bool manual) {
|
|
// this function putting bot creation process to queue to prevent engine crashes
|
|
|
|
BotRequest request {};
|
|
|
|
// fill the holder
|
|
request.name = name;
|
|
request.difficulty = difficulty;
|
|
request.personality = personality;
|
|
request.team = team;
|
|
request.skin = skin;
|
|
request.manual = manual;
|
|
|
|
// put to queue
|
|
m_addRequests.emplaceLast (cr::move (request));
|
|
}
|
|
|
|
void BotManager::addbot (StringRef name, StringRef difficulty, StringRef personality, StringRef team, StringRef skin, bool manual) {
|
|
// this function is same as the function above, but accept as parameters string instead of integers
|
|
|
|
BotRequest request {};
|
|
StringRef any = "*";
|
|
|
|
request.name = (name.empty () || name == any) ? StringRef ("\0") : name;
|
|
request.difficulty = (difficulty.empty () || difficulty == any) ? -1 : difficulty.int_ ();
|
|
request.team = (team.empty () || team == any) ? -1 : team.int_ ();
|
|
request.skin = (skin.empty () || skin == any) ? -1 : skin.int_ ();
|
|
request.personality = (personality.empty () || personality == any) ? -1 : personality.int_ ();
|
|
request.manual = manual;
|
|
|
|
m_addRequests.emplaceLast (cr::move (request));
|
|
}
|
|
|
|
void BotManager::maintainQuota () {
|
|
// this function keeps number of bots up to date, and don't allow to maintain bot creation
|
|
// while creation process in process.
|
|
|
|
if (graph.length () < 1 || graph.hasChanged ()) {
|
|
if (cv_quota.int_ () > 0) {
|
|
ctrl.msg ("There is no graph found. Cannot create bot.");
|
|
}
|
|
cv_quota.set (0);
|
|
return;
|
|
}
|
|
|
|
// bot's creation update
|
|
if (!m_addRequests.empty () && m_maintainTime < game.time ()) {
|
|
const BotRequest &request = m_addRequests.popFront ();
|
|
const BotCreateResult createResult = create (request.name, request.difficulty, request.personality, request.team, request.skin);
|
|
|
|
if (request.manual) {
|
|
cv_quota.set (cv_quota.int_ () + 1);
|
|
}
|
|
|
|
// check the result of creation
|
|
if (createResult == BotCreateResult::GraphError) {
|
|
m_addRequests.clear (); // something wrong with graph, reset tab of creation
|
|
cv_quota.set (0); // reset quota
|
|
}
|
|
else if (createResult == BotCreateResult::MaxPlayersReached) {
|
|
m_addRequests.clear (); // maximum players reached, so set quota to maximum players
|
|
cv_quota.set (getBotCount ());
|
|
}
|
|
else if (createResult == BotCreateResult::TeamStacked) {
|
|
ctrl.msg ("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)");
|
|
|
|
m_addRequests.clear ();
|
|
cv_quota.set (getBotCount ());
|
|
}
|
|
m_maintainTime = game.time () + 0.10f;
|
|
}
|
|
|
|
// now keep bot number up to date
|
|
if (m_quotaMaintainTime > game.time ()) {
|
|
return;
|
|
}
|
|
cv_quota.set (cr::clamp <int> (cv_quota.int_ (), 0, game.maxClients ()));
|
|
|
|
int totalHumansInGame = getHumansCount ();
|
|
int humanPlayersInGame = getHumansCount (true);
|
|
|
|
if (!game.isDedicated () && !totalHumansInGame) {
|
|
return;
|
|
}
|
|
|
|
int desiredBotCount = cv_quota.int_ ();
|
|
int botsInGame = getBotCount ();
|
|
|
|
if (strings.matches (cv_quota_mode.str (), "fill")) {
|
|
botsInGame += humanPlayersInGame;
|
|
}
|
|
else if (strings.matches (cv_quota_mode.str (), "match")) {
|
|
int detectQuotaMatch = cv_quota_match.int_ () == 0 ? cv_quota.int_ () : cv_quota_match.int_ ();
|
|
|
|
desiredBotCount = cr::max <int> (0, detectQuotaMatch * humanPlayersInGame);
|
|
}
|
|
|
|
if (cv_join_after_player.bool_ () && humanPlayersInGame == 0) {
|
|
desiredBotCount = 0;
|
|
}
|
|
int maxClients = game.maxClients ();
|
|
|
|
if (cv_autovacate.bool_ ()) {
|
|
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - (humanPlayersInGame + 1));
|
|
}
|
|
else {
|
|
desiredBotCount = cr::min <int> (desiredBotCount, maxClients - humanPlayersInGame);
|
|
}
|
|
int maxSpawnCount = game.getSpawnCount (Team::Terrorist) + game.getSpawnCount (Team::CT) - humanPlayersInGame;
|
|
|
|
// sent message only to console from here
|
|
ctrl.setFromConsole (true);
|
|
|
|
// add bots if necessary
|
|
if (desiredBotCount > botsInGame && botsInGame < maxSpawnCount) {
|
|
createRandom ();
|
|
}
|
|
else if (desiredBotCount < botsInGame) {
|
|
auto tp = countTeamPlayers ();
|
|
|
|
bool isKicked = false;
|
|
|
|
if (tp.first > tp.second) {
|
|
isKicked = kickRandom (false, Team::Terrorist);
|
|
}
|
|
else if (tp.first < tp.second) {
|
|
isKicked = kickRandom (false, Team::CT);
|
|
}
|
|
else {
|
|
isKicked = kickRandom (false, Team::Unassigned);
|
|
}
|
|
|
|
// if we can't kick player from correct team, just kick any random to keep quota control work
|
|
if (!isKicked) {
|
|
kickRandom (false, Team::Unassigned);
|
|
}
|
|
}
|
|
m_quotaMaintainTime = game.time () + 0.40f;
|
|
}
|
|
|
|
void BotManager::maintainLeaders () {
|
|
|
|
// select leader each team somewhere in round start
|
|
if (m_timeRoundStart + 5.0f > game.time () && m_timeRoundStart + 10.0f < game.time ()) {
|
|
for (int team = 0; team < kGameTeamNum; ++team) {
|
|
selectLeaders (team, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::maintainAutoKill () {
|
|
const float killDelay = cv_autokill_delay.float_ ();
|
|
|
|
if (killDelay < 1.0f || m_roundOver) {
|
|
return;
|
|
}
|
|
|
|
// check if we're reached the delay, so kill out bots
|
|
if (!cr::fzero (m_autoKillCheckTime) && m_autoKillCheckTime < game.time ()) {
|
|
killAllBots ();
|
|
m_autoKillCheckTime = 0.0f;
|
|
|
|
return;
|
|
}
|
|
|
|
int aliveBots = 0;
|
|
|
|
// do not interrupt bomb-defuse scenario
|
|
if (game.mapIs (MapFlags::Demolition) && isBombPlanted ()) {
|
|
return;
|
|
}
|
|
int totalHumans = getHumansCount (true); // we're ignore spectators intentionally
|
|
|
|
// if we're have no humans in teams do not bother to proceed
|
|
if (!totalHumans) {
|
|
return;
|
|
}
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_notKilled) {
|
|
++aliveBots;
|
|
|
|
// do not interrupt assassination scenario, if vip is a bot
|
|
if (game.is (MapFlags::Assassination) && util.isPlayerVIP (bot->ent ())) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
int aliveHumans = getAliveHumansCount ();
|
|
|
|
// check if we're have no alive players and some alive bots, and start autokill timer
|
|
if (!aliveHumans && aliveBots > 0 && cr::fzero (m_autoKillCheckTime)) {
|
|
m_autoKillCheckTime = game.time () + killDelay;
|
|
}
|
|
}
|
|
|
|
void BotManager::reset () {
|
|
m_grenadeUpdateTime = 0.0f;
|
|
m_entityUpdateTime = 0.0f;
|
|
m_plantSearchUpdateTime = 0.0f;
|
|
m_lastChatTime = 0.0f;
|
|
m_timeBombPlanted = 0.0f;
|
|
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
|
|
|
|
m_intrestingEntities.clear ();
|
|
m_activeGrenades.clear ();
|
|
}
|
|
|
|
void BotManager::initFilters () {
|
|
// table with all available actions for the bots (filtered in & out in bot::setconditions) some of them have subactions included
|
|
|
|
m_filters.emplace (&Bot::normal_, Task::Normal, 0.0f, kInvalidNodeIndex, 0.0f, true);
|
|
m_filters.emplace (&Bot::pause_, Task::Pause, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::moveToPos_, Task::MoveToPosition, 0.0f, kInvalidNodeIndex, 0.0f, true);
|
|
m_filters.emplace (&Bot::followUser_, Task::FollowUser, 0.0f, kInvalidNodeIndex, 0.0f, true);
|
|
m_filters.emplace (&Bot::pickupItem_, Task::PickupItem, 0.0f, kInvalidNodeIndex, 0.0f, true);
|
|
m_filters.emplace (&Bot::camp_, Task::Camp, 0.0f, kInvalidNodeIndex, 0.0f, true);
|
|
m_filters.emplace (&Bot::plantBomb_, Task::PlantBomb, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::defuseBomb_, Task::DefuseBomb, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::attackEnemy_, Task::Attack, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::huntEnemy_, Task::Hunt, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::seekCover_, Task::SeekCover, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::throwExplosive_, Task::ThrowExplosive, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::throwFlashbang_, Task::ThrowFlashbang, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::throwSmoke_, Task::ThrowSmoke, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::doublejump_, Task::DoubleJump, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::escapeFromBomb_, Task::EscapeFromBomb, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::shootBreakable_, Task::ShootBreakable, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::hide_, Task::Hide, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::blind_, Task::Blind, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
m_filters.emplace (&Bot::spraypaint_, Task::Spraypaint, 0.0f, kInvalidNodeIndex, 0.0f, false);
|
|
}
|
|
|
|
void BotManager::resetFilters () {
|
|
for (auto &task : m_filters) {
|
|
task.time = 0.0f;
|
|
}
|
|
}
|
|
|
|
void BotManager::decrementQuota (int by) {
|
|
if (by != 0) {
|
|
cv_quota.set (cr::clamp <int> (cv_quota.int_ () - by, 0, cv_quota.int_ ()));
|
|
return;
|
|
}
|
|
cv_quota.set (0);
|
|
}
|
|
|
|
void BotManager::initQuota () {
|
|
m_maintainTime = game.time () + cv_join_delay.float_ ();
|
|
m_quotaMaintainTime = game.time () + cv_join_delay.float_ ();
|
|
|
|
m_addRequests.clear ();
|
|
}
|
|
|
|
void BotManager::serverFill (int selection, int personality, int difficulty, int numToAdd) {
|
|
// this function fill server with bots, with specified team & personality
|
|
|
|
// always keep one slot
|
|
int maxClients = cv_autovacate.bool_ () ? game.maxClients () - 1 - (game.isDedicated () ? 0 : getHumansCount ()) : game.maxClients ();
|
|
|
|
if (getBotCount () >= maxClients - getHumansCount ()) {
|
|
return;
|
|
}
|
|
if (selection == 1 || selection == 2) {
|
|
mp_limitteams.set (0);
|
|
mp_autoteambalance.set (0);
|
|
}
|
|
else {
|
|
selection = 5;
|
|
}
|
|
char teams[6][12] = {"", {"Terrorists"}, {"CTs"}, "", "", {"Random"}, };
|
|
|
|
int toAdd = numToAdd == -1 ? maxClients - (getHumansCount () + getBotCount ()) : numToAdd;
|
|
|
|
for (int i = 0; i <= toAdd; ++i) {
|
|
addbot ("", difficulty, personality, selection, -1, true);
|
|
}
|
|
ctrl.msg ("Fill server with %s bots...", &teams[selection][0]);
|
|
}
|
|
|
|
void BotManager::kickEveryone (bool instant, bool zeroQuota) {
|
|
// this function drops all bot clients from server (this function removes only yapb's)
|
|
|
|
if (cv_quota.bool_ ()) {
|
|
ctrl.msg ("Bots are removed from server.");
|
|
}
|
|
|
|
if (zeroQuota) {
|
|
decrementQuota (0);
|
|
}
|
|
|
|
if (instant) {
|
|
for (const auto &bot : m_bots) {
|
|
bot->kick ();
|
|
}
|
|
}
|
|
m_addRequests.clear ();
|
|
}
|
|
|
|
void BotManager::kickFromTeam (Team team, bool removeAll) {
|
|
// this function remove random bot from specified team (if removeAll value = 1 then removes all players from team)
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (team == bot->m_team) {
|
|
decrementQuota ();
|
|
bot->kick ();
|
|
|
|
if (!removeAll) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::killAllBots (int team) {
|
|
// this function kills all bots on server (only this dll controlled bots)
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (team != -1 && team != bot->m_team) {
|
|
continue;
|
|
}
|
|
bot->kill ();
|
|
}
|
|
ctrl.msg ("All bots died...");
|
|
}
|
|
|
|
void BotManager::kickBot (int index) {
|
|
auto bot = findBotByIndex (index);
|
|
|
|
if (bot) {
|
|
decrementQuota ();
|
|
bot->kick ();
|
|
}
|
|
}
|
|
|
|
bool BotManager::kickRandom (bool decQuota, Team fromTeam) {
|
|
// this function removes random bot from server (only yapb's)
|
|
|
|
// if forTeam is unassigned, that means random team
|
|
bool deadBotFound = false;
|
|
|
|
auto updateQuota = [&] () {
|
|
if (decQuota) {
|
|
decrementQuota ();
|
|
}
|
|
};
|
|
|
|
auto belongsTeam = [&] (Bot *bot) {
|
|
if (fromTeam == Team::Unassigned) {
|
|
return true;
|
|
}
|
|
return bot->m_team == fromTeam;
|
|
};
|
|
|
|
// first try to kick the bot that is currently dead
|
|
for (const auto &bot : m_bots) {
|
|
if (!bot->m_notKilled && belongsTeam (bot.get ())) // is this slot used?
|
|
{
|
|
updateQuota ();
|
|
bot->kick ();
|
|
|
|
deadBotFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (deadBotFound) {
|
|
return true;
|
|
}
|
|
|
|
// if no dead bots found try to find one with lowest amount of frags
|
|
Bot *selected = nullptr;
|
|
float score = kInfiniteDistance;
|
|
|
|
// search bots in this team
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->pev->frags < score && belongsTeam (bot.get ())) {
|
|
selected = bot.get ();
|
|
score = bot->pev->frags;
|
|
}
|
|
}
|
|
|
|
// if found some bots
|
|
if (selected != nullptr) {
|
|
updateQuota ();
|
|
selected->kick ();
|
|
|
|
return true;
|
|
}
|
|
|
|
// worst case, just kick some random bot
|
|
for (const auto &bot : m_bots) {
|
|
if (belongsTeam (bot.get ())) // is this slot used?
|
|
{
|
|
updateQuota ();
|
|
bot->kick ();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void BotManager::setLastWinner (int winner) {
|
|
m_lastWinner = winner;
|
|
m_roundOver = true;
|
|
|
|
if (cv_radio_mode.int_ () != 2) {
|
|
return;
|
|
}
|
|
auto notify = findAliveBot ();
|
|
|
|
if (notify) {
|
|
if (notify->m_team == winner) {
|
|
if (getRoundMidTime () > game.time ()) {
|
|
notify->pushChatterMessage (Chatter::QuickWonRound);
|
|
}
|
|
else {
|
|
notify->pushChatterMessage (Chatter::WonTheRound);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::setWeaponMode (int selection) {
|
|
// this function sets bots weapon mode
|
|
|
|
selection--;
|
|
|
|
constexpr int std[7][kNumWeapons] = {
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only
|
|
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only
|
|
{-1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 2, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1}, // Machine Guns only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 2, 0, 1, -1, -1}, // Snipers only
|
|
{-1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1} // Standard
|
|
};
|
|
|
|
constexpr int as[7][kNumWeapons] = {
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only
|
|
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only
|
|
{-1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1}, // Machine Guns only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only
|
|
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 1, -1, -1}, // Snipers only
|
|
{-1, -1, -1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, -1, 1, 0, 1, 1, 0, 0, -1, 1, 1, 1} // Standard
|
|
};
|
|
constexpr char modes[7][12] = {{"Knife"}, {"Pistol"}, {"Shotgun"}, {"Machine Gun"}, {"Rifle"}, {"Sniper"}, {"Standard"}};
|
|
|
|
// get the raw weapons array
|
|
auto tab = conf.getRawWeapons ();
|
|
|
|
// set the correct weapon mode
|
|
for (int i = 0; i < kNumWeapons; ++i) {
|
|
tab[i].teamStandard = std[selection][i];
|
|
tab[i].teamAS = as[selection][i];
|
|
}
|
|
cv_jasonmode.set (selection == 0 ? 1 : 0);
|
|
|
|
ctrl.msg ("%s weapon mode selected.", &modes[selection][0]);
|
|
}
|
|
|
|
void BotManager::listBots () {
|
|
// this function list's bots currently playing on the server
|
|
|
|
ctrl.msg ("%-3.5s\t%-19.16s\t%-10.12s\t%-3.4s\t%-3.4s\t%-3.4s\t%-3.5s", "index", "name", "personality", "team", "difficulty", "frags", "alive");
|
|
|
|
for (const auto &bot : bots) {;
|
|
ctrl.msg ("[%-3.1d]\t%-19.16s\t%-10.12s\t%-3.4s\t%-3.1d\t%-3.1d\t%-3.4s", bot->index (), bot->pev->netname.chars (), bot->m_personality == Personality::Rusher ? "rusher" : bot->m_personality == Personality::Normal ? "normal" : "careful", bot->m_team == Team::CT ? "CT" : "T", bot->m_difficulty, static_cast <int> (bot->pev->frags), bot->m_notKilled ? "yes" : "no");
|
|
}
|
|
ctrl.msg ("%d bots", m_bots.length ());
|
|
}
|
|
|
|
float BotManager::getConnectTime (StringRef name, float original) {
|
|
// this function get's fake bot player time.
|
|
|
|
for (const auto &bot : m_bots) {
|
|
if (name == bot->pev->netname.chars ()) {
|
|
return bot->getConnectionTime ();
|
|
}
|
|
}
|
|
return original;
|
|
}
|
|
|
|
float BotManager::getAverageTeamKPD (bool calcForBots) {
|
|
Twin <float, int32> calc {};
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used)) {
|
|
continue;
|
|
}
|
|
auto bot = bots[client.ent];
|
|
|
|
if (calcForBots && bot) {
|
|
calc.first += client.ent->v.frags;
|
|
calc.second++;
|
|
}
|
|
else if (!calcForBots && !bot) {
|
|
calc.first += client.ent->v.frags;
|
|
calc.second++;
|
|
}
|
|
}
|
|
|
|
if (calc.second > 0) {
|
|
return calc.first / calc.second;
|
|
}
|
|
return 0.0f;
|
|
}
|
|
|
|
Twin <int, int> BotManager::countTeamPlayers () {
|
|
int ts = 0, cts = 0;
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if (client.flags & ClientFlags::Used) {
|
|
if (client.team2 == Team::Terrorist) {
|
|
++ts;
|
|
}
|
|
else if (client.team2 == Team::CT) {
|
|
++cts;
|
|
}
|
|
}
|
|
}
|
|
return { ts, cts };
|
|
}
|
|
|
|
Bot *BotManager::findHighestFragBot (int team) {
|
|
int bestIndex = 0;
|
|
float bestScore = -1;
|
|
|
|
// search bots in this team
|
|
for (const auto &bot : bots) {
|
|
if (bot->m_notKilled && bot->m_team == team) {
|
|
if (bot->pev->frags > bestScore) {
|
|
bestIndex = bot->index ();
|
|
bestScore = bot->pev->frags;
|
|
}
|
|
}
|
|
}
|
|
return findBotByIndex (bestIndex);
|
|
}
|
|
|
|
void BotManager::updateTeamEconomics (int team, bool setTrue) {
|
|
// this function decides is players on specified team is able to buy primary weapons by calculating players
|
|
// that have not enough money to buy primary (with economics), and if this result higher 80%, player is can't
|
|
// buy primary weapons.
|
|
|
|
extern ConVar cv_economics_rounds;
|
|
|
|
if (setTrue || !cv_economics_rounds.bool_ ()) {
|
|
m_economicsGood[team] = true;
|
|
return; // don't check economics while economics disable
|
|
}
|
|
const int *econLimit = conf.getEconLimit ();
|
|
|
|
int numPoorPlayers = 0;
|
|
int numTeamPlayers = 0;
|
|
|
|
// start calculating
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_team == team) {
|
|
if (bot->m_moneyAmount <= econLimit[EcoLimit::PrimaryGreater]) {
|
|
++numPoorPlayers;
|
|
}
|
|
++numTeamPlayers; // update count of team
|
|
}
|
|
}
|
|
m_economicsGood[team] = true;
|
|
|
|
if (numTeamPlayers <= 1) {
|
|
return;
|
|
}
|
|
// if 80 percent of team have no enough money to purchase primary weapon
|
|
if ((numTeamPlayers * 80) / 100 <= numPoorPlayers) {
|
|
m_economicsGood[team] = false;
|
|
}
|
|
|
|
// winner must buy something!
|
|
if (m_lastWinner == team) {
|
|
m_economicsGood[team] = true;
|
|
}
|
|
}
|
|
|
|
void BotManager::updateBotDifficulties () {
|
|
// if min/max difficulty is specified this should not have effect
|
|
if (cv_difficulty_min.int_ () != Difficulty::Invalid || cv_difficulty_max.int_ () != Difficulty::Invalid || cv_difficulty_auto.bool_ ()) {
|
|
return;
|
|
}
|
|
auto difficulty = cv_difficulty.int_ ();
|
|
|
|
if (difficulty != m_lastDifficulty) {
|
|
|
|
// sets new difficulty for all bots
|
|
for (const auto &bot : m_bots) {
|
|
bot->m_difficulty = difficulty;
|
|
}
|
|
m_lastDifficulty = difficulty;
|
|
}
|
|
}
|
|
|
|
void BotManager::balanceBotDifficulties () {
|
|
extern ConVar cv_whose_your_daddy;
|
|
|
|
// with nightmare difficulty, there is no balance
|
|
if (cv_whose_your_daddy.bool_ ()) {
|
|
return;
|
|
}
|
|
// difficulty chaning once per round (time)
|
|
auto updateDifficulty = [] (Bot *bot, int32 offset) {
|
|
bot->m_difficulty = cr::clamp (static_cast <Difficulty> (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert);
|
|
};
|
|
|
|
auto ratioPlayer = getAverageTeamKPD (false);
|
|
auto ratioBots = getAverageTeamKPD (true);
|
|
|
|
// calculate for each the bot
|
|
for (auto &bot : m_bots) {
|
|
float score = bot->m_kpdRatio;
|
|
|
|
// if kd ratio is going to go to low, we need to try to set higher difficulty
|
|
if (score < 0.8 || (score <= 1.2 && ratioBots < ratioPlayer)) {
|
|
updateDifficulty (bot.get (), +1);
|
|
}
|
|
else if (score > 4.0f || (score >= 2.5 && ratioBots > ratioPlayer)) {
|
|
updateDifficulty (bot.get (), -1);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::destroy () {
|
|
// this function free all bots slots (used on server shutdown)
|
|
|
|
m_bots.clear ();
|
|
}
|
|
|
|
Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
|
|
// this function does core operation of creating bot, it's called by addbot (),
|
|
// when bot setup completed, (this is a bot class constructor)
|
|
|
|
// we're not initializing all the variables in bot class, so do an ugly thing... memset this
|
|
plat.bzero (this, sizeof (*this));
|
|
|
|
|
|
int clientIndex = game.indexOfEntity (bot);
|
|
pev = &bot->v;
|
|
|
|
if (bot->pvPrivateData != nullptr) {
|
|
engfuncs.pfnFreeEntPrivateData (bot);
|
|
}
|
|
|
|
bot->pvPrivateData = nullptr;
|
|
bot->v.frags = 0;
|
|
|
|
// create the player entity by calling MOD's player function
|
|
bots.execGameEntity (bot);
|
|
|
|
// set all info buffer keys for this bot
|
|
auto buffer = engfuncs.pfnGetInfoKeyBuffer (bot);
|
|
engfuncs.pfnSetClientKeyValue (clientIndex, buffer, "_vgui_menus", "0");
|
|
|
|
if (!game.is (GameFlags::Legacy)) {
|
|
if (cv_show_latency.int_ () == 1) {
|
|
engfuncs.pfnSetClientKeyValue (clientIndex, buffer, "*bot", "1");
|
|
}
|
|
auto avatar = conf.getRandomAvatar ();
|
|
|
|
if (cv_show_avatars.bool_ () && !avatar.empty ()) {
|
|
engfuncs.pfnSetClientKeyValue (clientIndex, buffer, "*sid", avatar.chars ());
|
|
}
|
|
}
|
|
|
|
char reject[256] = {0, };
|
|
MDLL_ClientConnect (bot, bot->v.netname.chars (), strings.format ("127.0.0.%d", clientIndex + 100), reject);
|
|
|
|
if (!strings.isEmpty (reject)) {
|
|
logger.error ("Server refused '%s' connection (%s)", bot->v.netname.chars (), reject);
|
|
game.serverCommand ("kick \"%s\"", bot->v.netname.chars ()); // kick the bot player if the server refused it
|
|
|
|
bot->v.flags |= FL_KILLME;
|
|
return;
|
|
}
|
|
|
|
MDLL_ClientPutInServer (bot);
|
|
bot->v.flags |= FL_FAKECLIENT; // set this player as fakeclient
|
|
|
|
// initialize all the variables for this bot...
|
|
m_notStarted = true; // hasn't joined game yet
|
|
m_forceRadio = false;
|
|
|
|
m_index = clientIndex - 1;
|
|
m_startAction = BotMsg::None;
|
|
m_retryJoin = 0;
|
|
m_moneyAmount = 0;
|
|
m_logotypeIndex = conf.getRandomLogoIndex ();
|
|
|
|
// assign how talkative this bot will be
|
|
m_sayTextBuffer.chatDelay = rg.get (3.8f, 10.0f);
|
|
m_sayTextBuffer.chatProbability = rg.get (10, 100);
|
|
|
|
m_notKilled = false;
|
|
m_weaponBurstMode = BurstMode::Off;
|
|
m_difficulty = cr::clamp (static_cast <Difficulty> (difficulty), Difficulty::Noob, Difficulty::Expert);
|
|
|
|
auto minDifficulty = cv_difficulty_min.int_ ();
|
|
auto maxDifficulty = cv_difficulty_max.int_ ();
|
|
|
|
// if we're have min/max difficulty specified, choose value from they
|
|
if (minDifficulty != Difficulty::Invalid && maxDifficulty != Difficulty::Invalid) {
|
|
if (maxDifficulty > minDifficulty) {
|
|
cr::swap (maxDifficulty, minDifficulty);
|
|
}
|
|
m_difficulty = rg.get (minDifficulty, maxDifficulty);
|
|
}
|
|
m_basePing = rg.get (cv_ping_base_min.int_ (), cv_ping_base_max.int_ ());
|
|
|
|
m_lastCommandTime = game.time () - 0.1f;
|
|
m_frameInterval = game.time ();
|
|
m_heavyTimestamp = game.time ();
|
|
m_slowFrameTimestamp = 0.0f;
|
|
m_kpdRatio = 0.0f;
|
|
|
|
// stuff from jk_botti
|
|
m_playServerTime = 60.0f * rg.get (30.0f, 240.0f);
|
|
m_joinServerTime = plat.seconds () - m_playServerTime * rg.get (0.2f, 0.8f);
|
|
|
|
switch (personality) {
|
|
case 1:
|
|
m_personality = Personality::Rusher;
|
|
m_baseAgressionLevel = rg.get (0.7f, 1.0f);
|
|
m_baseFearLevel = rg.get (0.0f, 0.4f);
|
|
break;
|
|
|
|
case 2:
|
|
m_personality = Personality::Careful;
|
|
m_baseAgressionLevel = rg.get (0.2f, 0.5f);
|
|
m_baseFearLevel = rg.get (0.7f, 1.0f);
|
|
break;
|
|
|
|
default:
|
|
m_personality = Personality::Normal;
|
|
m_baseAgressionLevel = rg.get (0.4f, 0.7f);
|
|
m_baseFearLevel = rg.get (0.4f, 0.7f);
|
|
break;
|
|
}
|
|
|
|
plat.bzero (&m_ammoInClip, sizeof (m_ammoInClip));
|
|
plat.bzero (&m_ammo, sizeof (m_ammo));
|
|
|
|
m_currentWeapon = 0; // current weapon is not assigned at start
|
|
m_weaponType = WeaponType::None; // current weapon type is not assigned at start
|
|
|
|
m_voicePitch = rg.get (80, 115); // assign voice pitch
|
|
|
|
// copy them over to the temp level variables
|
|
m_agressionLevel = m_baseAgressionLevel;
|
|
m_fearLevel = m_baseFearLevel;
|
|
m_nextEmotionUpdate = game.time () + 0.5f;
|
|
m_healthValue = bot->v.health;
|
|
|
|
// just to be sure
|
|
m_msgQueue.clear ();
|
|
|
|
// init path walker
|
|
m_pathWalk.init (graph.getMaxRouteLength ());
|
|
|
|
// assign team and class
|
|
m_wantedTeam = team;
|
|
m_wantedSkin = skin;
|
|
|
|
newRound ();
|
|
}
|
|
|
|
float Bot::getFrameInterval () {
|
|
return m_frameInterval;
|
|
}
|
|
|
|
float Bot::getConnectionTime () {
|
|
auto current = plat.seconds ();
|
|
|
|
if (current - m_joinServerTime > m_playServerTime || current - m_joinServerTime <= 0.0f) {
|
|
m_playServerTime = 60.0f * rg.get (30.0f, 240.0f);
|
|
m_joinServerTime = current - m_playServerTime * rg.get (0.2f, 0.8f);
|
|
}
|
|
return current - m_joinServerTime;
|
|
}
|
|
|
|
int BotManager::getHumansCount (bool ignoreSpectators) {
|
|
// this function returns number of humans playing on the server
|
|
|
|
int count = 0;
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if ((client.flags & ClientFlags::Used) && !bots[client.ent] && !(client.ent->v.flags & FL_FAKECLIENT)) {
|
|
if (ignoreSpectators && client.team2 != Team::Terrorist && client.team2 != Team::CT) {
|
|
continue;
|
|
}
|
|
++count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int BotManager::getAliveHumansCount () {
|
|
// this function returns number of humans playing on the server
|
|
|
|
int count = 0;
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if ((client.flags & ClientFlags::Alive) && bots[client.ent] == nullptr && !(client.ent->v.flags & FL_FAKECLIENT)) {
|
|
++count;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
bool BotManager::isTeamStacked (int team) {
|
|
if (team != Team::CT && team != Team::Terrorist) {
|
|
return false;
|
|
}
|
|
int limitTeams = mp_limitteams.int_ ();
|
|
|
|
if (!limitTeams) {
|
|
return false;
|
|
}
|
|
int teamCount[kGameTeamNum] = { 0, };
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if ((client.flags & ClientFlags::Used) && client.team2 != Team::Unassigned && client.team2 != Team::Spectator) {
|
|
++teamCount[client.team2];
|
|
}
|
|
}
|
|
return teamCount[team] + 1 > teamCount[team == Team::CT ? Team::Terrorist : Team::CT] + limitTeams;
|
|
}
|
|
|
|
void BotManager::erase (Bot *bot) {
|
|
for (auto &e : m_bots) {
|
|
if (e.get () != bot) {
|
|
continue;
|
|
}
|
|
bot->markStale ();
|
|
|
|
auto index = m_bots.index (e);
|
|
e.reset ();
|
|
|
|
m_bots.erase (index, 1); // remove from bots array
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
void BotManager::handleDeath (edict_t *killer, edict_t *victim) {
|
|
auto killerTeam = game.getTeam (killer);
|
|
auto victimTeam = game.getTeam (victim);
|
|
|
|
if (cv_radio_mode.int_ () == 2) {
|
|
// need to send congrats on well placed shot
|
|
for (const auto ¬ify : bots) {
|
|
if (notify->m_notKilled && killerTeam == notify->m_team && killerTeam != victimTeam && killer != notify->ent () && notify->seesEntity (victim->v.origin)) {
|
|
if (!(killer->v.flags & FL_FAKECLIENT)) {
|
|
notify->pushChatterMessage (Chatter::NiceShotCommander);
|
|
}
|
|
else {
|
|
notify->pushChatterMessage (Chatter::NiceShotPall);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Bot *killerBot = nullptr;
|
|
Bot *victimBot = nullptr;
|
|
|
|
// notice nearby to victim teammates, that attacker is near
|
|
for (const auto ¬ify : bots) {
|
|
if (notify->m_seeEnemyTime + 2.0f < game.time () && notify->m_notKilled && notify->m_team == victimTeam && game.isNullEntity (notify->m_enemy) && killerTeam != victimTeam && util.isVisible (killer->v.origin, notify->ent ())) {
|
|
notify->m_actualReactionTime = 0.0f;
|
|
notify->m_seeEnemyTime = game.time ();
|
|
notify->m_enemy = killer;
|
|
notify->m_lastEnemy = killer;
|
|
notify->m_lastEnemyOrigin = killer->v.origin;
|
|
}
|
|
|
|
if (notify->ent () == killer) {
|
|
killerBot = notify.get ();
|
|
}
|
|
else if (notify->ent () == victim) {
|
|
victimBot = notify.get ();
|
|
}
|
|
}
|
|
|
|
// is this message about a bot who killed somebody?
|
|
if (killerBot != nullptr) {
|
|
killerBot->m_lastVictim = victim;
|
|
}
|
|
|
|
// did a human kill a bot on his team?
|
|
else {
|
|
if (victimBot != nullptr) {
|
|
if (killerTeam == victimBot->m_team) {
|
|
victimBot->m_voteKickIndex = game.indexOfEntity (killer);
|
|
}
|
|
victimBot->m_notKilled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::newRound () {
|
|
// this function initializes a bot after creation & at the start of each round
|
|
|
|
// delete all allocated path nodes
|
|
clearSearchNodes ();
|
|
clearRoute ();
|
|
|
|
m_pathOrigin = nullptr;
|
|
m_destOrigin = nullptr;
|
|
|
|
m_path = nullptr;
|
|
m_currentTravelFlags = 0;
|
|
m_desiredVelocity= nullptr;
|
|
m_currentNodeIndex = kInvalidNodeIndex;
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
m_chosenGoalIndex = kInvalidNodeIndex;
|
|
m_loosedBombNodeIndex = kInvalidNodeIndex;
|
|
m_plantedBombNodeIndex = kInvalidNodeIndex;
|
|
|
|
m_grenadeRequested = false;
|
|
m_moveToC4 = false;
|
|
m_duckDefuse = false;
|
|
m_duckDefuseCheckTime = 0.0f;
|
|
|
|
m_numFriendsLeft = 0;
|
|
m_numEnemiesLeft = 0;
|
|
m_oldButtons = pev->button;
|
|
|
|
for (auto &node : m_previousNodes) {
|
|
node = kInvalidNodeIndex;
|
|
}
|
|
|
|
m_navTimeset = game.time ();
|
|
m_team = game.getTeam (ent ());
|
|
m_isVIP = false;
|
|
|
|
switch (m_personality) {
|
|
default:
|
|
case Personality::Normal:
|
|
m_pathType = rg.chance (50) ? FindPath::Optimal : FindPath::Safe;
|
|
break;
|
|
|
|
case Personality::Rusher:
|
|
m_pathType = FindPath::Fast;
|
|
break;
|
|
|
|
case Personality::Careful:
|
|
m_pathType = FindPath::Safe;
|
|
break;
|
|
}
|
|
|
|
// clear all states & tasks
|
|
m_states = 0;
|
|
clearTasks ();
|
|
|
|
m_isVIP = false;
|
|
m_isLeader = false;
|
|
m_hasProgressBar = false;
|
|
m_canChooseAimDirection = true;
|
|
m_preventFlashing = 0.0f;
|
|
|
|
m_timeTeamOrder = 0.0f;
|
|
m_askCheckTime = rg.get (30.0f, 90.0f);
|
|
m_minSpeed = 260.0f;
|
|
m_prevSpeed = 0.0f;
|
|
m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance);
|
|
m_prevTime = game.time ();
|
|
m_lookUpdateTime = game.time ();
|
|
m_changeViewTime = game.time () + (rg.chance (25) ? mp_freezetime.float_ () : 0.0f);
|
|
m_aimErrorTime = game.time ();
|
|
|
|
m_viewDistance = 4096.0f;
|
|
m_maxViewDistance = 4096.0f;
|
|
|
|
m_liftEntity = nullptr;
|
|
m_pickupItem = nullptr;
|
|
m_itemIgnore = nullptr;
|
|
m_itemCheckTime = 0.0f;
|
|
|
|
m_breakableEntity = nullptr;
|
|
m_breakableOrigin = nullptr;
|
|
m_timeDoorOpen = 0.0f;
|
|
|
|
resetCollision ();
|
|
resetDoubleJump ();
|
|
|
|
m_enemy = nullptr;
|
|
m_lastVictim = nullptr;
|
|
m_lastEnemy = nullptr;
|
|
m_lastEnemyOrigin = nullptr;
|
|
m_trackingEdict = nullptr;
|
|
m_timeNextTracking = 0.0f;
|
|
|
|
m_buttonPushTime = 0.0f;
|
|
m_enemyUpdateTime = 0.0f;
|
|
m_enemyIgnoreTimer = 0.0f;
|
|
m_retreatTime = 0.0f;
|
|
m_seeEnemyTime = 0.0f;
|
|
m_shootAtDeadTime = 0.0f;
|
|
m_oldCombatDesire = 0.0f;
|
|
m_liftUsageTime = 0.0f;
|
|
|
|
m_avoidGrenade = nullptr;
|
|
m_needAvoidGrenade = 0;
|
|
|
|
m_lastDamageType = -1;
|
|
m_voteKickIndex = 0;
|
|
m_lastVoteKick = 0;
|
|
m_voteMap = 0;
|
|
m_tryOpenDoor = 0;
|
|
m_aimFlags = 0;
|
|
m_liftState = 0;
|
|
|
|
m_aimLastError= nullptr;
|
|
m_position = nullptr;
|
|
m_liftTravelPos = nullptr;
|
|
|
|
setIdealReactionTimers (true);
|
|
|
|
m_targetEntity = nullptr;
|
|
m_followWaitTime = 0.0f;
|
|
|
|
m_hostages.clear ();
|
|
|
|
for (auto &timer : m_chatterTimes) {
|
|
timer = kMaxChatterRepeatInteval;
|
|
}
|
|
|
|
m_isReloading = false;
|
|
m_reloadState = Reload::None;
|
|
|
|
m_reloadCheckTime = 0.0f;
|
|
m_shootTime = game.time ();
|
|
m_playerTargetTime = game.time ();
|
|
m_firePause = 0.0f;
|
|
m_timeLastFired = 0.0f;
|
|
|
|
m_sniperStopTime = 0.0f;
|
|
m_grenadeCheckTime = 0.0f;
|
|
m_isUsingGrenade = false;
|
|
m_bombSearchOverridden = false;
|
|
|
|
m_blindButton = 0;
|
|
m_blindTime = 0.0f;
|
|
m_jumpTime = 0.0f;
|
|
m_jumpFinished = false;
|
|
m_isStuck = false;
|
|
|
|
m_sayTextBuffer.timeNextChat = game.time ();
|
|
m_sayTextBuffer.entityIndex = -1;
|
|
m_sayTextBuffer.sayText.clear ();
|
|
|
|
m_buyState = BuyState::PrimaryWeapon;
|
|
m_lastEquipTime = 0.0f;
|
|
|
|
// if bot died, clear all weapon stuff and force buying again
|
|
if (!m_notKilled) {
|
|
plat.bzero (&m_ammoInClip, sizeof (m_ammoInClip));
|
|
plat.bzero (&m_ammo, sizeof (m_ammo));
|
|
|
|
m_currentWeapon = 0;
|
|
m_weaponType = 0;
|
|
}
|
|
m_flashLevel = 100.0f;
|
|
m_checkDarkTime = game.time ();
|
|
|
|
m_knifeAttackTime = game.time () + rg.get (1.3f, 2.6f);
|
|
m_nextBuyTime = game.time () + rg.get (0.6f, 2.0f);
|
|
|
|
m_buyPending = false;
|
|
m_inBombZone = false;
|
|
m_ignoreBuyDelay = false;
|
|
m_hasC4 = false;
|
|
|
|
m_fallDownTime = 0.0f;
|
|
m_shieldCheckTime = 0.0f;
|
|
m_zoomCheckTime = 0.0f;
|
|
m_strafeSetTime = 0.0f;
|
|
m_combatStrafeDir = Dodge::None;
|
|
m_fightStyle = Fight::None;
|
|
m_lastFightStyleCheck = 0.0f;
|
|
|
|
m_checkWeaponSwitch = true;
|
|
m_checkKnifeSwitch = true;
|
|
m_buyingFinished = false;
|
|
|
|
m_radioEntity = nullptr;
|
|
m_radioOrder = 0;
|
|
m_defendedBomb = false;
|
|
m_defendHostage = false;
|
|
m_headedTime = 0.0f;
|
|
|
|
m_timeLogoSpray = game.time () + rg.get (5.0f, 30.0f);
|
|
m_spawnTime = game.time ();
|
|
m_lastChatTime = game.time ();
|
|
|
|
m_timeCamping = 0.0f;
|
|
m_campDirection = 0;
|
|
m_nextCampDirTime = 0;
|
|
m_campButtons = 0;
|
|
|
|
m_soundUpdateTime = 0.0f;
|
|
m_heardSoundTime = game.time ();
|
|
|
|
// clear its message queue
|
|
for (auto &msg : m_messageQueue) {
|
|
msg = BotMsg::None;
|
|
}
|
|
m_msgQueue.clear ();
|
|
|
|
// clear last trace
|
|
for (auto i = 0; i < TraceChannel::Num; ++i) {
|
|
m_lastTrace[i] = {};
|
|
m_traceSkip[i] = 0;
|
|
}
|
|
|
|
// and put buying into its message queue
|
|
pushMsgQueue (BotMsg::Buy);
|
|
startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true);
|
|
|
|
// restore fake client bit, just in case
|
|
pev->flags |= FL_FAKECLIENT;
|
|
|
|
if (rg.chance (50)) {
|
|
pushChatterMessage (Chatter::NewRound);
|
|
}
|
|
m_updateInterval = game.is (GameFlags::Legacy | GameFlags::Xash3D) ? 0.0f : (1.0f / cr::clamp (cv_think_fps.float_ (), 30.0f, 60.0f));
|
|
}
|
|
|
|
void Bot::kill () {
|
|
// this function kills a bot (not just using ClientKill, but like the CSBot does)
|
|
// base code courtesy of Lazy (from bots-united forums!)
|
|
|
|
bots.touchKillerEntity (this);
|
|
}
|
|
|
|
void Bot::kick () {
|
|
// this function kick off one bot from the server.
|
|
auto username = pev->netname.chars ();
|
|
|
|
if (!(pev->flags & FL_CLIENT) || strings.isEmpty (username)) {
|
|
return;
|
|
}
|
|
markStale ();
|
|
|
|
game.serverCommand ("kick \"%s\"", username);
|
|
ctrl.msg ("Bot '%s' kicked.", username);
|
|
}
|
|
|
|
void Bot::markStale () {
|
|
showChaterIcon (false);
|
|
|
|
// clear the bot name
|
|
conf.clearUsedName (this);
|
|
|
|
// clear fakeclient bit
|
|
pev->flags &= ~FL_FAKECLIENT;
|
|
|
|
// make as not receiveing any messages
|
|
pev->flags |= FL_DORMANT;
|
|
}
|
|
|
|
void Bot::updateTeamJoin () {
|
|
// this function handles the selection of teams & class
|
|
|
|
if (!m_notStarted) {
|
|
return;
|
|
}
|
|
|
|
// cs prior beta 7.0 uses hud-based motd, so press fire once
|
|
if (game.is (GameFlags::Legacy)) {
|
|
pev->button |= IN_ATTACK;
|
|
}
|
|
|
|
// check if something has assigned team to us
|
|
else if (m_team == Team::Terrorist || m_team == Team::CT) {
|
|
m_notStarted = false;
|
|
}
|
|
else if (m_team == Team::Unassigned && m_retryJoin > 2) {
|
|
m_startAction = BotMsg::TeamSelect;
|
|
}
|
|
|
|
// if bot was unable to join team, and no menus popups, check for stacked team
|
|
if (m_startAction == BotMsg::None) {
|
|
if (++m_retryJoin > 3 && bots.isTeamStacked (m_wantedTeam - 1)) {
|
|
m_retryJoin = 0;
|
|
|
|
ctrl.msg ("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).");
|
|
kick ();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// handle counter-strike stuff here...
|
|
if (m_startAction == BotMsg::TeamSelect) {
|
|
m_startAction = BotMsg::None; // switch back to idle
|
|
|
|
char teamJoin = cv_join_team.str ()[0];
|
|
|
|
if (teamJoin == 'C' || teamJoin == 'c') {
|
|
m_wantedTeam = 2;
|
|
}
|
|
else if (teamJoin == 'T' || teamJoin == 't') {
|
|
m_wantedTeam = 1;
|
|
}
|
|
|
|
if (m_wantedTeam != 1 && m_wantedTeam != 2) {
|
|
auto players = bots.countTeamPlayers ();
|
|
|
|
// balance the team upon creation, we can't use game auto select (5) from now, as we use enforced skins belows
|
|
// due to we don't know the team bot selected, and TeamInfo messages still shows us we're spectators..
|
|
|
|
if (players.first > players.second) {
|
|
m_wantedTeam = 2;
|
|
}
|
|
else if (players.first < players.second) {
|
|
m_wantedTeam = 1;
|
|
}
|
|
else {
|
|
m_wantedTeam = rg.get (1, 2);
|
|
}
|
|
}
|
|
|
|
// select the team the bot wishes to join...
|
|
issueCommand ("menuselect %d", m_wantedTeam);
|
|
}
|
|
else if (m_startAction == BotMsg::ClassSelect) {
|
|
m_startAction = BotMsg::None; // switch back to idle
|
|
|
|
// czero has additional models
|
|
auto maxChoice = game.is (GameFlags::ConditionZero) ? 5 : 4;
|
|
auto enforcedSkin = 0;
|
|
|
|
// setup enforced skin based on selected team
|
|
if (m_wantedTeam == 1 || m_team == Team::Terrorist) {
|
|
enforcedSkin = cv_botskin_t.int_ ();
|
|
}
|
|
else if (m_wantedTeam == 2 || m_team == Team::CT) {
|
|
enforcedSkin = cv_botskin_ct.int_ ();
|
|
}
|
|
enforcedSkin = cr::clamp (enforcedSkin, 0, maxChoice);
|
|
|
|
// try to choice manually
|
|
if (m_wantedSkin < 1 || m_wantedSkin > maxChoice) {
|
|
m_wantedSkin = rg.get (1, maxChoice); // use random if invalid
|
|
}
|
|
|
|
// and set enforced if any
|
|
if (enforcedSkin > 0) {
|
|
m_wantedSkin = enforcedSkin;
|
|
}
|
|
|
|
// select the class the bot wishes to use...
|
|
issueCommand ("menuselect %d", m_wantedSkin);
|
|
|
|
// bot has now joined the game (doesn't need to be started)
|
|
m_notStarted = false;
|
|
|
|
// check for greeting other players, since we connected
|
|
if (rg.chance (20)) {
|
|
pushChatMessage (Chat::Hello);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::captureChatRadio (const char *cmd, const char *arg, edict_t *ent) {
|
|
if (game.isBotCmd ()) {
|
|
return;
|
|
}
|
|
|
|
if (strings.matches (cmd, "say") || strings.matches (cmd, "say_team")) {
|
|
bool alive = util.isAlive (ent);
|
|
int team = -1;
|
|
|
|
if (strcmp (cmd, "say_team") == 0) {
|
|
team = game.getTeam (ent);
|
|
}
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used) || (team != -1 && team != client.team2) || alive != util.isAlive (client.ent)) {
|
|
continue;
|
|
}
|
|
auto target = bots[client.ent];
|
|
|
|
if (target != nullptr) {
|
|
target->m_sayTextBuffer.entityIndex = game.indexOfPlayer (ent);
|
|
|
|
if (strings.isEmpty (engfuncs.pfnCmd_Args ())) {
|
|
continue;
|
|
}
|
|
target->m_sayTextBuffer.sayText = engfuncs.pfnCmd_Args ();
|
|
target->m_sayTextBuffer.timeNextChat = game.time () + target->m_sayTextBuffer.chatDelay;
|
|
}
|
|
}
|
|
}
|
|
Client &target = util.getClient (game.indexOfPlayer (ent));
|
|
|
|
// check if this player alive, and issue something
|
|
if ((target.flags & ClientFlags::Alive) && target.radio != 0 && strncmp (cmd, "menuselect", 10) == 0) {
|
|
int radioCommand = atoi (arg);
|
|
|
|
if (radioCommand != 0) {
|
|
radioCommand += 10 * (target.radio - 1);
|
|
|
|
if (radioCommand != Radio::RogerThat && radioCommand != Radio::Negative && radioCommand != Radio::ReportingIn) {
|
|
for (const auto &bot : bots) {
|
|
|
|
// validate bot
|
|
if (bot->m_team == target.team && ent != bot->ent () && bot->m_radioOrder == 0) {
|
|
bot->m_radioOrder = radioCommand;
|
|
bot->m_radioEntity = ent;
|
|
}
|
|
}
|
|
}
|
|
bots.setLastRadioTimestamp (target.team, game.time ());
|
|
}
|
|
target.radio = 0;
|
|
}
|
|
else if (strncmp (cmd, "radio", 5) == 0) {
|
|
target.radio = atoi (&cmd[5]);
|
|
}
|
|
}
|
|
|
|
void BotManager::notifyBombDefuse () {
|
|
// notify all terrorists that CT is starting bomb defusing
|
|
|
|
if (!isBombPlanted ()) {
|
|
return;
|
|
}
|
|
|
|
for (const auto &bot : bots) {
|
|
if (bot->m_team == Team::Terrorist && bot->m_notKilled && bot->getCurrentTaskId () != Task::MoveToPosition) {
|
|
bot->clearSearchNodes ();
|
|
|
|
bot->m_position = graph.getBombOrigin ();
|
|
bot->startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::updateActiveGrenade () {
|
|
if (m_grenadeUpdateTime > game.time ()) {
|
|
return;
|
|
}
|
|
m_activeGrenades.clear (); // clear previously stored grenades
|
|
|
|
// need to ignore bomb model in active grenades...
|
|
auto bombModel = conf.getBombModelName ();
|
|
|
|
// search the map for any type of grenade
|
|
game.searchEntities ("classname", "grenade", [&] (edict_t *e) {
|
|
// do not count c4 as a grenade
|
|
if (!util.isModel (e, bombModel)) {
|
|
m_activeGrenades.push (e);
|
|
}
|
|
return EntitySearchResult::Continue; // continue iteration
|
|
});
|
|
m_grenadeUpdateTime = game.time () + 0.25f;
|
|
}
|
|
|
|
void BotManager::updateIntrestingEntities () {
|
|
if (m_entityUpdateTime > game.time ()) {
|
|
return;
|
|
}
|
|
|
|
// clear previously stored entities
|
|
m_intrestingEntities.clear ();
|
|
|
|
// search the map for any type of grenade
|
|
game.searchEntities (nullptr, kInfiniteDistance, [&] (edict_t *e) {
|
|
auto classname = e->v.classname.chars ();
|
|
|
|
// search for grenades, weaponboxes, weapons, items and armoury entities
|
|
if (strncmp ("weaponbox", classname, 9) == 0 || strncmp ("grenade", classname, 7) == 0 || util.isItem (e) || strncmp ("armoury", classname, 7) == 0) {
|
|
m_intrestingEntities.push (e);
|
|
}
|
|
|
|
// pickup some csdm stuff if we're running csdm
|
|
if (game.mapIs (MapFlags::HostageRescue) && strncmp ("hostage", classname, 7) == 0) {
|
|
m_intrestingEntities.push (e);
|
|
}
|
|
|
|
// add buttons
|
|
if (game.mapIs (MapFlags::HasButtons) && strncmp ("func_button", classname, 11) == 0) {
|
|
m_intrestingEntities.push (e);
|
|
}
|
|
|
|
// pickup some csdm stuff if we're running csdm
|
|
if (game.is (GameFlags::CSDM) && strncmp ("csdm", classname, 4) == 0) {
|
|
m_intrestingEntities.push (e);
|
|
}
|
|
|
|
if (cv_attack_monsters.bool_ () && util.isMonster (e)) {
|
|
m_intrestingEntities.push (e);
|
|
}
|
|
|
|
// continue iteration
|
|
return EntitySearchResult::Continue;
|
|
});
|
|
m_entityUpdateTime = game.time () + 0.5f;
|
|
}
|
|
|
|
void BotManager::selectLeaders (int team, bool reset) {
|
|
if (reset) {
|
|
m_leaderChoosen[team] = false;
|
|
return;
|
|
}
|
|
|
|
if (m_leaderChoosen[team]) {
|
|
return;
|
|
}
|
|
|
|
if (game.mapIs (MapFlags::Assassination)) {
|
|
if (team == Team::CT && !m_leaderChoosen[Team::CT]) {
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_isVIP) {
|
|
// vip bot is the leader
|
|
bot->m_isLeader = true;
|
|
|
|
if (rg.chance (50)) {
|
|
bot->pushRadioMessage (Radio::FollowMe);
|
|
bot->m_campButtons = 0;
|
|
}
|
|
}
|
|
}
|
|
m_leaderChoosen[Team::CT] = true;
|
|
}
|
|
else if (team == Team::Terrorist && !m_leaderChoosen[Team::Terrorist]) {
|
|
auto bot = bots.findHighestFragBot (team);
|
|
|
|
if (bot != nullptr && bot->m_notKilled) {
|
|
bot->m_isLeader = true;
|
|
|
|
if (rg.chance (45)) {
|
|
bot->pushRadioMessage (Radio::FollowMe);
|
|
}
|
|
}
|
|
m_leaderChoosen[Team::Terrorist] = true;
|
|
}
|
|
}
|
|
else if (game.mapIs (MapFlags::Demolition)) {
|
|
if (team == Team::Terrorist && !m_leaderChoosen[Team::Terrorist]) {
|
|
for (const auto &bot : m_bots) {
|
|
if (bot->m_hasC4) {
|
|
// bot carrying the bomb is the leader
|
|
bot->m_isLeader = true;
|
|
|
|
// terrorist carrying a bomb needs to have some company
|
|
if (rg.chance (75)) {
|
|
if (cv_radio_mode.int_ () == 2) {
|
|
bot->pushChatterMessage (Chatter::GoingToPlantBomb);
|
|
}
|
|
else {
|
|
bot->pushChatterMessage (Radio::FollowMe);
|
|
}
|
|
bot->m_campButtons = 0;
|
|
}
|
|
}
|
|
}
|
|
m_leaderChoosen[Team::Terrorist] = true;
|
|
}
|
|
else if (!m_leaderChoosen[Team::CT]) {
|
|
if (auto bot = bots.findHighestFragBot (team)) {
|
|
bot->m_isLeader = true;
|
|
|
|
if (rg.chance (30)) {
|
|
bot->pushRadioMessage (Radio::FollowMe);
|
|
}
|
|
}
|
|
m_leaderChoosen[Team::CT] = true;
|
|
}
|
|
}
|
|
else if (game.mapIs (MapFlags::Escape | MapFlags::KnifeArena | MapFlags::Fun)) {
|
|
auto bot = bots.findHighestFragBot (team);
|
|
|
|
if (!m_leaderChoosen[team] && bot) {
|
|
bot->m_isLeader = true;
|
|
|
|
if (rg.chance (30)) {
|
|
bot->pushRadioMessage (Radio::FollowMe);
|
|
}
|
|
m_leaderChoosen[team] = true;
|
|
}
|
|
}
|
|
else {
|
|
auto bot = bots.findHighestFragBot (team);
|
|
|
|
if (!m_leaderChoosen[team] && bot) {
|
|
bot->m_isLeader = true;
|
|
|
|
if (rg.chance (team == Team::Terrorist ? 30 : 40)) {
|
|
bot->pushRadioMessage (Radio::FollowMe);
|
|
}
|
|
m_leaderChoosen[team] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void BotManager::initRound () {
|
|
// this is called at the start of each round
|
|
|
|
m_roundOver = false;
|
|
|
|
// check team economics
|
|
for (int team = 0; team < kGameTeamNum; ++team) {
|
|
updateTeamEconomics (team);
|
|
selectLeaders (team, true);
|
|
|
|
m_lastRadioTime[team] = 0.0f;
|
|
}
|
|
reset ();
|
|
|
|
// notify all bots about new round arrived
|
|
for (const auto &bot : bots) {
|
|
bot->newRound ();
|
|
}
|
|
|
|
// reset current radio message for all client
|
|
for (auto &client : util.getClients ()) {
|
|
client.radio = 0;
|
|
}
|
|
|
|
graph.setBombOrigin (true);
|
|
graph.clearVisited ();
|
|
|
|
m_bombSayStatus = BombPlantedSay::ChatSay | BombPlantedSay::Chatter;
|
|
m_timeBombPlanted = 0.0f;
|
|
m_plantSearchUpdateTime = 0.0f;
|
|
m_autoKillCheckTime = 0.0f;
|
|
m_botsCanPause = false;
|
|
|
|
resetFilters ();
|
|
graph.updateGlobalPractice (); // update experience data on round start
|
|
|
|
// calculate the round mid/end in world time
|
|
m_timeRoundStart = game.time () + mp_freezetime.float_ ();
|
|
m_timeRoundMid = m_timeRoundStart + mp_roundtime.float_ () * 60.0f * 0.5f;
|
|
m_timeRoundEnd = m_timeRoundStart + mp_roundtime.float_ () * 60.0f;
|
|
|
|
// update difficulty balance, if needed
|
|
if (cv_difficulty_auto.bool_ ()) {
|
|
balanceBotDifficulties ();
|
|
}
|
|
}
|
|
|
|
void BotManager::setBombPlanted (bool isPlanted) {
|
|
if (isPlanted) {
|
|
m_timeBombPlanted = game.time ();
|
|
}
|
|
m_bombPlanted = isPlanted;
|
|
}
|