From b6e516592c43be79f2be4783463d980450bedf43 Mon Sep 17 00:00:00 2001 From: caxanga334 <10157643+caxanga334@users.noreply.github.com> Date: Wed, 3 Apr 2024 15:42:32 -0300 Subject: [PATCH] [TF2] Add Bot Upgrade Manager --- configs/tf/mvm_upgrades.cfg | 1 + extension/AMBuilder | 1 + extension/bot/tf2/tf2bot.cpp | 16 ++ extension/bot/tf2/tf2bot.h | 6 + extension/bot/tf2/tf2bot_upgrades.cpp | 223 +++++++++++++++++++++ extension/bot/tf2/tf2bot_upgrades.h | 105 ++++++++++ extension/mods/tf2/mvm_upgrade.h | 21 +- extension/mods/tf2/mvm_upgrade_manager.cpp | 84 +++++++- extension/mods/tf2/mvm_upgrade_manager.h | 12 +- 9 files changed, 459 insertions(+), 10 deletions(-) create mode 100644 extension/bot/tf2/tf2bot_upgrades.cpp create mode 100644 extension/bot/tf2/tf2bot_upgrades.h diff --git a/configs/tf/mvm_upgrades.cfg b/configs/tf/mvm_upgrades.cfg index e6517e2..685ee6b 100644 --- a/configs/tf/mvm_upgrades.cfg +++ b/configs/tf/mvm_upgrades.cfg @@ -16,6 +16,7 @@ TFMvMUpgrades * * priority, bots will buy upgrades with the lowest priority first. * in case two or more upgrades have the same priority, they will be selected randomly + * priority must start at 1, bot thinks they finished their upgrades when no upgrades are found for a given priority * "priority" "1" * "slot" "0" -- weapon slot used by this upgrade, -1 for player upgrades, 9 for canteen power up bottle * "weapons" "5,7,333" -- comma-delimited list of item definition index this upgrade is restricted to diff --git a/extension/AMBuilder b/extension/AMBuilder index 45a7c1b..7399b9a 100644 --- a/extension/AMBuilder +++ b/extension/AMBuilder @@ -64,6 +64,7 @@ sourceFiles = [ 'bot/tf2/tf2bot_controller.cpp', 'bot/tf2/tf2bot_movement.cpp', 'bot/tf2/tf2bot_sensor.cpp', + 'bot/tf2/tf2bot_upgrades.cpp', 'bot/tf2/tasks/tf2bot_maintask.cpp', 'bot/tf2/tasks/tf2bot_tactical.cpp', 'bot/tf2/tasks/tf2bot_scenario.cpp', diff --git a/extension/bot/tf2/tf2bot.cpp b/extension/bot/tf2/tf2bot.cpp index 5c3a126..49d71e1 100644 --- a/extension/bot/tf2/tf2bot.cpp +++ b/extension/bot/tf2/tf2bot.cpp @@ -23,6 +23,7 @@ CTF2Bot::CTF2Bot(edict_t* edict) : CBaseBot(edict) m_tf2behavior = std::make_unique(this); m_desiredclass = TeamFortress2::TFClassType::TFClass_Unknown; m_knownspythink = TIME_TO_TICKS(knownspy_think_interval()); + m_upgrademan.SetMe(this); } CTF2Bot::~CTF2Bot() @@ -64,6 +65,7 @@ void CTF2Bot::Spawn() void CTF2Bot::FirstSpawn() { CBaseBot::FirstSpawn(); + m_upgrademan.InitUpgrades(); } int CTF2Bot::GetMaxHealth() const @@ -472,6 +474,20 @@ void CTF2Bot::FindMyBuildings() } } +int CTF2Bot::GetCurrency() const +{ + int currency = 0; + entprops->GetEntProp(GetIndex(), Prop_Send, "m_nCurrency", currency); + return currency; +} + +bool CTF2Bot::IsInUpgradeZone() const +{ + bool val = false; + entprops->GetEntPropBool(GetIndex(), Prop_Send, "m_bInUpgradeZone", val); + return val; +} + const CTF2Bot::KnownSpy* CTF2Bot::GetKnownSpy(edict_t* spy) const { int index = gamehelpers->IndexOfEdict(spy); diff --git a/extension/bot/tf2/tf2bot.h b/extension/bot/tf2/tf2bot.h index 2509ab4..98e783a 100644 --- a/extension/bot/tf2/tf2bot.h +++ b/extension/bot/tf2/tf2bot.h @@ -14,6 +14,7 @@ #include "tf2bot_controller.h" #include "tf2bot_movement.h" #include "tf2bot_sensor.h" +#include "tf2bot_upgrades.h" struct edict_t; @@ -102,6 +103,8 @@ class CTF2Bot : public CBaseBot void SetMyTeleporterEntrance(edict_t* entity); void SetMyTeleporterExit(edict_t* entity); void FindMyBuildings(); + int GetCurrency() const; + bool IsInUpgradeZone() const; /** * @brief Gets a known spy information about a spy or NULL if none @@ -111,6 +114,8 @@ class CTF2Bot : public CBaseBot const KnownSpy* GetKnownSpy(edict_t* spy) const; const KnownSpy* UpdateOrCreateKnownSpy(edict_t* spy, KnownSpyInfo info, const bool updateinfo = false, const bool updateclass = false); + CTF2BotUpgradeManager& GetUpgradeManager() { return m_upgrademan; } + private: std::unique_ptr m_tf2movement; std::unique_ptr m_tf2controller; @@ -124,6 +129,7 @@ class CTF2Bot : public CBaseBot CBaseHandle m_myTeleporterExit; std::vector m_knownspies; int m_knownspythink; + CTF2BotUpgradeManager m_upgrademan; inline void UpdateKnownSpies() { diff --git a/extension/bot/tf2/tf2bot_upgrades.cpp b/extension/bot/tf2/tf2bot_upgrades.cpp new file mode 100644 index 0000000..bbad1e1 --- /dev/null +++ b/extension/bot/tf2/tf2bot_upgrades.cpp @@ -0,0 +1,223 @@ +#include +#include +#include +#include +#include +#include +#include "tf2bot.h" +#include "tf2bot_upgrades.h" + +CTF2BotUpgradeManager::CTF2BotUpgradeManager() +{ + m_me = nullptr; + m_nextpriority = 1; + m_numupgrades = 0; + m_tobuylist.reserve(16); + m_boughtlist.reserve(32); + m_state = STATE_GOBUY; +} + +CTF2BotUpgradeManager::~CTF2BotUpgradeManager() +{ +} + +void CTF2BotUpgradeManager::Update() +{ + if (!CanAffordAnyUpgrade() && m_state != STATE_DOREFUND) + { + OnDoneUpgrading(); + return; + } + + switch (m_state) + { + case CTF2BotUpgradeManager::STATE_GOBUY: + m_state = STATE_BUYINGSTUFF; + ExecuteBuy(); + RemoveFinishedUpgrades(); // clean up + return; + case CTF2BotUpgradeManager::STATE_BUYINGSTUFF: + ExecuteBuy(); + RemoveFinishedUpgrades(); // clean up + return; + case CTF2BotUpgradeManager::STATE_WAITFORNEXTWAVE: + return; + case CTF2BotUpgradeManager::STATE_DOREFUND: + ExecuteRefund(); + OnUpgradesRefunded(); + return; + default: + return; + } +} + +void CTF2BotUpgradeManager::ExecuteBuy() +{ + if (!m_me->IsInUpgradeZone()) + { + m_me->DebugPrintToConsole(BOTDEBUG_ERRORS, 255, 0, 0, "Error: CTF2BotUpgradeManager::Update called outside an upgrade zone!\n"); + return; + } + + if (m_tobuylist.empty()) + { + m_me->DebugPrintToConsole(BOTDEBUG_TASKS, 0, 150, 0, "%s CTF2BotUpgradeManager::Update buy list is empty, advancing priority!\n", m_me->GetDebugIdentifier()); + AdvancePriority(); + FetchUpgrades(); + return; + } + + auto upgradeinfo = m_tobuylist[randomgen->GetRandomInt(0U, m_tobuylist.size() - 1U)]; + int currency = m_me->GetCurrency(); + auto data = GetOrCreateUpgradeData(upgradeinfo->upgrade); + + if (data->IsMaxed()) + { + RemoveFinishedUpgrades(); // clean up + return; + } + + if (!upgradeinfo->upgrade->CanAfford(m_me->GetCurrency())) + { + return; + } + + if (m_me->IsDebugging(BOTDEBUG_TASKS)) + { + m_me->DebugPrintToConsole(BOTDEBUG_TASKS, 255, 87, 51, "%s bought upgrade ID %i <%s, %i>", m_me->GetDebugIdentifier(), upgradeinfo->GetUpgradeIndex(), + upgradeinfo->attribute.c_str(), upgradeinfo->quality); + } + + BeginBuyingUpgrades(); + BuySingleUpgrade(upgradeinfo->GetUpgradeIndex(), upgradeinfo->itemslot, 1); + EndBuyingUpgrades(); + data->times_bought++; // for now we must assume that buying an upgrade will never fail +} + +void CTF2BotUpgradeManager::ExecuteRefund() +{ + BeginBuyingUpgrades(); + RefundUpgrades(); + EndBuyingUpgrades(); +} + +bool CTF2BotUpgradeManager::CanAffordAnyUpgrade() const +{ + int currency = m_me->GetCurrency(); + + for (auto data : m_tobuylist) + { + if (data->upgrade->CanAfford(currency)) + { + return true; + } + } + + return false; +} + +void CTF2BotUpgradeManager::BeginBuyingUpgrades() +{ + KeyValues* kvcmd = new KeyValues("MvM_UpgradesBegin"); + engine->ClientCommandKeyValues(m_me->GetEdict(), kvcmd); + // kvcmd->deleteThis(); + kvcmd = nullptr; +} + +void CTF2BotUpgradeManager::BuySingleUpgrade(const int upgradeID, const int itemslot, const int quantity) +{ + KeyValues* kvcmd = new KeyValues("MvM_UpgradesBegin"); + KeyValues* kUpg = kvcmd->FindKey("Upgrade", true); + kUpg->SetInt("itemslot", itemslot); + kUpg->SetInt("Upgrade", upgradeID); + kUpg->SetInt("count", quantity); + engine->ClientCommandKeyValues(m_me->GetEdict(), kvcmd); + // kvcmd->deleteThis(); + kvcmd = nullptr; + m_numupgrades += quantity; +} + +void CTF2BotUpgradeManager::RefundUpgrades() +{ + KeyValues* kvcmd = new KeyValues("MVM_Respec"); + engine->ClientCommandKeyValues(m_me->GetEdict(), kvcmd); + // kvcmd->deleteThis(); + kvcmd = nullptr; + m_numupgrades = 0; +} + +void CTF2BotUpgradeManager::EndBuyingUpgrades() +{ + KeyValues* kvcmd = new KeyValues("MvM_UpgradesDone"); + kvcmd->SetInt("num_upgrades", m_numupgrades); + engine->ClientCommandKeyValues(m_me->GetEdict(), kvcmd); + // kvcmd->deleteThis(); + kvcmd = nullptr; + m_numupgrades = 0; +} + +void CTF2BotUpgradeManager::FetchUpgrades() +{ + CTeamFortress2Mod::GetTF2Mod()->GetMvMUpgradeManager().CollectUpgradesToBuy(m_tobuylist, m_nextpriority, m_me->GetMyClassType()); + FilterUpgrades(); +} + +void CTF2BotUpgradeManager::FilterUpgrades() +{ + // Collect weapon item definition indexes + std::vector myweaponindexes; + m_me->ForEveryWeapon([&myweaponindexes](const CBotWeapon& weapon) { + myweaponindexes.push_back(weapon.GetWeaponEconIndex()); + }); + + auto start = std::remove_if(m_tobuylist.begin(), m_tobuylist.end(), [&myweaponindexes](const TF2BotUpgradeInfo_t* upgradeinfo) { + if (upgradeinfo->AnyWeapon()) + { + return false; // no restrictions on weapons + } + + for (auto& index : myweaponindexes) + { + if (upgradeinfo->allowedweapons.find(index) != upgradeinfo->allowedweapons.end()) + { + return false; // bot has a weapon that can use this upgrade + } + } + + return true; // upgrade has weapon restriction and the bot doesn't have the needed weapon, remove this upgrade + }); + + m_tobuylist.erase(start, m_tobuylist.end()); // remove upgrades +} + +void CTF2BotUpgradeManager::RemoveFinishedUpgrades() +{ + auto start = std::remove_if(m_tobuylist.begin(), m_tobuylist.end(), [this](const TF2BotUpgradeInfo_t* upgradeinfo) { + for (auto& bought : m_boughtlist) + { + if (bought.IsMaxed() && bought.upgrade->index == upgradeinfo->upgrade->index) + { + return true; // upgrade maxed, remove it + } + } + + return false; + }); + + m_tobuylist.erase(start, m_tobuylist.end()); // remove upgrades +} + +TF2BotUpgrade_t* CTF2BotUpgradeManager::GetOrCreateUpgradeData(const MvMUpgrade_t* upgrade) +{ + for (auto& data : m_boughtlist) + { + if (data.upgrade->index == upgrade->index) + { + return &data; + } + } + + auto& newdata = m_boughtlist.emplace_back(upgrade); + return &newdata; +} + diff --git a/extension/bot/tf2/tf2bot_upgrades.h b/extension/bot/tf2/tf2bot_upgrades.h new file mode 100644 index 0000000..03ebde5 --- /dev/null +++ b/extension/bot/tf2/tf2bot_upgrades.h @@ -0,0 +1,105 @@ +#ifndef NAVBOT_TF2_MVM_UPGRADES_H_ +#define NAVBOT_TF2_MVM_UPGRADES_H_ +#pragma once + +#include +#include + +class CTF2Bot; + +class CTF2BotUpgradeManager +{ +public: + CTF2BotUpgradeManager(); + ~CTF2BotUpgradeManager(); + static constexpr int INITIAL_PRIORITY = 1; + + void SetMe(CTF2Bot* me) { m_me = me; } + + enum BuyUpgradeState : int + { + STATE_GOBUY = 0, // Go and buy something + STATE_BUYINGSTUFF, // Buying stuff + STATE_WAITFORNEXTWAVE, // Already bought stuff, wait until the bot gets more money from the next wave + STATE_DOREFUND, // Bot should refund and rebuy stuff + + MAX_STATES + }; + + // Call this to make the bot buy an upgrade + void Update(); + + void Reset() + { + m_numupgrades = 0; + m_nextpriority = 1; + m_tobuylist.clear(); + m_boughtlist.clear(); + m_state = STATE_GOBUY; + } + + void OnUpgradesRefunded() + { + m_tobuylist.clear(); + m_numupgrades = 0; + m_state = STATE_GOBUY; + InitUpgrades(); + } + + void OnDoneUpgrading() + { + m_state = STATE_WAITFORNEXTWAVE; + } + + // Called when a wave ends + void OnWaveEnd() + { + m_state = STATE_GOBUY; + } + + // Called when a wave is failed + void OnWaveFailed() + { + // It's hard to keep track of upgrades during wave failures, just refund everything and start again + m_state = STATE_DOREFUND; + } + + void InitUpgrades() + { + m_nextpriority = 1; + FetchUpgrades(); + } + + TF2BotUpgrade_t* GetOrCreateUpgradeData(const MvMUpgrade_t* upgrade); + void DoRefund() { m_state = STATE_DOREFUND; } + + const int GetCurrentPriority() const { return m_nextpriority; } + const bool IsUpgradingDone() const { return m_tobuylist.empty(); } + const bool IsAtInitialPriority() const { return m_nextpriority == INITIAL_PRIORITY; } + const bool ShouldGoToAnUpgradeStation() const { return m_state == STATE_GOBUY || m_state == STATE_DOREFUND; } + const bool IsDoneForCurrentWave() const { return m_state == STATE_WAITFORNEXTWAVE; } + const bool IsBuyingUpgrades() const { return m_state == STATE_GOBUY || m_state == STATE_BUYINGSTUFF; } + BuyUpgradeState GetState() const { return m_state; } + +private: + CTF2Bot* m_me; + int m_numupgrades; + int m_nextpriority; + std::vector m_tobuylist; + std::vector m_boughtlist; + BuyUpgradeState m_state; + + void BeginBuyingUpgrades(); + void BuySingleUpgrade(const int upgradeID, const int itemslot, const int quantity); + void RefundUpgrades(); + void EndBuyingUpgrades(); + void FetchUpgrades(); + void FilterUpgrades(); + void RemoveFinishedUpgrades(); + void AdvancePriority() { ++m_nextpriority; } + void ExecuteBuy(); + void ExecuteRefund(); + bool CanAffordAnyUpgrade() const; +}; + +#endif // !NAVBOT_TF2_MVM_UPGRADES_H_ diff --git a/extension/mods/tf2/mvm_upgrade.h b/extension/mods/tf2/mvm_upgrade.h index b881441..dfce446 100644 --- a/extension/mods/tf2/mvm_upgrade.h +++ b/extension/mods/tf2/mvm_upgrade.h @@ -58,6 +58,11 @@ struct MvMUpgrade_t max_purchases = count; } + const bool CanAfford(const int currency) const + { + return currency >= cost; + } + int index; std::string attribute; float increment; @@ -67,6 +72,7 @@ struct MvMUpgrade_t int max_purchases; }; + // Used by the bot to reference an upgrade bought struct TF2BotUpgrade_t { @@ -82,6 +88,7 @@ struct TF2BotUpgrade_t int times_bought; }; + // Upgrade Information struct TF2BotUpgradeInfo_t { @@ -89,9 +96,10 @@ struct TF2BotUpgradeInfo_t { attribute.reserve(64); quality = 0; // default - priority = 0; + priority = 1; itemslot = 0; allowedweapons.reserve(4); + upgrade = nullptr; } // if allowedweapons is empty, then any weapon is allowed @@ -104,11 +112,22 @@ struct TF2BotUpgradeInfo_t return allowedweapons.find(itemindex) != allowedweapons.end(); } + void SetUpgrade(const MvMUpgrade_t* upg) + { + this->upgrade = upg; + } + + int GetUpgradeIndex() const + { + return upgrade->index; + } + std::string attribute; int quality; int priority; int itemslot; std::unordered_set allowedweapons; + const MvMUpgrade_t* upgrade; }; #endif // !NAVBOT_TF2_MVM_UPGRADE_H_ diff --git a/extension/mods/tf2/mvm_upgrade_manager.cpp b/extension/mods/tf2/mvm_upgrade_manager.cpp index ac60284..7bf4851 100644 --- a/extension/mods/tf2/mvm_upgrade_manager.cpp +++ b/extension/mods/tf2/mvm_upgrade_manager.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include +#include #include "mvm_upgrade_manager.h" SourceMod::SMCResult CMvMUpgradeManager::ReadSMC_NewSection(const SourceMod::SMCStates* states, const char* name) @@ -132,7 +134,9 @@ void CMvMUpgradeManager::ParseUpgradeFile() } void CMvMUpgradeManager::ParseBotUpgradeInfoFile() -{ +{ + InitUpgradeInfo(); + char path[PLATFORM_MAX_PATH]; smutils->BuildPath(SourceMod::Path_SM, path, sizeof(path), "configs/navbot/tf/mvm_upgrades.cfg"); SourceMod::SMCStates states; @@ -143,6 +147,8 @@ void CMvMUpgradeManager::ParseBotUpgradeInfoFile() smutils->LogError(myself, "Failed to parse configuration file \"%s\"!", path); return; } + + PostLoadBotUpgradeInfo(); } void CMvMUpgradeManager::ConCommand_ListUpgrades() const @@ -154,7 +160,25 @@ void CMvMUpgradeManager::ConCommand_ListUpgrades() const } } -const MvMUpgrade_t* CMvMUpgradeManager::FindUpgrade(std::string& attributename, int quality) const +void CMvMUpgradeManager::Reload() +{ + // Clean up pointers while they are still valid + extmanager->ForEachBot([](CBaseBot* bot) { + CTF2Bot* tf2bot = static_cast(bot); + tf2bot->GetUpgradeManager().Reset(); + }); + + Reset(); + ParseUpgradeFile(); + ParseBotUpgradeInfoFile(); + + extmanager->ForEachBot([](CBaseBot* bot) { + CTF2Bot* tf2bot = static_cast(bot); + tf2bot->GetUpgradeManager().InitUpgrades(); + }); +} + +const MvMUpgrade_t* CMvMUpgradeManager::FindUpgrade(const std::string& attributename, int quality) const { for (auto& upgrade : m_upgrades) { @@ -167,6 +191,27 @@ const MvMUpgrade_t* CMvMUpgradeManager::FindUpgrade(std::string& attributename, return nullptr; } +void CMvMUpgradeManager::CollectUpgradesToBuy(std::vector& upgradeVec, int priority, TeamFortress2::TFClassType classtype) const +{ + auto it = m_upgradeinfos.find(static_cast(classtype)); + + if (it == m_upgradeinfos.end()) + { + smutils->LogError(myself, "CMvMUpgradeManager::CollectUpgradesToBuy no upgrades available for class %s!", tf2lib::GetClassNameFromType(classtype)); + return; + } + + auto& storage = it->second; + + for (auto& upgrade : storage.m_upgradeinfos) + { + if (upgrade.priority == priority) + { + upgradeVec.push_back(&upgrade); + } + } +} + void CMvMUpgradeManager::ParseUpgradeBlock(KeyValues* kv) { if (!kv) @@ -210,3 +255,38 @@ void CMvMUpgradeManager::ParseUpgradeBlock(KeyValues* kv) m_lastindex++; } } + +void CMvMUpgradeManager::PostLoadBotUpgradeInfo() +{ + for (auto& storage : m_upgradeinfos) + { + for (auto& info : storage.second.m_upgradeinfos) + { + auto upgrade = FindUpgrade(info.attribute, info.quality); + + if (upgrade == nullptr) + { + smutils->LogError(myself, "Failed to assign an upgrade to \"%s - %i\"", info.attribute.c_str(), info.quality); + continue; + } + + info.SetUpgrade(upgrade); + } + } + + // Remove upgrade info without an assigned upgrade + + for (auto& storage : m_upgradeinfos) + { + auto start = std::remove_if(storage.second.m_upgradeinfos.begin(), storage.second.m_upgradeinfos.end(), [](const TF2BotUpgradeInfo_t& object) { + if (object.upgrade == nullptr) + { + return true; + } + + return false; + }); + + storage.second.m_upgradeinfos.erase(start, storage.second.m_upgradeinfos.end()); + } +} diff --git a/extension/mods/tf2/mvm_upgrade_manager.h b/extension/mods/tf2/mvm_upgrade_manager.h index 07752b5..3a17219 100644 --- a/extension/mods/tf2/mvm_upgrade_manager.h +++ b/extension/mods/tf2/mvm_upgrade_manager.h @@ -52,12 +52,7 @@ class CMvMUpgradeManager : public SourceMod::ITextListener_SMC void ParseBotUpgradeInfoFile(); void ConCommand_ListUpgrades() const; - void Reload() - { - Reset(); - InitUpgradeInfo(); - ParseUpgradeFile(); - } + void Reload(); /** * @brief Searches for an MvM Upgrade @@ -65,7 +60,8 @@ class CMvMUpgradeManager : public SourceMod::ITextListener_SMC * @param quality Upgrade quality to search for * @return Poiner to upgrade definition or NULL if none found */ - const MvMUpgrade_t* FindUpgrade(std::string& attributename, int quality = MVM_DEFAULT_QUALITY) const; + const MvMUpgrade_t* FindUpgrade(const std::string& attributename, int quality = MVM_DEFAULT_QUALITY) const; + void CollectUpgradesToBuy(std::vector& upgradeVec, int priority, TeamFortress2::TFClassType classtype) const; private: std::vector m_upgrades; @@ -89,6 +85,8 @@ class CMvMUpgradeManager : public SourceMod::ITextListener_SMC m_upgradeinfos.emplace(static_cast(TeamFortress2::TFClass_Sniper), CMvMBotUpgradeInfoStorage{}); m_upgradeinfos.emplace(static_cast(TeamFortress2::TFClass_Spy), CMvMBotUpgradeInfoStorage{}); } + + void PostLoadBotUpgradeInfo(); }; #endif // !NAVBOT_MVM_UPGRADE_PARSER_H_