From 8ccb8f49a3cb6855ff4782ad21bbf68e1bab8d5e Mon Sep 17 00:00:00 2001 From: trickerer Date: Thu, 5 Dec 2024 08:00:21 +0700 Subject: [PATCH] NPCBots: Implement bot rent cost. New config param: `NpcBot.Cost.Rent`. Config param renamed: `NpcBot.Cost` -> `NpcBot.Cost.Hire`. (cherry picked from commit 66fce97251a1c1ab95f2ba3d6a98a69696ec4c1c) # Conflicts: # src/server/worldserver/worldserver.conf.dist --- .../apps/worldserver/worldserver.conf.dist | 19 +++-- src/server/game/AI/NpcBots/bot_ai.cpp | 39 ++++++++-- src/server/game/AI/NpcBots/bot_ai.h | 1 + src/server/game/AI/NpcBots/botcommands.cpp | 2 +- src/server/game/AI/NpcBots/botcommon.h | 2 + src/server/game/AI/NpcBots/botgiver.cpp | 2 +- src/server/game/AI/NpcBots/botmgr.cpp | 71 +++++++++++++++---- src/server/game/AI/NpcBots/botmgr.h | 6 +- 8 files changed, 114 insertions(+), 28 deletions(-) diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 5ec0da05fc92b0..d825d68a3055bf 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -4704,12 +4704,19 @@ NpcBot.Limit.Raid = 1 NpcBot.HideSpawns = 1 # -# NpcBot.Cost -# Description: Bot recruitment cost (in copper). -# Note: This value is for level 80 characters, for lower levels cost is reduced. -# Default: 1000000 (100 gold) - -NpcBot.Cost = 1000000 +# NpcBot.Cost.Hire +# NpcBot.Cost.Rent +# Description: Bot recruitment cost (in copper). Hire cost is the amount player has to pay +# to recruit a bot. Rent cost is the amount player has to pay per 1 hour +# to keep a bot with them, bot is automatically removed if player doesn't have +# enough money to pay the rent cost. +# Note1: Hire cost is for level 80 characters, for lower levels cost is reduced greatly +# Note2: Rent is collected every 10 minutes so at least 6 copper per hour is taken +# Default: 1000000 - (NpcBot.Cost.Hire, 100 gold) +# 0 - (NpcBot.Cost.Rent) + +NpcBot.Cost.Hire = 1000000 +NpcBot.Cost.Rent = 0 # # NpcBot.UpdateDelay.Base diff --git a/src/server/game/AI/NpcBots/bot_ai.cpp b/src/server/game/AI/NpcBots/bot_ai.cpp index 76d2b9551d9469..921fa469cce35d 100644 --- a/src/server/game/AI/NpcBots/bot_ai.cpp +++ b/src/server/game/AI/NpcBots/bot_ai.cpp @@ -158,8 +158,6 @@ static void ApplyBotPercentModFloatVar(float &var, float val, bool apply) static uint16 __rand; //calculated for each bot separately once every updateAI tick -static std::set BotCustomSpells; - bot_ai::bot_ai(Creature* creature) : CreatureAI(creature), _botData(const_cast(BotDataMgr::SelectNpcBotData(creature->GetEntry()))), _botExtras(const_cast(BotDataMgr::SelectNpcBotExtras(creature->GetEntry()))) @@ -270,6 +268,8 @@ bot_ai::bot_ai(Creature* creature) : CreatureAI(creature), _wmoAreaUpdateTimer = 0; + _rentTimer = 0; + _contestedPvPTimer = 0; _groupUpdateTimer = BOT_GROUP_UPDATE_TIMER; @@ -341,8 +341,7 @@ const std::string& bot_ai::LocalizedNpcText(Player const* forPlayer, uint32 text if (!unk_botstrings.contains(textId)) { - BOT_LOG_ERROR("entities.player", "NPCBots: bot text string #{} is not localized, at least for {}", - textId, localeNames[loc]); + BOT_LOG_ERROR("entities.player", "NPCBots: bot text string #{} is not localized, at least for {}", textId, localeNames[loc]); std::ostringstream msg; msg << (loc == DEFAULT_LOCALE ? ""; @@ -596,7 +595,10 @@ void bot_ai::ResetBotAI(uint8 resetType) if (resetType & BOTAI_RESET_MASK_RESET_MASTER) master = reinterpret_cast(me); if (resetType & BOTAI_RESET_MASK_ABANDON_MASTER) + { _ownerGuid = 0; + _rentTimer = 0; + } if (resetType == BOTAI_RESET_INIT || resetType == BOTAI_RESET_LOGOUT) { _checkOwershipTimer = (BotMgr::GetOwnershipExpireTime() && _botData->owner) ? (resetType == BOTAI_RESET_INIT) ? 1000 : CalculateOwnershipCheckTime() : 0; @@ -7931,7 +7933,7 @@ bool bot_ai::OnGossipHello(Player* player, uint32 /*option*/) { if (IAmFree() && !IsWanderer()) { - uint32 cost = BotMgr::GetNpcBotCost(player->GetLevel(), _botclass); + uint32 cost = BotMgr::GetNpcBotCostHire(player->GetLevel(), _botclass); int8 reason = 0; if (me->HasAura(BERSERK)) @@ -7973,6 +7975,8 @@ bool bot_ai::OnGossipHello(Player* player, uint32 /*option*/) message2 << LocalizedNpcText(player, BOT_TEXT_HIREOPTION_DEFAULT); } + message1 << "\n(" << BotMgr::GetNpcBotCostStr(player->GetLevel(), _botclass) << ")"; + if (!reason) player->PlayerTalkClass->GetGossipMenu().AddMenuItem(-1, GOSSIP_ICON_TAXI, message2.str(), GOSSIP_SENDER_HIRE, GOSSIP_ACTION_INFO_DEF + 0, message1.str(), cost, false); else @@ -17961,6 +17965,26 @@ bool bot_ai::GlobalUpdate(uint32 diff) { _updateTimerEx2 = urand(2000, 4000); + //Rent Collecting + if (_rentTimer >= RENT_COLLECT_TIMER && BotMgr::GetNpcBotCostRent() && !HasBotCommandState(BOT_COMMAND_UNBIND) && !IAmFree()) + { + uint32 rent_money = 0; + while (_rentTimer >= RENT_COLLECT_TIMER) + { + rent_money += uint32(uint64(BotMgr::GetNpcBotCostRent()) * (RENT_COLLECT_TIMER / 1000) / (RENT_TIMER / 1000)); + _rentTimer -= RENT_COLLECT_TIMER; + } + + rent_money = std::max(rent_money, 1); + if (!master->HasEnoughMoney(rent_money)) + { + ChatHandler(master->GetSession()).SendNotification(LocalizedNpcText(master, BOT_TEXT_HIREFAIL_COST).c_str()); + master->GetBotMgr()->RemoveBot(me->GetGUID(), BOT_REMOVE_UNAFFORD); + return false; + } + master->ModifyMoney(-int32(rent_money)); + } + if (BotMgr::HideBotSpawns() && IAmFree() && !IsWanderer()) { // !!bot may be out of world!! @@ -18721,6 +18745,11 @@ void bot_ai::CommonTimers(uint32 diff) if (IAmFree()) UpdateReviveTimer(diff); + else + { + if (BotMgr::GetNpcBotCostRent() && me->IsInWorld() && !HasBotCommandState(BOT_COMMAND_UNBIND)) + _rentTimer += diff; + } if (me->IsInWorld()) { diff --git a/src/server/game/AI/NpcBots/bot_ai.h b/src/server/game/AI/NpcBots/bot_ai.h index 4c5171122c7993..eaed68230b2d20 100644 --- a/src/server/game/AI/NpcBots/bot_ai.h +++ b/src/server/game/AI/NpcBots/bot_ai.h @@ -714,6 +714,7 @@ class bot_ai : public CreatureAI uint32 lastdiff, checkAurasTimer, checkMasterTimer, roleTimer, ordersTimer, regenTimer, _updateTimerMedium, _updateTimerEx1, _updateTimerEx2; uint32 _checkOwershipTimer; uint32 _moveBehindTimer; + uint32 _rentTimer; uint32 _wmoAreaUpdateTimer; uint32 waitTimer; uint32 itemsAutouseTimer; diff --git a/src/server/game/AI/NpcBots/botcommands.cpp b/src/server/game/AI/NpcBots/botcommands.cpp index a9760d5b9f2e79..5d83656e2e0fe4 100644 --- a/src/server/game/AI/NpcBots/botcommands.cpp +++ b/src/server/game/AI/NpcBots/botcommands.cpp @@ -3103,7 +3103,7 @@ class script_bot_commands : public CommandScript } if (owner->GetBotMgr()->GetBotsHidden()) { - handler->GetSession()->SendNotification("You can't do that while bots are hidden"); + handler->SendNotification("You can't do that while bots are hidden"); handler->SetSentErrorMessage(true); return false; } diff --git a/src/server/game/AI/NpcBots/botcommon.h b/src/server/game/AI/NpcBots/botcommon.h index d7875b765c166a..a18199254c7e0d 100644 --- a/src/server/game/AI/NpcBots/botcommon.h +++ b/src/server/game/AI/NpcBots/botcommon.h @@ -53,6 +53,8 @@ enum BotCommonValues REVIVE_TIMER_SHORT = 60000, //1 Minute INOUTDOORS_ENSURE_TIMER = 1500, BOT_GROUP_UPDATE_TIMER = 2000, + RENT_TIMER = 3600000, //1 Hour + RENT_COLLECT_TIMER = 600000, //10 Minutes //VEHICLE CREATURES CREATURE_NEXUS_SKYTALON_1 = 32535, // [Q] Aces High CREATURE_EOE_SKYTALON_N = 30161, // Eye of Eternity diff --git a/src/server/game/AI/NpcBots/botgiver.cpp b/src/server/game/AI/NpcBots/botgiver.cpp index 0b502ea6e7884b..78a235010f8c96 100644 --- a/src/server/game/AI/NpcBots/botgiver.cpp +++ b/src/server/game/AI/NpcBots/botgiver.cpp @@ -179,7 +179,7 @@ class script_bot_giver : public CreatureScript uint8 botclass = action - GOSSIP_ACTION_INFO_DEF; - uint32 cost = BotMgr::GetNpcBotCost(player->GetLevel(), botclass); + uint32 cost = BotMgr::GetNpcBotCostHire(player->GetLevel(), botclass); if (!player->HasEnoughMoney(cost)) { WhisperTo(player, me, bot_ai::LocalizedNpcText(player, BOT_TEXT_HIREFAIL_COST).c_str()); diff --git a/src/server/game/AI/NpcBots/botmgr.cpp b/src/server/game/AI/NpcBots/botmgr.cpp index 69d17ba2efeb90..4c22154007605a 100644 --- a/src/server/game/AI/NpcBots/botmgr.cpp +++ b/src/server/game/AI/NpcBots/botmgr.cpp @@ -68,7 +68,8 @@ uint8 _npcBotOwnerExpireMode; int32 _botInfoPacketsLimit; uint32 _gearBankCapacity; uint32 _gearBankEquipmentSetsCount; -uint32 _npcBotsCost; +uint32 _npcBotsCostHire; +uint32 _npcBotsCostRent; uint32 _npcBotUpdateDelayBase; uint32 _npcBotEngageDelayDPS_default; uint32 _npcBotEngageDelayHeal_default; @@ -282,6 +283,7 @@ void AddNpcBotScripts() BotMgr::BotMgr(Player* const master) : _owner(master), _dpstracker(new DPSTracker()) { _quickrecall = false; + _update_lock = false; _data = nullptr; } BotMgr::~BotMgr() @@ -385,7 +387,8 @@ void BotMgr::LoadConfig(bool reload) _limitNpcBotsRaids = sConfigMgr->GetBoolDefault("NpcBot.Limit.Raid", true); _hideSpawns = sConfigMgr->GetBoolDefault("NpcBot.HideSpawns", false); _botInfoPacketsLimit = sConfigMgr->GetIntDefault("NpcBot.InfoPacketsLimit", -1); - _npcBotsCost = sConfigMgr->GetIntDefault("NpcBot.Cost", 1000000); + _npcBotsCostHire = sConfigMgr->GetIntDefault("NpcBot.Cost.Hire", 1000000); + _npcBotsCostRent = sConfigMgr->GetIntDefault("NpcBot.Cost.Rent", 0); _npcBotUpdateDelayBase = sConfigMgr->GetIntDefault("NpcBot.UpdateDelay.Base", 0); _npcBotEngageDelayDPS_default = sConfigMgr->GetIntDefault("NpcBot.EngageDelay.DPS", 0); _npcBotEngageDelayHeal_default = sConfigMgr->GetIntDefault("NpcBot.EngageDelay.Heal", 0); @@ -1191,6 +1194,12 @@ bool BotMgr::IsWanderingWorldBot(Creature const* bot) void BotMgr::Update(uint32 diff) { + while (!_delayedRemoveList.empty()) + { + decltype(_delayedRemoveList)::iterator itr = _delayedRemoveList.begin(); + RemoveBot(itr->first, itr->second); + } + //remove temp bots from bot map before updating it while (!_removeList.empty()) { @@ -1218,6 +1227,8 @@ void BotMgr::Update(uint32 diff) if (partyCombat) bot_ai::CalculateAoeSpots(_owner, _aoespots); + _update_lock = true; + for (BotMap::const_iterator itr = _bots.begin(); itr != _bots.end(); ++itr) { //guid = itr->first; @@ -1265,6 +1276,8 @@ void BotMgr::Update(uint32 diff) ai->canUpdate = false; } + _update_lock = false; + if (_quickrecall) { _quickrecall = false; @@ -1806,6 +1819,14 @@ void BotMgr::RemoveAllBots(uint8 removetype) //Bot is being abandoned by player void BotMgr::RemoveBot(ObjectGuid guid, uint8 removetype) { + if (_update_lock) + { + _delayedRemoveList.emplace_back(guid, BotRemoveType(removetype)); + return; + } + else if (!_delayedRemoveList.empty()) + _delayedRemoveList.remove_if([=](decltype(_delayedRemoveList)::value_type const& p) { return p.first == guid; }); + BotMap::const_iterator itr = _bots.find(guid); ASSERT(itr != _bots.end(), "Trying to remove bot which does not belong to this botmgr(a)!!"); //ASSERT(_owner->IsInWorld(), "Trying to remove bot while not in world(a)!!"); @@ -1844,16 +1865,16 @@ void BotMgr::RemoveBot(ObjectGuid guid, uint8 removetype) BotAIResetType resetType; switch (removetype) { - case BOT_REMOVE_DISMISS: resetType = BOTAI_RESET_DISMISS; break; - case BOT_REMOVE_UNBIND: resetType = BOTAI_RESET_UNBIND; break; - default: resetType = BOTAI_RESET_LOGOUT; break; + case BOT_REMOVE_DISMISS: case BOT_REMOVE_UNAFFORD: resetType = BOTAI_RESET_DISMISS; break; + case BOT_REMOVE_UNBIND: resetType = BOTAI_RESET_UNBIND; break; + default: resetType = BOTAI_RESET_LOGOUT; break; } bot->GetBotAI()->ResetBotAI(resetType); bot->SetFaction(bot->GetCreatureTemplate()->faction); bot->SetLevel(bot->GetCreatureTemplate()->minlevel); - if (removetype == BOT_REMOVE_DISMISS) + if (resetType == BOTAI_RESET_DISMISS) { BotDataMgr::ResetNpcBotTransmogData(bot->GetEntry(), false); uint32 newOwner = 0; @@ -1926,7 +1947,7 @@ BotAddResult BotMgr::AddBot(Creature* bot) //} if (!owned) { - uint32 cost = GetNpcBotCost(_owner->GetLevel(), bot->GetBotClass()); + uint32 cost = GetNpcBotCostHire(_owner->GetLevel(), bot->GetBotClass()); if (!_owner->HasEnoughMoney(cost)) { ChatHandler ch(_owner->GetSession()); @@ -2068,7 +2089,12 @@ bool BotMgr::RemoveAllBotsFromGroup() return true; } -uint32 BotMgr::GetNpcBotCost(uint8 level, uint8 botclass) +uint32 BotMgr::GetNpcBotCostRent() +{ + return _npcBotsCostRent; +} + +uint32 BotMgr::GetNpcBotCostHire(uint8 level, uint8 botclass) { //assuming default 1000000 //level 1: 500 //5 silver @@ -2079,11 +2105,11 @@ uint32 BotMgr::GetNpcBotCost(uint8 level, uint8 botclass) //rest is linear //rare / rareelite bots have their cost adjusted uint32 cost = - level < 10 ? _npcBotsCost / 2000 : //5 silver - level < 20 ? _npcBotsCost / 100 : //1 gold - level < 30 ? _npcBotsCost / 20 : //5 gold - level < 40 ? _npcBotsCost / 5 : //20 gold - (_npcBotsCost * (level - (level % 10))) / DEFAULT_MAX_LEVEL; //50 - 100 gold + level < 10 ? _npcBotsCostHire / 2000 : //5 silver + level < 20 ? _npcBotsCostHire / 100 : //1 gold + level < 30 ? _npcBotsCostHire / 20 : //5 gold + level < 40 ? _npcBotsCostHire / 5 : //20 gold + (_npcBotsCostHire * (level - (level % 10))) / DEFAULT_MAX_LEVEL; //50 - 100 gold switch (botclass) { @@ -2111,7 +2137,7 @@ std::string BotMgr::GetNpcBotCostStr(uint8 level, uint8 botclass) { std::ostringstream money; - if (uint32 cost = GetNpcBotCost(level, botclass)) + if (uint32 cost = GetNpcBotCostHire(level, botclass)) { uint32 gold = uint32(cost / GOLD); cost -= (gold * GOLD); @@ -2126,6 +2152,23 @@ std::string BotMgr::GetNpcBotCostStr(uint8 level, uint8 botclass) money << cost << " |TInterface\\Icons\\INV_Misc_Coin_05:8|t"; } + if (uint32 rcost = GetNpcBotCostRent()) + { + uint32 gold = uint32(rcost / GOLD); + rcost -= (gold * GOLD); + uint32 silver = uint32(rcost / SILVER); + rcost -= (silver * SILVER); + + money << " + |TInterface\\Icons\\INV_Misc_PocketWatch_01:16|t"; + + if (gold != 0) + money << gold << " |TInterface\\Icons\\INV_Misc_Coin_01:8|t"; + if (silver != 0) + money << silver << " |TInterface\\Icons\\INV_Misc_Coin_03:8|t"; + if (rcost) + money << rcost << " |TInterface\\Icons\\INV_Misc_Coin_05:8|t"; + } + return money.str(); } diff --git a/src/server/game/AI/NpcBots/botmgr.h b/src/server/game/AI/NpcBots/botmgr.h index d2f8e4c70bf21a..1cd28d087d1fd1 100644 --- a/src/server/game/AI/NpcBots/botmgr.h +++ b/src/server/game/AI/NpcBots/botmgr.h @@ -68,6 +68,7 @@ enum BotRemoveType BOT_REMOVE_DISMISS = 1, BOT_REMOVE_UNSUMMON = 2, BOT_REMOVE_UNBIND = 3, + BOT_REMOVE_UNAFFORD = 4, BOT_REMOVE_BY_DEFAULT = BOT_REMOVE_LOGOUT }; @@ -262,7 +263,8 @@ class AC_GAME_API BotMgr bool HasBotPetType(uint32 petType) const; bool IsBeingResurrected(WorldObject const* corpse) const; - static uint32 GetNpcBotCost(uint8 level, uint8 botclass); + static uint32 GetNpcBotCostRent(); + static uint32 GetNpcBotCostHire(uint8 level, uint8 botclass); static std::string GetNpcBotCostStr(uint8 level, uint8 botclass); static uint8 BotClassByClassName(std::string const& className); static uint8 GetBotPlayerClass(uint8 bot_class); @@ -369,10 +371,12 @@ class AC_GAME_API BotMgr Player* const _owner; BotMap _bots; std::list _removeList; + std::list> _delayedRemoveList; DPSTracker* const _dpstracker; NpcBotMgrData* _data; bool _quickrecall; + bool _update_lock; AoeSpotsVec _aoespots;