diff --git a/ext/crlib/cr-array.h b/ext/crlib/cr-array.h index fe613e6..4f8c520 100644 --- a/ext/crlib/cr-array.h +++ b/ext/crlib/cr-array.h @@ -413,4 +413,3 @@ public: template using SmallArray = Array ; CR_NAMESPACE_END - diff --git a/ext/crlib/cr-binheap.h b/ext/crlib/cr-binheap.h index a328d69..f50d2f0 100644 --- a/ext/crlib/cr-binheap.h +++ b/ext/crlib/cr-binheap.h @@ -156,4 +156,4 @@ private: } }; -CR_NAMESPACE_END \ No newline at end of file +CR_NAMESPACE_END diff --git a/ext/crlib/cr-complete.h b/ext/crlib/cr-complete.h index 6173fef..55ec4ac 100644 --- a/ext/crlib/cr-complete.h +++ b/ext/crlib/cr-complete.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include diff --git a/ext/crlib/cr-deque.h b/ext/crlib/cr-deque.h new file mode 100644 index 0000000..60138fc --- /dev/null +++ b/ext/crlib/cr-deque.h @@ -0,0 +1,232 @@ +// +// CRLib - Simple library for STL replacement in private projects. +// Copyright © 2020 YaPB Development Team . +// +// 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 3 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. +// + +#pragma once + +#include +#include +#include + +CR_NAMESPACE_BEGIN + +template class Deque : private DenyCopying { +private: + size_t capacity_ {}; + + UniquePtr contents_ {}; + Twin index_ {}; + +private: + size_t pickFrontIndex () { + if (index_.first == 0) { + if (capacity_ && index_.second != capacity_ - 1) { + return capacity_ - 1; + } + } + else if (index_.first - 1 != index_.second) { + return index_.first - 1; + } + extendCapacity (); + + return capacity_ - 1; + } + + size_t pickRearIndex () { + if (index_.second < index_.first) { + if (index_.second + 1 != index_.first) { + return index_.second + 1; + } + } + else { + if (index_.second + 1 < capacity_) { + return index_.second + 1; + } + if (index_.first != 0) { + return 0; + } + } + extendCapacity (); + + return index_.second + 1; + } + + void extendCapacity () { + auto capacity = capacity_ ? capacity_ * 2 : 8; + auto contents = cr::makeUnique (sizeof (T) * capacity); + + if (index_.first < index_.second) { + for (size_t i = 0; i < index_.second - index_.first; ++i) { + contents[i] = cr::move (contents_[index_.first + i]); + } + index_.second = index_.second - index_.first; + index_.first = 0; + } + else { + for (size_t i = 0; i < capacity_ - index_.first; ++i) { + contents[i] = cr::move (contents_[index_.first + i]); + } + + for (size_t i = 0; i < index_.second; ++i) { + contents[capacity_ - index_.first + i] = cr::move (contents_[i]); + } + index_.second = index_.second + (capacity_ - index_.first); + index_.first = 0; + } + contents_ = cr::move (contents); + capacity_ = capacity; + } + + void destroy () { + auto destruct = [&] (size_t start, size_t end) { + for (size_t i = start; i < end; ++i) { + cr::alloc.destruct (&contents_[i]); + } + }; + + if (index_.first <= index_.second) { + destruct (index_.first, index_.second); + } + else { + destruct (index_.first, capacity_); + destruct (0, index_.second); + } + contents_ = nullptr; + } + + void reset () { + contents_ = nullptr; + capacity_ = 0; + index_ = {}; + } + +public: + explicit Deque () : contents_ (nullptr), capacity_ (0) + { } + + Deque (Deque &&rhs) : contents_ (cr::move (rhs.contents_)), capacity_ (rhs.capacity_) { + index_.first (rhs.index_.first); + index_.second (rhs.index_.second); + + rhs.reset (); + } + + ~Deque () { + destroy (); + } + +public: + bool empty () const { + return index_.first == index_.second; + } + + template void emplaceLast (U &&object) { + auto rear = pickRearIndex (); + + cr::alloc.construct (&contents_[index_.second], cr::forward (object)); + index_.second = rear; + } + + template void emplaceFront (U &&object) { + index_.first = pickFrontIndex (); + + cr::alloc.construct (&contents_[index_.first], cr::forward (object)); + } + + void discardFront () { + cr::alloc.destruct (&contents_[index_.first]); + + if (index_.first == capacity_ - 1) { + index_.first = 0; + } + else { + index_.first++; + } + } + + void discardLast () { + if (index_.second == 0) { + index_.second = capacity_ - 1; + } + else { + index_.second--; + } + cr::alloc.destruct (&contents_[index_.second]); + } + + T popFront () { + auto first (front ()); + discardFront (); + + return first; + } + + T popLast () { + auto last (back ()); + discardLast (); + + return last; + } + +public: + const T &front () const { + return contents_[index_.first]; + } + + const T &last () const { + if (index_.second == 0) { + return contents_[capacity_ - 1]; + } + return contents_[index_.second - 1]; + } + + T &front () { + return contents_[index_.first]; + } + + T &last () { + if (index_.second == 0) { + return contents_[capacity_ - 1]; + } + return contents_[index_.second - 1]; + } + + size_t length () const { + if (index_.first == index_.second) { + return 0; + } + return index_.first < index_.second ? index_.second - index_.first : index_.second + (capacity_ - index_.first); + } + + void clear () { + index_.first = 0; + index_.second = 0; + } + +public: + Deque &operator = (Deque &&rhs) { + destroy (); + + contents_ = cr::move (rhs.contents_); + capacity_ = rhs.capacity_; + + index_.first = rhs.index_.first; + index_.second = rhs.index_.second; + + rhs.reset (); + return *this; + } +}; + +CR_NAMESPACE_END diff --git a/ext/crlib/cr-detour.h b/ext/crlib/cr-detour.h index c803eda..cd5b76f 100644 --- a/ext/crlib/cr-detour.h +++ b/ext/crlib/cr-detour.h @@ -186,7 +186,23 @@ public: detour_ = nullptr; } +private: + class DetourSwitch final { + private: + Detour *detour_; + + public: + DetourSwitch (Detour *detour) : detour_ (detour) { + detour_->restore (); + } + + ~DetourSwitch () { + detour_->detour (); + } + }; + public: + void install (void *detour, const bool enable = false) { if (!original_) { return; @@ -228,11 +244,8 @@ public: } template decltype (auto) operator () (Args &&...args) { - restore (); - auto res = reinterpret_cast (original_) (args...); - detour (); - - return res; + DetourSwitch sw (this); + return reinterpret_cast (original_) (args...); } }; diff --git a/ext/crlib/cr-logger.h b/ext/crlib/cr-logger.h index efb1986..a4c8d59 100644 --- a/ext/crlib/cr-logger.h +++ b/ext/crlib/cr-logger.h @@ -105,4 +105,4 @@ public: // expose global instance CR_EXPOSE_GLOBAL_SINGLETON (SimpleLogger, logger); -CR_NAMESPACE_END \ No newline at end of file +CR_NAMESPACE_END diff --git a/ext/hlsdk/progdefs.h b/ext/hlsdk/progdefs.h index 6310586..c46a030 100644 --- a/ext/hlsdk/progdefs.h +++ b/ext/hlsdk/progdefs.h @@ -211,4 +211,3 @@ typedef struct entvars_s { edict_t *euser3; edict_t *euser4; } entvars_t; - diff --git a/inc/engine.h b/inc/engine.h index 53b7e6b..91b4414 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -355,6 +355,11 @@ public: return !m_breakables.empty (); } + // find variable value by variable name + StringRef findCvar (StringRef name) { + return engfuncs.pfnCVarGetString (name.chars ()); + } + // helper to sending the client message void sendClientMessage (bool console, edict_t *ent, const char *message); diff --git a/inc/manager.h b/inc/manager.h index 6411645..d6d6f48 100644 --- a/inc/manager.h +++ b/inc/manager.h @@ -117,6 +117,7 @@ public: int getAliveHumansCount (); float getConnectTime (int botId, float original); + float getAverageTeamKPD (bool calcForBots); void setBombPlanted (bool isPlanted); void frame (); @@ -142,6 +143,7 @@ public: void setWeaponMode (int selection); void updateTeamEconomics (int team, bool setTrue = false); void updateBotDifficulties (); + void balanceBotDifficulties (); void reset (); void initFilters (); void resetFilters (); diff --git a/inc/message.h b/inc/message.h index 0091ad3..73652bb 100644 --- a/inc/message.h +++ b/inc/message.h @@ -38,7 +38,8 @@ CR_DECLARE_SCOPED_ENUM (NetMsg, NVGToggle = 19, FlashBat = 20, Fashlight = 21, - ItemStatus = 22 + ItemStatus = 22, + ScoreInfo = 23 ) // vgui menus (since latest steam updates is obsolete, but left for old cs) @@ -124,6 +125,7 @@ private: void netMsgItemStatus (); void netMsgNVGToggle (); void netMsgFlashBat (); + void netMsgScoreInfo (); public: MessageDispatcher (); diff --git a/inc/yapb.h b/inc/yapb.h index 58db048..28cf201 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -760,7 +760,6 @@ private: int findDefendNode (const Vector &origin); int findBestGoal (); int findGoalPost (int tactic, IntArray *defensive, IntArray *offsensive); - int getMsgQueue (); int bestPrimaryCarried (); int bestSecondaryCarried (); int bestGrenadeCarried (); @@ -975,6 +974,8 @@ public: float m_firePause; // time to pause firing float m_shootTime; // time to shoot float m_timeLastFired; // time to last firing + float m_difficultyChange; // time when auto-difficulty was last applied to this bot + float m_kpdRatio; // kill per death ratio int m_basePing; // base ping for bot int m_numEnemiesLeft; // number of enemies alive left on map @@ -988,8 +989,6 @@ public: int m_buyState; // current count in buying int m_blindButton; // buttons bot press, when blind int m_radioOrder; // actual command - int m_actMessageIndex; // current processed message - int m_pushMessageIndex; // offset for next pushed message int m_prevGoalIndex; // holds destination goal node int m_chosenGoalIndex; // used for experience, same as above int m_lastDamageType; // stores last damage @@ -1034,6 +1033,7 @@ public: BurstMode m_weaponBurstMode; // bot using burst mode? (famas/glock18, but also silencer mode) Personality m_personality; // bots type Array m_tasks; + Deque m_msgQueue; public: Bot (edict_t *bot, int difficulty, int personality, int team, int member); diff --git a/src/botlib.cpp b/src/botlib.cpp index 1500cae..b9a256b 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -45,15 +45,6 @@ ConVar mp_footsteps ("mp_footsteps", nullptr, Var::GameRef); ConVar sv_gravity ("sv_gravity", nullptr, Var::GameRef); -int Bot::getMsgQueue () { - // this function get the current message from the bots message queue - - int message = m_messageQueue[m_actMessageIndex++]; - m_actMessageIndex &= 0x1f; // wraparound - - return message; -} - void Bot::pushMsgQueue (int message) { // this function put a message into the bot message queue @@ -71,8 +62,7 @@ void Bot::pushMsgQueue (int message) { } } } - m_messageQueue[m_pushMessageIndex++] = message; - m_pushMessageIndex &= 0x1f; // wraparound + m_msgQueue.emplaceLast (message); } float Bot::isInFOV (const Vector &destination) { @@ -1016,11 +1006,12 @@ void Bot::checkMsgQueue () { extern ConVar mp_freezetime; // no new message? - if (m_actMessageIndex == m_pushMessageIndex) { + if (m_msgQueue.empty ()) { return; } - // get message from stack - int state = getMsgQueue (); + + // get message from deque + auto state = m_msgQueue.popFront (); // nothing to do? if (state == BotMsg::None || (state == BotMsg::Radio && game.is (GameFlags::FreeForAll))) { @@ -1072,7 +1063,7 @@ void Bot::checkMsgQueue () { // prevent terrorists from buying on es maps if (game.mapIs (MapFlags::Escape) && m_team == Team::Terrorist) { - m_buyState = BuyState::Done;; + m_buyState = BuyState::Done; } // prevent teams from buying on fun maps @@ -1184,20 +1175,25 @@ bool Bot::isWeaponRestricted (int weaponIndex) { bool Bot::isWeaponRestrictedAMX (int weaponIndex) { // this function checks restriction set by AMX Mod, this function code is courtesy of KWo. + if (!game.is (GameFlags::Metamod)) { + return false; + } + + // check for weapon restrictions if (cr::bit (weaponIndex) & (kPrimaryWeaponMask | kSecondaryWeaponMask | Weapon::Shield)) { - auto restrictedWeapons = engfuncs.pfnCVarGetString ("amx_restrweapons"); + auto restrictedWeapons = game.findCvar ("amx_restrweapons"); - if (strings.isEmpty (restrictedWeapons)) { + if (restrictedWeapons.empty ()) { return false; } - int indices[] = {4, 25, 20, -1, 8, -1, 12, 19, -1, 5, 6, 13, 23, 17, 18, 1, 2, 21, 9, 24, 7, 16, 10, 22, -1, 3, 15, 14, 0, 11}; + constexpr int indices[] = {4, 25, 20, -1, 8, -1, 12, 19, -1, 5, 6, 13, 23, 17, 18, 1, 2, 21, 9, 24, 7, 16, 10, 22, -1, 3, 15, 14, 0, 11}; // find the weapon index int index = indices[weaponIndex - 1]; // validate index range - if (index < 0 || index >= static_cast (strlen (restrictedWeapons))) { + if (index < 0 || index >= static_cast (restrictedWeapons.length ())) { return false; } return restrictedWeapons[index] != '0'; @@ -1205,18 +1201,18 @@ bool Bot::isWeaponRestrictedAMX (int weaponIndex) { // check for equipment restrictions else { - auto restrictedEquipment = engfuncs.pfnCVarGetString ("amx_restrequipammo"); + auto restrictedEquipment = game.findCvar ("amx_restrequipammo"); - if (strings.isEmpty (restrictedEquipment)) { + if (restrictedEquipment.empty ()) { return false; } - int indices[] = {-1, -1, -1, 3, -1, -1, -1, -1, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, 0, 1, 5}; + constexpr int indices[] = {-1, -1, -1, 3, -1, -1, -1, -1, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, 0, 1, 5}; // find the weapon index int index = indices[weaponIndex - 1]; // validate index range - if (index < 0 || index >= static_cast (strlen (restrictedEquipment))) { + if (index < 0 || index >= static_cast (restrictedEquipment.length ())) { return false; } return restrictedEquipment[index] != '0'; @@ -1517,7 +1513,7 @@ void Bot::buyStuff () { break; case BuyState::SecondaryWeapon: // if bot has still some money, buy a better secondary weapon - if (isPistolMode || (isFirstRound && hasDefaultPistols) || (hasDefaultPistols && bots.getLastWinner () == m_team && m_moneyAmount > rg.int_ (2000, 3000)) || (hasPrimaryWeapon () && hasDefaultPistols && m_moneyAmount > rg.int_ (7500, 9000))) { + if (isPistolMode || (isFirstRound && hasDefaultPistols && rg.chance (50)) || (hasDefaultPistols && bots.getLastWinner () == m_team && m_moneyAmount > rg.int_ (2000, 3000)) || (hasPrimaryWeapon () && hasDefaultPistols && m_moneyAmount > rg.int_ (7500, 9000))) { do { pref--; @@ -1952,11 +1948,12 @@ void Bot::filterTasks () { ratio = timeHeard * 0.1f; } bool lowAmmo = m_ammoInClip[m_currentWeapon] < conf.findWeaponById (m_currentWeapon).maxClip * 0.18f; + bool sniping = m_sniperStopTime <= game.time () && lowAmmo; if (bots.isBombPlanted () || m_isStuck || usesKnife ()) { ratio /= 3.0f; // reduce the seek cover desire if bomb is planted } - else if (m_isVIP || m_isReloading || (lowAmmo && usesSniper ())) { + else if (m_isVIP || m_isReloading || (sniping && usesSniper ())) { ratio *= 3.0f; // triple the seek cover desire if bot is VIP or reloading } else { @@ -3000,7 +2997,7 @@ void Bot::update () { else if (m_buyingFinished && !(pev->maxspeed < 10.0f && getCurrentTaskId () != Task::PlantBomb && getCurrentTaskId () != Task::DefuseBomb) && !cv_freeze_bots.bool_ () && !graph.hasChanged ()) { botMovement = true; } - checkMsgQueue (); // check for pending messages + checkMsgQueue (); if (botMovement) { logic (); // execute main code @@ -3147,7 +3144,7 @@ void Bot::normal_ () { m_timeCamping = game.time () + rg.float_ (10.0f, 25.0f); startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, m_timeCamping, true); - m_camp = m_path->origin + m_path->start.forward () * 500.0f;; + m_camp = m_path->origin + m_path->start.forward () * 500.0f; m_aimFlags |= AimFlags::Camp; m_campDirection = 0; @@ -5796,32 +5793,34 @@ bool Bot::isBombDefusing (const Vector &bombOrigin) { return false; } bool defusingInProgress = false; + constexpr auto distanceToBomb = cr::square (140.0f); for (const auto &client : util.getClients ()) { auto bot = bots[client.ent]; + auto bombDistance = (client.ent->v.origin - bombOrigin).lengthSq (); - if (bot == nullptr || bot == this || !bot->m_notKilled) { - continue; // skip invalid bots - } - - if (m_team != bot->m_team || bot->getCurrentTaskId () == Task::EscapeFromBomb) { - continue; // skip other mess - } - float bombDistance = (client.ent->v.origin - bombOrigin).lengthSq (); - - if (bombDistance < cr::square (140.0f) && (bot->getCurrentTaskId () == Task::DefuseBomb || bot->m_hasProgressBar)) { - defusingInProgress = true; - break; - } - - // take in account peoples too - if (defusingInProgress || !(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team != m_team || util.isFakeClient (client.ent)) { + if (bot && !bot->m_notKilled) { + if (m_team != bot->m_team || bot->getCurrentTaskId () == Task::EscapeFromBomb) { + continue; // skip other mess + } + + // if close enough, mark as progressing + if (bombDistance < distanceToBomb && (bot->getCurrentTaskId () == Task::DefuseBomb || bot->m_hasProgressBar)) { + defusingInProgress = true; + break; + } continue; } - if (bombDistance < cr::square (140.0f) && ((client.ent->v.button | client.ent->v.oldbuttons) & IN_USE)) { - defusingInProgress = true; - break; + // take in account peoples too + if ((client.flags & ClientFlags::Used) && (client.flags & ClientFlags::Alive) && client.team == m_team) { + + // if close enough, mark as progressing + if (bombDistance < distanceToBomb && ((client.ent->v.button | client.ent->v.oldbuttons) & IN_USE)) { + defusingInProgress = true; + break; + } + continue; } } return defusingInProgress; @@ -5880,4 +5879,4 @@ bool Bot::isEnemyInFrustum (edict_t *enemy) { } } return true; -} \ No newline at end of file +} diff --git a/src/combat.cpp b/src/combat.cpp index 82ace4d..b69b5c8 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -505,7 +505,7 @@ float Bot::getEnemyBodyOffsetCorrection (float distance) { { 6.5f, 6.5f, 4.5f }, // pistol { 9.5f, 9.0f, -5.0f }, // shotgun { 4.5f, 3.5f, -5.0f }, // zoomrifle - { 5.5f, 1.0f, -4.5f }, // rifle + { 4.5f, 1.0f, -4.5f }, // rifle { 5.5f, 3.5f, -4.5f }, // smg { 3.5f, 3.5f, 4.5f }, // sniper { 2.5f, -2.0f, -6.0f } // heavy @@ -1243,7 +1243,7 @@ bool Bot::usesPistol () { } bool Bot::usesSubmachine () { - return m_weaponType == WeaponType::SMG;; + return m_weaponType == WeaponType::SMG; } bool Bot::usesShotgun () { diff --git a/src/linkage.cpp b/src/linkage.cpp index 6c76137..dd1ba0d 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -960,4 +960,3 @@ DLL_GIVEFNPTRSTODLL GiveFnptrsToDll (enginefuncs_t *functionTable, globalvars_t // add linkents for android #include "android.cpp" - diff --git a/src/manager.cpp b/src/manager.cpp index c2937c4..cc4a3cc 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -26,10 +26,14 @@ ConVar cv_autokill_delay ("yb_autokill_delay", "0.0", "Specifies amount of time 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); @@ -187,7 +191,6 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal } } } - BotName *botName = nullptr; // setup name @@ -765,6 +768,27 @@ float BotManager::getConnectTime (int botId, float original) { return original; } +float BotManager::getAverageTeamKPD (bool calcForBots) { + Twin calc {}; + + for (const auto &client : util.getClients ()) { + if (!(client.flags & ClientFlags::Used)) { + continue; + } + auto bot = bots[client.ent]; + + if ((calcForBots && bot) || (!calcForBots && !bot)) { + calc.first += client.ent->v.frags; + calc.second++; + } + } + + if (calc.second > 0) { + return calc.first / calc.second; + } + return 0.0f; +} + Twin BotManager::countTeamPlayers () { int ts = 0, cts = 0; @@ -839,7 +863,11 @@ void BotManager::updateTeamEconomics (int team, bool setTrue) { } void BotManager::updateBotDifficulties () { - int difficulty = cv_difficulty.int_ (); + // 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) { @@ -851,6 +879,35 @@ void BotManager::updateBotDifficulties () { } } +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 (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) @@ -922,13 +979,25 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int member) { m_notKilled = false; m_weaponBurstMode = BurstMode::Off; - m_difficulty = cr::clamp (difficulty, 0, 4); + m_difficulty = cr::clamp (static_cast (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.int_ (minDifficulty, maxDifficulty); + } m_basePing = rg.int_ (7, 14); 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.float_ (30.0f, 240.0f); @@ -968,8 +1037,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int member) { m_nextEmotionUpdate = game.time () + 0.5f; // just to be sure - m_actMessageIndex = 0; - m_pushMessageIndex = 0; + m_msgQueue.clear (); // assign team and class m_wantedTeam = team; @@ -1304,9 +1372,7 @@ void Bot::newRound () { for (auto &msg : m_messageQueue) { msg = BotMsg::None; } - - m_actMessageIndex = 0; - m_pushMessageIndex = 0; + m_msgQueue.clear (); // clear last trace for (auto i = 0; i < TraceChannel::Num; ++i) { @@ -1690,6 +1756,11 @@ void BotManager::initRound () { 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) { diff --git a/src/message.cpp b/src/message.cpp index 08c927f..f433f1a 100644 --- a/src/message.cpp +++ b/src/message.cpp @@ -332,6 +332,26 @@ void MessageDispatcher::netMsgTeamInfo () { client.team = game.is (GameFlags::FreeForAll) ? m_args[index].long_ : client.team2; } +void MessageDispatcher::netMsgScoreInfo () { + // this message gets sent when scoreboard info is update, we're use it to track k-d ratio + + enum args { index = 0, score = 1, deaths = 2, class_id = 3, team_id = 4, min = 5 }; + + // check the minimum states + if (m_args.length () < min) { + return; + } + auto &client = util.getClient (m_args[index].long_ - 1); + + // get the bot in this msg + auto bot = bots[client.ent]; + + // if we're have bot, set the kd ratio + if (bot != nullptr) { + bot->m_kpdRatio = bot->pev->frags / cr::max (m_args[deaths].long_, 1); + } +} + void MessageDispatcher::netMsgBarTime () { enum args { enabled = 0, min = 1 }; @@ -415,6 +435,7 @@ MessageDispatcher::MessageDispatcher () { addWanted ("ItemStatus", NetMsg::ItemStatus, &MessageDispatcher::netMsgItemStatus); addWanted ("NVGToggle", NetMsg::NVGToggle, &MessageDispatcher::netMsgNVGToggle); addWanted ("FlashBat", NetMsg::FlashBat, &MessageDispatcher::netMsgFlashBat); + addWanted ("ScoreInfo", NetMsg::ScoreInfo, &MessageDispatcher::netMsgScoreInfo); // we're need next messages IDs but we're won't handle them, so they will be removed from wanted list as soon as they get engine IDs addWanted ("BotVoice", NetMsg::BotVoice, nullptr); diff --git a/vc/yapb.rc b/vc/yapb.rc index a6f8a1e..f922a7d 100644 --- a/vc/yapb.rc +++ b/vc/yapb.rc @@ -44,4 +44,4 @@ FILETYPE 0x2 { BLOCK "VarFileInfo" { VALUE "Translation", 0x400, 1200 } -} \ No newline at end of file +} diff --git a/vc/yapb.vcxproj b/vc/yapb.vcxproj index 5ae9f6c..ac5443a 100644 --- a/vc/yapb.vcxproj +++ b/vc/yapb.vcxproj @@ -17,6 +17,7 @@ + diff --git a/vc/yapb.vcxproj.filters b/vc/yapb.vcxproj.filters index 2837356..388638a 100644 --- a/vc/yapb.vcxproj.filters +++ b/vc/yapb.vcxproj.filters @@ -141,6 +141,9 @@ inc\ext\crlib + + inc\ext\crlib +