From 99052aec8bf62950eb30c1d8c7a580d68e0c5b15 Mon Sep 17 00:00:00 2001 From: Aeadoin <109764533+Aeadoin@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:09:08 -0500 Subject: [PATCH] [Bot] Add EVENT_TRADE Support to Bots. (#2560) * [Bot] Add EVENT_TRADE Support to Bots. * Fixed issue with duplicate items after Event Trade * Update logic * Add CalcBotStats call after Bot Trade Event * Fix Lua EVENT_TRADE. * Formatting. * More formatting. Co-authored-by: Kinglykrab --- zone/bot.cpp | 123 +++++++++++++++++++++++-------- zone/embparser.cpp | 89 +++++++++++----------- zone/lua_parser.cpp | 1 + zone/lua_parser_events.cpp | 114 ++++++++++++++++++++++------ zone/lua_parser_events.h | 10 +++ zone/quest_parser_collection.cpp | 4 + 6 files changed, 242 insertions(+), 99 deletions(-) diff --git a/zone/bot.cpp b/zone/bot.cpp index 279c55de0..229eafd4a 100644 --- a/zone/bot.cpp +++ b/zone/bot.cpp @@ -4571,11 +4571,11 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* using namespace EQ; struct ClientTrade { - const ItemInstance* trade_item_instance; + ItemInstance* trade_item_instance; int16 from_client_slot; int16 to_bot_slot; - ClientTrade(const ItemInstance* item, int16 from) : trade_item_instance(item), from_client_slot(from), to_bot_slot(invslot::SLOT_INVALID) { } + ClientTrade(ItemInstance* item, int16 from) : trade_item_instance(item), from_client_slot(from), to_bot_slot(invslot::SLOT_INVALID) { } }; struct ClientReturn { @@ -4639,11 +4639,19 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* } std::list client_trade; + std::list event_trade; std::list client_return; + bool trade_event_exists = false; + if (parse->BotHasQuestSub(EVENT_TRADE)) { + // There is a EVENT_TRADE, we will let the Event handle returning of items. + trade_event_exists = true; + } + // pre-checks for incoming illegal transfers + EQ::InventoryProfile& user_inv = client->GetInv(); for (int16 trade_index = begin_slot_id; trade_index <= end_slot_id; ++trade_index) { - auto trade_instance = client->GetInv()[trade_index]; + auto trade_instance = user_inv.GetItem(trade_index); if (!trade_instance) { continue; } @@ -4675,37 +4683,61 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* } if (trade_instance->IsStackable() && trade_instance->GetCharges() < trade_instance->GetItem()->StackSize) { // temp until partial stacks are implemented - client->Message( - Chat::Yellow, - fmt::format( - "{} is only a partially stacked item, the trade has been cancelled!", - item_link - ).c_str() - ); - client->ResetTrade(); - return; + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_instance, trade_index)); + continue; + } + else { + client->Message( + Chat::Yellow, + fmt::format( + "{} is only a partially stacked item, the trade has been cancelled!", + item_link + ).c_str() + ); + client->ResetTrade(); + return; + } } if (CheckLoreConflict(trade_instance->GetItem())) { - client->Message( - Chat::Yellow, - fmt::format( - "This bot already has {}, the trade has been cancelled!", - item_link - ).c_str() - ); - client->ResetTrade(); - return; + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_instance, trade_index)); + continue; + } + else { + client->Message( + Chat::Yellow, + fmt::format( + "This bot already has {}, the trade has been cancelled!", + item_link + ).c_str() + ); + client->ResetTrade(); + return; + } } if (!trade_instance->IsType(item::ItemClassCommon)) { - client_return.push_back(ClientReturn(trade_instance, trade_index)); - continue; + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_instance, trade_index)); + continue; + } + else { + client->ResetTrade(); + return; + } } if (!trade_instance->IsEquipable(GetBaseRace(), GetClass()) || (GetLevel() < trade_instance->GetItem()->ReqLevel)) { // deity checks will be handled within IsEquipable() - client_return.push_back(ClientReturn(trade_instance, trade_index)); - continue; + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_instance, trade_index)); + continue; + } + else { + client->ResetTrade(); + return; + } } client_trade.push_back(ClientTrade(trade_instance, trade_index)); @@ -4713,7 +4745,9 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* // check for incoming lore hacks for (auto& trade_iterator : client_trade) { - if (!trade_iterator.trade_item_instance->GetItem()->LoreFlag) { + auto trade_instance = trade_iterator.trade_item_instance; + auto trade_index = trade_iterator.from_client_slot; + if (!trade_instance->GetItem()->LoreFlag) { continue; } @@ -4726,14 +4760,14 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* continue; } - if (trade_iterator.trade_item_instance->GetItem()->LoreGroup == -1 && check_iterator.trade_item_instance->GetItem()->ID == trade_iterator.trade_item_instance->GetItem()->ID) { + if (trade_instance->GetItem()->LoreGroup == -1 && check_iterator.trade_item_instance->GetItem()->ID == trade_instance->GetItem()->ID) { LogError("Bot::PerformTradeWithClient trade hack detected by {} with {}.", client->GetCleanName(), GetCleanName()); client->Message(Chat::White, "Trade hack detected, the trade has been cancelled."); client->ResetTrade(); return; } - if ((trade_iterator.trade_item_instance->GetItem()->LoreGroup > 0) && (check_iterator.trade_item_instance->GetItem()->LoreGroup == trade_iterator.trade_item_instance->GetItem()->LoreGroup)) { + if ((trade_instance->GetItem()->LoreGroup > 0) && (check_iterator.trade_item_instance->GetItem()->LoreGroup == trade_instance->GetItem()->LoreGroup)) { LogError("Bot::PerformTradeWithClient trade hack detected by {} with {}.", client->GetCleanName(), GetCleanName()); client->Message(Chat::White, "Trade hack detected, the trade has been cancelled."); client->ResetTrade(); @@ -4833,12 +4867,19 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* } } - // move unassignable items from trade list to return list + // move unassignable items from trade list to event list for (std::list::iterator trade_iterator = client_trade.begin(); trade_iterator != client_trade.end();) { if (trade_iterator->to_bot_slot == invslot::SLOT_INVALID) { - client_return.push_back(ClientReturn(trade_iterator->trade_item_instance, trade_iterator->from_client_slot)); - trade_iterator = client_trade.erase(trade_iterator); - continue; + if (trade_event_exists) { + event_trade.push_back(ClientTrade(trade_iterator->trade_item_instance, trade_iterator->from_client_slot)); + trade_iterator = client_trade.erase(trade_iterator); + continue; + } + else { + client_return.push_back(ClientReturn(trade_iterator->trade_item_instance, trade_iterator->from_client_slot)); + trade_iterator = client_trade.erase(trade_iterator); + continue; + } } ++trade_iterator; } @@ -5035,6 +5076,24 @@ void Bot::PerformTradeWithClient(int16 begin_slot_id, int16 end_slot_id, Client* if (accepted_count) { CalcBotStats(client->GetBotOption(Client::booStatsUpdate)); } + + if (event_trade.size()) { + // Get Traded Items + EQ::ItemInstance* insts[8] = { 0 }; + EQ::InventoryProfile& user_inv = client->GetInv(); + for (int i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_END; ++i) { + insts[i - EQ::invslot::TRADE_BEGIN] = user_inv.GetItem(i); + client->DeleteItemInInventory(i); + } + + // copy to be filtered by task updates, null trade slots preserved for quest event arg + std::vector items(insts, insts + std::size(insts)); + + // Check if EVENT_TRADE accepts any items + std::vector item_list(items.begin(), items.end()); + parse->EventBot(EVENT_TRADE, this, client, "", 0, &item_list); + CalcBotStats(false); + } } bool Bot::Death(Mob *killerMob, int64 damage, uint16 spell_id, EQ::skills::SkillType attack_skill) { diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 6927f86b8..bc997913e 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -1116,12 +1116,12 @@ void PerlembParser::GetQuestTypes( if ( event == EVENT_SPELL_EFFECT_CLIENT || event == EVENT_SPELL_EFFECT_NPC || -#ifdef BOTS +#ifdef BOTS event == EVENT_SPELL_EFFECT_BOT || #endif event == EVENT_SPELL_EFFECT_BUFF_TIC_CLIENT || event == EVENT_SPELL_EFFECT_BUFF_TIC_NPC || -#ifdef BOTS +#ifdef BOTS event == EVENT_SPELL_EFFECT_BUFF_TIC_BOT || #endif event == EVENT_SPELL_FADE || @@ -1241,7 +1241,7 @@ void PerlembParser::ExportQGlobals( if ( !isPlayerQuest && !isGlobalPlayerQuest && - !isBotQuest && + !isBotQuest && !isGlobalBotQuest && !isItemQuest && !isSpellQuest @@ -1427,7 +1427,7 @@ void PerlembParser::ExportMobVariables( if ( !isPlayerQuest && !isGlobalPlayerQuest && - !isBotQuest && + !isBotQuest && !isGlobalBotQuest && !isItemQuest && !isSpellQuest @@ -1525,56 +1525,55 @@ void PerlembParser::ExportEventVariables( case EVENT_TRADE: { if (extra_pointers) { - size_t sz = extra_pointers->size(); + size_t sz = extra_pointers->size(); for (size_t i = 0; i < sz; ++i) { - EQ::ItemInstance *inst = std::any_cast(extra_pointers->at(i)); + auto* inst = std::any_cast(extra_pointers->at(i)); + const uint32 item_id = inst ? inst->GetItem()->ID : 0; + const int16 item_charges = inst ? inst->GetCharges() : 0; + const auto is_attuned = inst ? inst->IsAttuned() : false; - std::string var_name = "item"; - var_name += std::to_string(i + 1); + auto var_name = fmt::format("item{}", i + 1); + ExportVar(package_name.c_str(), var_name.c_str(), item_id); + auto temp_var_name = fmt::format("{}_charges", var_name); + ExportVar(package_name.c_str(), temp_var_name.c_str(), item_charges); + + temp_var_name = fmt::format("{}_attuned", var_name); + ExportVar(package_name.c_str(), temp_var_name.c_str(), is_attuned); + + temp_var_name = fmt::format("{}_inst", var_name); if (inst) { - ExportVar(package_name.c_str(), var_name.c_str(), inst->GetItem()->ID); - - std::string temp_var_name = var_name; - temp_var_name += "_charges"; - ExportVar(package_name.c_str(), temp_var_name.c_str(), inst->GetCharges()); - - temp_var_name = var_name; - temp_var_name += "_attuned"; - ExportVar(package_name.c_str(), temp_var_name.c_str(), inst->IsAttuned()); - - temp_var_name = var_name; - temp_var_name += "_inst"; ExportVar(package_name.c_str(), temp_var_name.c_str(), "QuestItem", inst); - } - else { - ExportVar(package_name.c_str(), var_name.c_str(), 0); - - std::string temp_var_name = var_name; - temp_var_name += "_charges"; - ExportVar(package_name.c_str(), temp_var_name.c_str(), 0); - - temp_var_name = var_name; - temp_var_name += "_attuned"; - ExportVar(package_name.c_str(), temp_var_name.c_str(), 0); - - temp_var_name = var_name; - temp_var_name += "_inst"; + } else { ExportVar(package_name.c_str(), temp_var_name.c_str(), 0); } } } - ExportVar(package_name.c_str(), "copper", GetVar("copper." + std::string(itoa(objid))).c_str()); - ExportVar(package_name.c_str(), "silver", GetVar("silver." + std::string(itoa(objid))).c_str()); - ExportVar(package_name.c_str(), "gold", GetVar("gold." + std::string(itoa(objid))).c_str()); - ExportVar(package_name.c_str(), "platinum", GetVar("platinum." + std::string(itoa(objid))).c_str()); - std::string hashname = package_name + std::string("::itemcount"); - perl->eval(std::string("%").append(hashname).append(" = ();").c_str()); - perl->eval(std::string("++$").append(hashname).append("{$").append(package_name).append("::item1};").c_str()); - perl->eval(std::string("++$").append(hashname).append("{$").append(package_name).append("::item2};").c_str()); - perl->eval(std::string("++$").append(hashname).append("{$").append(package_name).append("::item3};").c_str()); - perl->eval(std::string("++$").append(hashname).append("{$").append(package_name).append("::item4};").c_str()); + auto unique_id = npcmob->GetNPCTypeID(); + if (npcmob->IsBot()) { + unique_id = npcmob->CastToBot()->GetBotID(); + } + + ExportVar(package_name.c_str(), "copper", GetVar(fmt::format("copper.{}", unique_id)).c_str()); + ExportVar(package_name.c_str(), "silver", GetVar(fmt::format("silver.{}", unique_id)).c_str()); + ExportVar(package_name.c_str(), "gold", GetVar(fmt::format("gold.{}", unique_id)).c_str()); + ExportVar(package_name.c_str(), "platinum", GetVar(fmt::format("platinum.{}", unique_id)).c_str()); + + auto hash_name = fmt::format("{}::itemcount", package_name); + perl->eval(fmt::format("%{} = ();", hash_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item1}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item2}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item3}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item4}};", hash_name, package_name).c_str()); + + if (npcmob->IsBot()) { + perl->eval(fmt::format("++${}{{${}::item5}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item6}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item7}};", hash_name, package_name).c_str()); + perl->eval(fmt::format("++${}{{${}::item8}};", hash_name, package_name).c_str()); + } + break; } @@ -1744,7 +1743,7 @@ void PerlembParser::ExportEventVariables( break; } - + #ifdef BOTS case EVENT_SPELL_EFFECT_BUFF_TIC_BOT: #endif diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index 7a1d0c84f..b8ff97fbe 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -294,6 +294,7 @@ LuaParser::LuaParser() { BotArgumentDispatch[EVENT_SLAY] = handle_bot_slay; BotArgumentDispatch[EVENT_TARGET_CHANGE] = handle_bot_target_change; BotArgumentDispatch[EVENT_TIMER] = handle_bot_timer; + BotArgumentDispatch[EVENT_TRADE] = handle_bot_trade; BotArgumentDispatch[EVENT_USE_SKILL] = handle_bot_use_skill; #endif diff --git a/zone/lua_parser_events.cpp b/zone/lua_parser_events.cpp index 9a7ed9c04..0735ef3b5 100644 --- a/zone/lua_parser_events.cpp +++ b/zone/lua_parser_events.cpp @@ -49,16 +49,15 @@ void handle_npc_event_trade(QuestInterface *parse, lua_State* L, NPC* npc, Mob * luabind::adl::object l_client_o = luabind::adl::object(L, l_client); l_client_o.push(L); lua_setfield(L, -2, "other"); - + lua_createtable(L, 0, 0); - std::stringstream ident; - ident << npc->GetNPCTypeID(); - - if(extra_pointers) { + const auto npc_id = npc->GetNPCTypeID(); + + if (extra_pointers) { size_t sz = extra_pointers->size(); - for(size_t i = 0; i < sz; ++i) { - std::string prefix = "item" + std::to_string(i + 1); - EQ::ItemInstance *inst = std::any_cast(extra_pointers->at(i)); + for (size_t i = 0; i < sz; ++i) { + auto prefix = fmt::format("item{}", i + 1); + auto* inst = std::any_cast(extra_pointers->at(i)); Lua_ItemInst l_inst = inst; luabind::adl::object l_inst_o = luabind::adl::object(L, l_inst); @@ -68,17 +67,30 @@ void handle_npc_event_trade(QuestInterface *parse, lua_State* L, NPC* npc, Mob * } } - lua_pushinteger(L, std::stoul(parse->GetVar("platinum." + ident.str()))); + auto money_string = fmt::format("platinum.{}", npc_id); + uint32 money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); lua_setfield(L, -2, "platinum"); - lua_pushinteger(L, std::stoul(parse->GetVar("gold." + ident.str()))); + money_string = fmt::format("gold.{}", npc_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); lua_setfield(L, -2, "gold"); - lua_pushinteger(L, std::stoul(parse->GetVar("silver." + ident.str()))); + money_string = fmt::format("silver.{}", npc_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); lua_setfield(L, -2, "silver"); - lua_pushinteger(L, std::stoul(parse->GetVar("copper." + ident.str()))); + money_string = fmt::format("copper.{}", npc_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); lua_setfield(L, -2, "copper"); + lua_setfield(L, -2, "trade"); } @@ -262,12 +274,12 @@ void handle_npc_loot_zone(QuestInterface *parse, lua_State* L, NPC* npc, Mob *in luabind::adl::object l_client_o = luabind::adl::object(L, l_client); l_client_o.push(L); lua_setfield(L, -2, "other"); - + Lua_ItemInst l_item(std::any_cast(extra_pointers->at(0))); luabind::adl::object l_item_o = luabind::adl::object(L, l_item); l_item_o.push(L); lua_setfield(L, -2, "item"); - + Lua_Corpse l_corpse(std::any_cast(extra_pointers->at(1))); luabind::adl::object l_corpse_o = luabind::adl::object(L, l_corpse); l_corpse_o.push(L); @@ -420,10 +432,10 @@ void handle_player_cast(QuestInterface *parse, lua_State* L, Client* client, std } lua_setfield(L, -2, "spell"); - + lua_pushinteger(L, std::stoi(sep.arg[1])); lua_setfield(L, -2, "caster_id"); - + lua_pushinteger(L, std::stoi(sep.arg[2])); lua_setfield(L, -2, "caster_level"); } @@ -534,7 +546,7 @@ void handle_player_combine(QuestInterface *parse, lua_State* L, Client* client, lua_setfield(L, -2, "recipe_id"); lua_pushstring(L, data.c_str()); - lua_setfield(L, -2, "recipe_name"); + lua_setfield(L, -2, "recipe_name"); } void handle_player_feign(QuestInterface *parse, lua_State* L, Client* client, std::string data, uint32 extra_data, @@ -650,7 +662,7 @@ void handle_player_quest_combine(QuestInterface* parse, lua_State* L, Client* cl lua_pushinteger(L, std::stoi(data)); lua_setfield(L, -2, "container_slot"); } - + void handle_player_consider(QuestInterface* parse, lua_State* L, Client* client, std::string data, uint32 extra_data, std::vector* extra_pointers) { lua_pushinteger(L, std::stoi(data)); lua_setfield(L, -2, "entity_id"); @@ -817,7 +829,7 @@ void handle_spell_event(QuestInterface *parse, lua_State* L, Mob* mob, Client* c lua_pushinteger(L, std::stoi(sep.arg[3])); lua_setfield(L, -2, "buff_slot"); - + Lua_Spell l_spell(spell_id); luabind::adl::object l_spell_o = luabind::adl::object(L, l_spell); l_spell_o.push(L); @@ -853,7 +865,7 @@ void handle_player_equip_item(QuestInterface *parse, lua_State* L, Client* clien lua_pushnumber(L, std::stoi(sep.arg[1])); lua_setfield(L, -2, "slot_id"); - + Lua_ItemInst l_item(extra_data); luabind::adl::object l_item_o = luabind::adl::object(L, l_item); l_item_o.push(L); @@ -995,10 +1007,10 @@ void handle_bot_cast( } lua_setfield(L, -2, "spell"); - + lua_pushinteger(L, std::stoi(sep.arg[1])); lua_setfield(L, -2, "caster_id"); - + lua_pushinteger(L, std::stoi(sep.arg[2])); lua_setfield(L, -2, "caster_level"); } @@ -1153,6 +1165,64 @@ void handle_bot_timer( lua_setfield(L, -2, "timer"); } +void handle_bot_trade( + QuestInterface *parse, + lua_State* L, + Bot* bot, + Mob *init, + std::string data, + uint32 extra_data, + std::vector *extra_pointers +) { + Lua_Client l_client(reinterpret_cast(init)); + luabind::adl::object l_client_o = luabind::adl::object(L, l_client); + l_client_o.push(L); + lua_setfield(L, -2, "other"); + + lua_createtable(L, 0, 0); + const auto bot_id = bot->GetBotID(); + + if (extra_pointers) { + size_t sz = extra_pointers->size(); + for (size_t i = 0; i < sz; ++i) { + auto prefix = fmt::format("item{}", i + 1); + auto* inst = std::any_cast(extra_pointers->at(i)); + + Lua_ItemInst l_inst = inst; + luabind::adl::object l_inst_o = luabind::adl::object(L, l_inst); + l_inst_o.push(L); + + lua_setfield(L, -2, prefix.c_str()); + } + } + + auto money_string = fmt::format("platinum.{}", bot_id); + uint32 money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); + lua_setfield(L, -2, "platinum"); + + money_string = fmt::format("gold.{}", bot_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); + lua_setfield(L, -2, "gold"); + + money_string = fmt::format("silver.{}", bot_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); + lua_setfield(L, -2, "silver"); + + money_string = fmt::format("copper.{}", bot_id); + money_value = !parse->GetVar(money_string).empty() ? std::stoul(parse->GetVar(money_string)) : 0; + + lua_pushinteger(L, money_value); + lua_setfield(L, -2, "copper"); + + lua_setfield(L, -2, "trade"); +} + void handle_bot_use_skill( QuestInterface *parse, lua_State* L, diff --git a/zone/lua_parser_events.h b/zone/lua_parser_events.h index 67d50e2b3..0732359be 100644 --- a/zone/lua_parser_events.h +++ b/zone/lua_parser_events.h @@ -276,6 +276,16 @@ void handle_bot_timer( std::vector *extra_pointers ); +void handle_bot_trade( + QuestInterface *parse, + lua_State* L, + Bot* bot, + Mob* init, + std::string data, + uint32 extra_data, + std::vector *extra_pointers +); + void handle_bot_use_skill( QuestInterface *parse, lua_State* L, diff --git a/zone/quest_parser_collection.cpp b/zone/quest_parser_collection.cpp index 6099bb4ba..81e4c2a9e 100644 --- a/zone/quest_parser_collection.cpp +++ b/zone/quest_parser_collection.cpp @@ -1234,6 +1234,10 @@ bool QuestParserCollection::BotHasQuestSubGlobal(QuestEventID evt) { return false; } +bool QuestParserCollection::BotHasQuestSub(QuestEventID evt) { + return BotHasQuestSubLocal(evt) || BotHasQuestSubGlobal(evt); +} + QuestInterface *QuestParserCollection::GetQIByBotQuest(std::string &filename) { if (!zone || !zone->IsLoaded()) { return nullptr;