diff --git a/.gitignore b/.gitignore index d835a1ceb..be8fccbc3 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,5 @@ bin/ /Win32 /x64 /client_files/**/CMakeFiles/ - -.idea +submodules/libuv +.idea \ No newline at end of file diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 000000000..9204f06eb --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,15 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + } + ] +} \ No newline at end of file diff --git a/common/repositories/base/base_raid_members_repository.h b/common/repositories/base/base_raid_members_repository.h index ed90853f0..11a77d36b 100644 --- a/common/repositories/base/base_raid_members_repository.h +++ b/common/repositories/base/base_raid_members_repository.h @@ -28,6 +28,9 @@ public: int isgroupleader; int israidleader; int islooter; +#ifdef BOTS + int isbot; +#endif }; static std::string PrimaryKey() @@ -47,6 +50,9 @@ public: "isgroupleader", "israidleader", "islooter", +#ifdef BOTS + "isbot", +#endif }; } @@ -111,6 +117,9 @@ public: entry.isgroupleader = 0; entry.israidleader = 0; entry.islooter = 0; +#ifdef BOTS + entry.isbot = 0; +#endif return entry; } @@ -154,7 +163,10 @@ public: entry.name = row[5] ? row[5] : ""; entry.isgroupleader = atoi(row[6]); entry.israidleader = atoi(row[7]); - entry.islooter = atoi(row[8]); + entry.islooter = atoi(row[8]); +#ifdef BOTS + entry.isbot = atoi(row[9]); +#endif return entry; } @@ -197,6 +209,9 @@ public: update_values.push_back(columns[6] + " = " + std::to_string(raid_members_entry.isgroupleader)); update_values.push_back(columns[7] + " = " + std::to_string(raid_members_entry.israidleader)); update_values.push_back(columns[8] + " = " + std::to_string(raid_members_entry.islooter)); +#ifdef BOTS + update_values.push_back(columns[9] + " = " + std::to_string(raid_members_entry.isbot)); +#endif auto results = db.QueryDatabase( fmt::format( @@ -227,7 +242,9 @@ public: insert_values.push_back(std::to_string(raid_members_entry.isgroupleader)); insert_values.push_back(std::to_string(raid_members_entry.israidleader)); insert_values.push_back(std::to_string(raid_members_entry.islooter)); - +#ifdef BOTS + insert_values.push_back(std::to_string(raid_members_entry.isbot)); +#endif auto results = db.QueryDatabase( fmt::format( "{} VALUES ({})", @@ -265,7 +282,9 @@ public: insert_values.push_back(std::to_string(raid_members_entry.isgroupleader)); insert_values.push_back(std::to_string(raid_members_entry.israidleader)); insert_values.push_back(std::to_string(raid_members_entry.islooter)); - +#ifdef BOTS + insert_values.push_back(std::to_string(raid_members_entry.isbot)); +#endif insert_chunks.push_back("(" + implode(",", insert_values) + ")"); } @@ -307,7 +326,9 @@ public: entry.isgroupleader = atoi(row[6]); entry.israidleader = atoi(row[7]); entry.islooter = atoi(row[8]); - +#ifdef BOTS + entry.isbot = atoi(row[9]); +#endif all_entries.push_back(entry); } @@ -340,7 +361,9 @@ public: entry.isgroupleader = atoi(row[6]); entry.israidleader = atoi(row[7]); entry.islooter = atoi(row[8]); - +#ifdef BOTS + entry.isbot = atoi(row[9]); +#endif all_entries.push_back(entry); } diff --git a/common/ruletypes.h b/common/ruletypes.h index d06f9280c..0bb9758b7 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -622,10 +622,13 @@ RULE_REAL(Bots, LeashDistance, 562500.0f, "Distance a bot is allowed to travel f RULE_BOOL(Bots, AllowApplyPoisonCommand, true, "Allows the use of the bot command 'applypoison'") RULE_BOOL(Bots, AllowApplyPotionCommand, true, "Allows the use of the bot command 'applypotion'") RULE_BOOL(Bots, RestrictApplyPotionToRogue, true, "Restricts the bot command 'applypotion' to rogue-usable potions (i.e., poisons)") +RULE_BOOL(Bots, DisplayHealDamage, false, "Enables the display of bot heal damage to the bot owner client") +RULE_BOOL(Bots, DisplaySpellDamage, false, "Enables the display of bot spell damage to the bot owner client") RULE_BOOL(Bots, OldRaceRezEffects, false, "Older clients had ID 757 for races with high starting STR, but it doesn't seem used anymore") RULE_BOOL(Bots, ResurrectionSickness, true, "Use Resurrection Sickness based on Resurrection spell cast, set to false to disable Resurrection Sickness.") RULE_INT(Bots, OldResurrectionSicknessSpell, 757, "757 is Default Old Resurrection Sickness Spell") RULE_INT(Bots, ResurrectionSicknessSpell, 756, "756 is Default Resurrection Sickness Spell") + RULE_CATEGORY_END() #endif diff --git a/common/types.h b/common/types.h index b3be97d32..2a6355296 100644 --- a/common/types.h +++ b/common/types.h @@ -57,6 +57,9 @@ typedef const char Const_char; //for perl XS #define safe_delete(d) if(d) { delete d; d=nullptr; } #define safe_delete_array(d) if(d) { delete[] d; d=nullptr; } +//#define safe_delete(d)(_RPTF0(_CRT_WARN,"Testing delete function\n")); +//#define safe_delete(d) if(d) { } +//#define safe_delete_array(d) if(d) { } #define L32(i) ((uint32) i) #define H32(i) ((uint32) (i >> 32)) #define L16(i) ((uint16) i) diff --git a/utils/sql/git/bots/bots_db_update_manifest.txt b/utils/sql/git/bots/bots_db_update_manifest.txt index 221020683..114ee942a 100644 --- a/utils/sql/git/bots/bots_db_update_manifest.txt +++ b/utils/sql/git/bots/bots_db_update_manifest.txt @@ -27,6 +27,7 @@ 9026|2019_09_09_bots_owner_options_rework.sql|SHOW COLUMNS FROM `bot_owner_options` LIKE 'option_type'|empty| 9027|2020_03_30_bots_view_update.sql|SELECT * FROM db_version WHERE bots_version >= 9027|empty| 9028|2021_06_04_bot_create_combinations.sql|SHOW TABLES LIKE 'bot_create_combinations'|empty| +9029|2022_02_08_bots_raid_members.sql|SELECT * FROM db_version WHERE bots_version >= 9028|empty| # Upgrade conditions: # This won't be needed after this system is implemented, but it is used database that are not diff --git a/utils/sql/git/bots/required/2022_02_08_bots_raid_members.sql b/utils/sql/git/bots/required/2022_02_08_bots_raid_members.sql new file mode 100644 index 000000000..1f80a1a29 --- /dev/null +++ b/utils/sql/git/bots/required/2022_02_08_bots_raid_members.sql @@ -0,0 +1 @@ +ALTER TABLE `raid_members` ADD COLUMN `isbot` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''; \ No newline at end of file diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 51fc7f875..442c74344 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -11,6 +11,7 @@ SET(zone_sources beacon.cpp bonuses.cpp bot.cpp + bot_raid.cpp bot_command.cpp bot_database.cpp botspellsai.cpp diff --git a/zone/bot.h b/zone/bot.h index fa64ae5da..b53128fbd 100644 --- a/zone/bot.h +++ b/zone/bot.h @@ -35,6 +35,7 @@ #include "../common/global_define.h" #include "guild_mgr.h" #include "worldserver.h" +#include "raids.h" #include @@ -151,8 +152,8 @@ public: ExtraAttackOptions *opts = nullptr); virtual bool HasRaid() { return (GetRaid() ? true : false); } virtual bool HasGroup() { return (GetGroup() ? true : false); } - virtual Raid* GetRaid() { return entity_list.GetRaidByMob(this); } - virtual Group* GetGroup() { return entity_list.GetGroupByMob(this); } + virtual Raid* GetRaid() { return entity_list.GetRaidByMob(this); } // GetRaidByMob(this); + virtual Group* GetGroup() { return entity_list.GetGroupByMob(this); } // GetGroupByMob; // Common, but informal "interfaces" with Client object uint32 CharacterID() { return GetBotID(); } // Just returns the Bot Id @@ -375,6 +376,15 @@ public: static bool CheckDisciplineRecastTimers(Bot *caster, int timer_index); static uint32 GetDisciplineRemainingTime(Bot *caster, int timer_index); + //Raid methods + void PetAIProcess_Raid(); + void AI_Process_Raid(); + bool AICastSpell_Raid(Mob* tar, uint8 iChance, uint32 iSpellTypes); + static void ProcessRaidInvite(Bot* invitee, Client* invitor); + static void ProcessRaidInvite(Client* invitee, Client* invitor); + uint8 GetNumberNeedingHealedInRaidGroup(uint8 hpr, bool includePets); + inline void SetDirtyAutoHaters() { m_dirtyautohaters = true; } + static std::list GetBotSpellsForSpellEffect(Bot* botCaster, int spellEffect); static std::list GetBotSpellsForSpellEffectAndTargetType(Bot* botCaster, int spellEffect, SpellTargetType targetType); static std::list GetBotSpellsBySpellType(Bot* botCaster, uint32 spellType); @@ -610,6 +620,9 @@ public: int32 GetBaseDR() { return _baseDR; } int32 GetBaseCorrup() { return _baseCorrup; } + //Raid additions + Raid* p_raid_instance; + protected: virtual void PetAIProcess(); virtual void BotMeditate(bool isSitting); @@ -674,6 +687,7 @@ private: Timer m_auto_defend_timer; //Timer m_combat_jitter_timer; //bool m_combat_jitter_flag; + bool m_dirtyautohaters; bool m_guard_flag; bool m_hold_flag; bool m_attack_flag; @@ -740,6 +754,7 @@ private: public: static uint8 spell_casting_chances[SPELL_TYPE_COUNT][PLAYER_CLASS_COUNT][EQ::constants::STANCE_TYPE_COUNT][cntHSND]; + }; #endif // BOTS diff --git a/zone/bot_raid.cpp b/zone/bot_raid.cpp new file mode 100644 index 000000000..d7853299a --- /dev/null +++ b/zone/bot_raid.cpp @@ -0,0 +1,2630 @@ +/* EQEMu: Everquest Server Emulator + Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) + + 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; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#ifdef BOTS + +#include "bot.h" +#include "object.h" +#include "raids.h" +#include "doors.h" +#include "quest_parser_collection.h" +#include "lua_parser.h" +#include "../common/string_util.h" +#include "../common/say_link.h" + +extern volatile bool is_zone_loaded; +extern bool Critical; + +// AI Processing for the Bot object + +constexpr float MAX_CASTER_DISTANCE[PLAYER_CLASS_COUNT] = { + 0, (34 * 34), (24 * 24), (28 * 28), (26 * 26), (42 * 42), 0, (30 * 30), 0, (38 * 38), (54 * 54), (48 * 48), (52 * 52), (50 * 50), (32 * 32), 0 + // W C P R S D M B R S N W M E B B + // A L A N H R N R O H E I A N S E + // R R L G D U K D G M C Z G C T R +}; + +void Bot::AI_Process_Raid() +{ +#define TEST_COMBATANTS() if (!GetTarget() || GetAppearance() == eaDead) { return; } +#define PULLING_BOT (GetPullingFlag() || GetReturningFlag()) +#define NOT_PULLING_BOT (!GetPullingFlag() && !GetReturningFlag()) +#define GUARDING (GetGuardFlag()) +#define NOT_GUARDING (!GetGuardFlag()) +#define HOLDING (GetHoldFlag()) +#define NOT_HOLDING (!GetHoldFlag()) +#define PASSIVE (GetBotStance() == EQ::constants::stancePassive) +#define NOT_PASSIVE (GetBotStance() != EQ::constants::stancePassive) + + Raid* raid = entity_list.GetRaidByBotName(this->GetName()); + Client* bot_owner = (GetBotOwner() && GetBotOwner()->IsClient() ? GetBotOwner()->CastToClient() : nullptr); + uint32 r_group = raid->GetGroup(GetName()); + + LogAI("Bot_Raid: Entered Raid Process() for [{}].", this->GetCleanName()); + + //#pragma region PRIMARY AI SKIP CHECKS + + // Primary reasons for not processing AI + if (!bot_owner || (!raid) || !IsAIControlled()) { + return; + } + + if (bot_owner->IsDead()) { + + SetTarget(nullptr); + SetBotOwner(nullptr); + + return; + } + + // We also need a leash owner and follow mob (subset of primary AI criteria) + Client* leash_owner = nullptr; + if (r_group < 12 && raid->IsGroupLeader(this->GetName())) { + leash_owner = raid->GetLeader(); + } + else if (r_group < 12) { + leash_owner = raid->GetGroupLeader(r_group); + } + else { + leash_owner = bot_owner; + } + + if (!leash_owner) { + return; + } + + //#pragma endregion + + Mob* follow_mob = entity_list.GetMob(GetFollowID()); + if (!follow_mob) { + + follow_mob = leash_owner; + SetFollowID(leash_owner->GetID()); + } + + if (this->mana_timer.Check(false)) { + raid->SendHPManaEndPacketsFrom(this); + } + if (send_hp_update_timer.Check(false)) { + + raid->SendHPManaEndPacketsFrom(this); + + } + // Berserk updates should occur if primary AI criteria are met + if (GetClass() == WARRIOR || GetClass() == BERSERKER) { + + if (!berserk && GetHP() > 0 && GetHPRatio() < 30.0f) { + + entity_list.MessageCloseString(this, false, 200, 0, BERSERK_START, GetName()); + berserk = true; + } + + if (berserk && GetHPRatio() >= 30.0f) { + + entity_list.MessageCloseString(this, false, 200, 0, BERSERK_END, GetName()); + berserk = false; + } + } + + //#pragma region SECONDARY AI SKIP CHECKS + + // Secondary reasons for not processing AI + if (GetPauseAI() || IsStunned() || IsMezzed() || (GetAppearance() == eaDead)) { + + if (IsCasting()) { + InterruptSpell(); + } + + if (IsMyHealRotationSet() || (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this)) { + + AdvanceHealRotation(false); + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + } + + return; + } + + //#pragma endregion + + float fm_distance = DistanceSquared(m_Position, follow_mob->GetPosition()); + float lo_distance = DistanceSquared(m_Position, leash_owner->GetPosition()); + float leash_distance = RuleR(Bots, LeashDistance); + + //#pragma region CURRENTLY CASTING CHECKS + + if (IsCasting()) { + + if (IsHealRotationMember() && + m_member_of_heal_rotation->CastingOverride() && + m_member_of_heal_rotation->CastingTarget() != nullptr && + m_member_of_heal_rotation->CastingReady() && + m_member_of_heal_rotation->CastingMember() == this && + !m_member_of_heal_rotation->MemberIsCasting(this)) + { + InterruptSpell(); + } + else if (AmICastingForHealRotation() && m_member_of_heal_rotation->CastingMember() == this) { + + AdvanceHealRotation(false); + return; + } + else if (GetClass() != BARD) { + + if (IsEngaged()) { + return; + } + + if ( + (NOT_GUARDING && fm_distance > GetFollowDistance()) || // Cancel out-of-combat casting if movement to follow mob is required + (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) > GetFollowDistance()) // Cancel out-of-combat casting if movement to guard point is required + ) { + InterruptSpell(); + } + + return; + } + } + else if (IsHealRotationMember()) { + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + } + + //#pragma endregion + + // Can't move if rooted... + if (IsRooted() && IsMoving()) { + + StopMoving(); + return; + } + + //#pragma region HEAL ROTATION CASTING CHECKS + + if (IsMyHealRotationSet()) { + + if (AIHealRotation(HealRotationTarget(), UseHealRotationFastHeals())) { + + m_member_of_heal_rotation->SetMemberIsCasting(this); + m_member_of_heal_rotation->UpdateTargetHealingStats(HealRotationTarget()); + AdvanceHealRotation(); + } + else { + + m_member_of_heal_rotation->SetMemberIsCasting(this, false); + AdvanceHealRotation(false); + } + } + + //#pragma endregion + + bool bo_alt_combat = (RuleB(Bots, AllowOwnerOptionAltCombat) && bot_owner->GetBotOption(Client::booAltCombat)); + + //#pragma region ATTACK FLAG + + if (GetAttackFlag()) { // Push owner's target onto our hate list + + if (GetPet() && PULLING_BOT) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto attack_target = bot_owner->GetTarget(); + + if (attack_target) { + + InterruptSpell(); + WipeHateList(); + AddToHateList(attack_target, 1); + SetTarget(attack_target); + SetAttackingFlag(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->WipeHateList(); + GetPet()->AddToHateList(attack_target, 1); + GetPet()->SetTarget(attack_target); + } + } + } + } + + //#pragma endregion + + //#pragma region PULL FLAG + + else if (GetPullFlag()) { // Push owner's target onto our hate list and set flags so other bots do not aggro + + SetAttackFlag(false); + SetAttackingFlag(false); + SetPullFlag(false); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto pull_target = bot_owner->GetTarget(); + if (pull_target) { + + Bot::BotGroupSay(this, "Pulling %s to the group..", pull_target->GetCleanName()); + //raid->RaidBotGroupSay(this, 0, 100, "Pulling %s to the group..", pull_target->GetCleanName()); + InterruptSpell(); + WipeHateList(); + AddToHateList(pull_target, 1); + SetTarget(pull_target); + SetPullingFlag(); + bot_owner->SetBotPulling(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + m_previous_pet_order = GetPet()->GetPetOrder(); + GetPet()->SetPetOrder(SPO_Guard); + } + } + } + } + + //#pragma endregion + + //#pragma region ALT COMBAT (ACQUIRE HATE) + + else if (bo_alt_combat && m_alt_combat_hate_timer.Check(false)) { // 'Alt Combat' gives some more 'control' options on how bots process aggro + + // Empty hate list - let's find some aggro + if (!IsEngaged() && NOT_HOLDING && NOT_PASSIVE && (!bot_owner->GetBotPulling() || NOT_PULLING_BOT)) { + + Mob* lo_target = leash_owner->GetTarget(); + if (lo_target && + lo_target->IsNPC() && + !lo_target->IsMezzed() && + ((bot_owner->GetBotOption(Client::booAutoDefend) && lo_target->GetHateAmount(leash_owner)) || leash_owner->AutoAttackEnabled()) && + lo_distance <= leash_distance && + DistanceSquared(m_Position, lo_target->GetPosition()) <= leash_distance && + (CheckLosFN(lo_target) || leash_owner->CheckLosFN(lo_target)) && + IsAttackAllowed(lo_target)) + { + AddToHateList(lo_target, 1); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(lo_target, 1); + GetPet()->SetTarget(lo_target); + } + } + else { + + std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); + for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { + + Mob* bg_member = iter->member;// bot_group->members[counter]; + if (!bg_member) { + continue; + } + + Mob* bgm_target = bg_member->GetTarget(); + if (!bgm_target || !bgm_target->IsNPC()) { + continue; + } + + if (!bgm_target->IsMezzed() && + ((bot_owner->GetBotOption(Client::booAutoDefend) && bgm_target->GetHateAmount(bg_member)) || leash_owner->AutoAttackEnabled()) && + lo_distance <= leash_distance && + DistanceSquared(m_Position, bgm_target->GetPosition()) <= leash_distance && + (CheckLosFN(bgm_target) || leash_owner->CheckLosFN(bgm_target)) && + IsAttackAllowed(bgm_target)) + { + AddToHateList(bgm_target, 1); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(bgm_target, 1); + GetPet()->SetTarget(bgm_target); + } + + break; + } + } + } + } + } + + //#pragma endregion + + glm::vec3 Goal(0, 0, 0); + + // We have aggro to choose from + if (IsEngaged()) { + + if (rest_timer.Enabled()) { + rest_timer.Disable(); + } + + //#pragma region PULLING FLAG (TARGET VALIDATION) + + if (GetPullingFlag()) { + + if (!GetTarget()) { + + WipeHateList(); + SetTarget(nullptr); + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + + return; + } + else if (GetTarget()->GetHateList().size()) { + + WipeHateList(); + SetTarget(nullptr); + SetPullingFlag(false); + SetReturningFlag(); + + return; + } + else { + // Default action is to aggress towards enemy + } + } + + //#pragma endregion + + //#pragma region RETURNING FLAG + + else if (GetReturningFlag()) { + + // Need to make it back to group before clearing return flag + if (fm_distance <= GetFollowDistance()) { + + // Once we're back, clear blocking flags so everyone else can join in + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + // Need to keep puller out of combat until they reach their 'return to' destination + if (HasTargetReflection()) { + + SetTarget(nullptr); + WipeHateList(); + + return; + } + } + + //#pragma endregion + + //#pragma region ALT COMBAT (ACQUIRE TARGET) + + else if (bo_alt_combat && m_alt_combat_hate_timer.Check()) { // Find a mob from hate list to target + + // Raid Group roles can be expounded upon in the future + //r_group is the uint32 group id + auto assist_mob = raid->GetRaidMainAssistOneByName(this->GetName()); + bool find_target = true; + + if (!assist_mob) { + bot_owner->Message(Chat::Yellow, "Assist Mob is nullptr"); + } + + if (assist_mob) { + + if (assist_mob->GetTarget()) { + + if (assist_mob != this) { + + SetTarget(assist_mob->GetTarget()); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + // This artificially inflates pet's target aggro..but, less expensive than checking hate each AI process + GetPet()->AddToHateList(assist_mob->GetTarget(), 1); + GetPet()->SetTarget(assist_mob->GetTarget()); + } + } + + find_target = false; + } + else if (assist_mob != this) { + + SetTarget(nullptr); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + + find_target = false; + } + } + + if (find_target) { + + if (IsRooted()) { + SetTarget(hate_list.GetClosestEntOnHateList(this, true)); + } + else { + + // This will keep bots on target for now..but, future updates will allow for rooting/stunning + SetTarget(hate_list.GetEscapingEntOnHateList(leash_owner, leash_distance)); + if (!GetTarget()) { + SetTarget(hate_list.GetEntWithMostHateOnList(this, nullptr, true)); + } + } + } + } + + //#pragma endregion + + //#pragma region DEFAULT (ACQUIRE TARGET) + + else { + + // Default behavior doesn't have a means of acquiring a target from the bot's hate list.. + // ..that action occurs through commands or out-of-combat checks + // (Use current target, if already in combat) + } + + //#pragma endregion + + //#pragma region VERIFY TARGET AND STANCE + + Mob* tar = GetTarget(); // We should have a target..if not, we're awaiting new orders + if (!tar || PASSIVE) { + + SetTarget(nullptr); + WipeHateList(); + SetAttackFlag(false); + SetAttackingFlag(false); + if (PULLING_BOT) { + + // 'Flags' should only be set on the bot that is pulling + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + if (GetArchetype() == ARCHETYPE_CASTER) { + BotMeditate(true); + } + + return; + } + + //#pragma endregion + + //#pragma region ATTACKING FLAG (HATE VALIDATION) + + if (GetAttackingFlag() && tar->CheckAggro(this)) { + SetAttackingFlag(false); + } + + //#pragma endregion + + float tar_distance = DistanceSquared(m_Position, tar->GetPosition()); + + //#pragma region TARGET VALIDATION + + // DOUBLE-CHECK THIS CRITERIA + + // Verify that our target has attackable criteria + if (HOLDING || + !tar->IsNPC() || + tar->IsMezzed() || + lo_distance > leash_distance || + tar_distance > leash_distance || + (!GetAttackingFlag() && !CheckLosFN(tar) && !leash_owner->CheckLosFN(tar)) || // This is suppose to keep bots from attacking things behind walls + !IsAttackAllowed(tar) || + (bo_alt_combat && + (!GetAttackingFlag() && NOT_PULLING_BOT && !leash_owner->AutoAttackEnabled() && !tar->GetHateAmount(this) && !tar->GetHateAmount(leash_owner)) + ) + ) + { + // Normally, we wouldn't want to do this without class checks..but, too many issues can arise if we let enchanter animation pets run rampant + if (HasPet()) { + + GetPet()->RemoveFromHateList(tar); + GetPet()->SetTarget(nullptr); + } + + RemoveFromHateList(tar); + SetTarget(nullptr); + + SetAttackFlag(false); + SetAttackingFlag(false); + if (PULLING_BOT) { + + SetPullingFlag(false); + SetReturningFlag(false); + bot_owner->SetBotPulling(false); + if (GetPet()) { + GetPet()->SetPetOrder(m_previous_pet_order); + } + } + + if (IsMoving()) { + StopMoving(); + } + + return; + } + + //#pragma endregion + + // This causes conflicts with default pet handler (bounces between targets) + if (NOT_PULLING_BOT && HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + // We don't add to hate list here because it's assumed to already be on the list + GetPet()->SetTarget(tar); + } + + if (DivineAura()) { + return; + } + + if (!(m_PlayerState & static_cast(PlayerState::Aggressive))) { + SendAddPlayerState(PlayerState::Aggressive); + } + + //#pragma region PULLING FLAG (ACTIONABLE RANGE) + + if (GetPullingFlag()) { + + constexpr size_t PULL_AGGRO = 5225; // spells[5225]: 'Throw Stone' - 0 cast time + + if (tar_distance <= (spells[PULL_AGGRO].range * spells[PULL_AGGRO].range)) { + + StopMoving(); + CastSpell(PULL_AGGRO, tar->GetID()); + return; + } + } + + //#pragma endregion + + //#pragma region COMBAT RANGE CALCS + + bool atCombatRange = false; + + const auto* p_item = GetBotItem(EQ::invslot::slotPrimary); + const auto* s_item = GetBotItem(EQ::invslot::slotSecondary); + + bool behind_mob = false; + bool backstab_weapon = false; + if (GetClass() == ROGUE) { + + behind_mob = BehindMob(tar, GetX(), GetY()); // Can be separated for other future use + backstab_weapon = p_item && p_item->GetItemBackstabDamage(); + } + + // Calculate melee distances + float melee_distance_max = 0.0f; + float melee_distance = 0.0f; + { + float size_mod = GetSize(); + float other_size_mod = tar->GetSize(); + + if (GetRace() == RT_DRAGON || GetRace() == RT_WURM || GetRace() == RT_DRAGON_7) { // For races with a fixed size + size_mod = 60.0f; + } + else if (size_mod < 6.0f) { + size_mod = 8.0f; + } + + if (tar->GetRace() == RT_DRAGON || tar->GetRace() == RT_WURM || tar->GetRace() == RT_DRAGON_7) { // For races with a fixed size + other_size_mod = 60.0f; + } + else if (other_size_mod < 6.0f) { + other_size_mod = 8.0f; + } + + if (other_size_mod > size_mod) { + size_mod = other_size_mod; + } + + if (size_mod > 29.0f) { + size_mod *= size_mod; + } + else if (size_mod > 19.0f) { + size_mod *= (size_mod * 2.0f); + } + else { + size_mod *= (size_mod * 4.0f); + } + + // Prevention of ridiculously sized hit boxes + if (size_mod > 10000.0f) { + size_mod = (size_mod / 7.0f); + } + + melee_distance_max = size_mod; + + switch (GetClass()) { + case WARRIOR: + case PALADIN: + case SHADOWKNIGHT: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.45f; + } + else if ((s_item && s_item->GetItem()->IsTypeShield()) || (!p_item && !s_item)) { + melee_distance = melee_distance_max * 0.35f; + } + else { + melee_distance = melee_distance_max * 0.40f; + } + + break; + case NECROMANCER: + case WIZARD: + case MAGICIAN: + case ENCHANTER: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.95f; + } + else { + melee_distance = melee_distance_max * 0.75f; + } + + break; + case ROGUE: + if (behind_mob && backstab_weapon) { + if (p_item->GetItem()->IsType2HWeapon()) { // 'p_item' tested in 'backstab_weapon' check above + melee_distance = melee_distance_max * 0.30f; + } + else { + melee_distance = melee_distance_max * 0.25f; + } + + break; + } + // Fall-through + default: + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + melee_distance = melee_distance_max * 0.70f; + } + else { + melee_distance = melee_distance_max * 0.50f; + } + + break; + } + } + float melee_distance_min = melee_distance / 2.0f; + + // Calculate caster distances + float caster_distance_max = 0.0f; + float caster_distance_min = 0.0f; + float caster_distance = 0.0f; + { + if (GetLevel() >= GetStopMeleeLevel() && GetClass() >= WARRIOR && GetClass() <= BERSERKER) { + caster_distance_max = MAX_CASTER_DISTANCE[(GetClass() - 1)]; + } + + if (caster_distance_max) { + + caster_distance_min = melee_distance_max; + if (caster_distance_max <= caster_distance_min) { + caster_distance_max = caster_distance_min * 1.25f; + } + + caster_distance = ((caster_distance_max + caster_distance_min) / 2); + } + } + + bool atArcheryRange = IsArcheryRange(tar); + + if (GetRangerAutoWeaponSelect()) { + + bool changeWeapons = false; + + if (atArcheryRange && !IsBotArcher()) { + + SetBotArcher(true); + changeWeapons = true; + } + else if (!atArcheryRange && IsBotArcher()) { + + SetBotArcher(false); + changeWeapons = true; + } + + if (changeWeapons) { + ChangeBotArcherWeapons(IsBotArcher()); + } + } + + if (IsBotArcher() && atArcheryRange) { + atCombatRange = true; + } + else if (caster_distance_max && tar_distance <= caster_distance_max) { + atCombatRange = true; + } + else if (tar_distance <= melee_distance) { + atCombatRange = true; + } + + //#pragma endregion + + //#pragma region ENGAGED AT COMBAT RANGE + + // We can fight + if (atCombatRange) { + + //if (IsMoving() || GetCombatJitterFlag()) { // StopMoving() needs to be called so that the jitter timer can be reset + if (IsMoving()) { + + // Since we're using a pseudo-shadowstep for jitter, disregard the combat jitter flag + //if (!GetCombatJitterFlag()) { + StopMoving(CalculateHeadingToTarget(tar->GetX(), tar->GetY())); + //} + + return; + } + + // Combat 'jitter' code + // Note: Combat Jitter is disabled until a working movement solution can be found + if (AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == BARD)) { + + if (!IsRooted()) { + + if (HasTargetReflection()) { + + if (!tar->IsFeared() && !tar->IsStunned()) { + + if (GetClass() == ROGUE) { + + if (m_evade_timer.Check(false)) { // Attempt to evade + + int timer_duration = (HideReuseTime - GetSkillReuseTime(EQ::skills::SkillHide)) * 1000; + if (timer_duration < 0) { + timer_duration = 0; + } + + m_evade_timer.Start(timer_duration); + if (zone->random.Int(0, 260) < (int)GetSkill(EQ::skills::SkillHide)) { + RogueEvade(tar); + } + + return; + } + } + + //if (tar->IsRooted()) { // Move caster/rogue back from rooted mob - out of combat range, if necessary + + // if (GetArchetype() == ARCHETYPE_CASTER || GetClass() == ROGUE) { + + // if (tar_distance <= melee_distance_max) { + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + //} + } + } + //else { + + // if (caster_distance_min && tar_distance < caster_distance_min && !tar->IsFeared()) { // Caster back-off adjustment + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // if (DistanceSquared(Goal, tar->GetPosition()) <= caster_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (tar_distance < melee_distance_min) { // Melee back-off adjustment + + // if (PlotPositionAroundTarget(this, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionBehindMeFacingTarget(tar, Goal.x, Goal.y, Goal.z)) { + + // if (DistanceSquared(Goal, tar->GetPosition()) <= melee_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (backstab_weapon && !behind_mob) { // Move the rogue to behind the mob + + // if (PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //if (PlotPositionOnArcBehindTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= melee_distance_min && */distance_squared <= melee_distance_max) { + + // Teleport(Goal); + // //RunTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + // else if (m_combat_jitter_timer.Check()) { + + // if (!caster_distance && PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //if (!caster_distance && PlotPositionOnArcInFrontOfTarget(tar, Goal.x, Goal.y, Goal.z, melee_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= melee_distance_min && */distance_squared <= melee_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // else if (caster_distance && PlotPositionAroundTarget(tar, Goal.x, Goal.y, Goal.z)) { + // //else if (caster_distance && PlotPositionOnArcInFrontOfTarget(tar, Goal.x, Goal.y, Goal.z, caster_distance)) { + + // float distance_squared = DistanceSquared(Goal, tar->GetPosition()); + // if (/*distance_squared >= caster_distance_min && */distance_squared <= caster_distance_max) { + + // Teleport(Goal); + // //WalkTo(Goal.x, Goal.y, Goal.z); + // SetCombatJitterFlag(); + + // return; + // } + // } + // } + + // if (!IsFacingMob(tar)) { + + // FaceTarget(tar); + // return; + // } + //} + } + else { + + if (!IsSitting() && !IsFacingMob(tar)) { + + FaceTarget(tar); + return; + } + } + } + + if (!IsBotNonSpellFighter() && AI_EngagedCastCheck()) { + return; + } + + // Up to this point, GetTarget() has been safe to dereference since the initial + // TEST_COMBATANTS() call. Due to the chance of the target dying and our pointer + // being nullified, we need to test it before dereferencing to avoid crashes + + if (IsBotArcher() && ranged_timer.Check(false)) { // Can shoot mezzed, stunned and dead!? + + TEST_COMBATANTS(); + if (GetTarget()->GetHPRatio() <= 99.0f) { + BotRangedAttack(tar); + } + } + else if (!IsBotArcher() && GetLevel() < GetStopMeleeLevel()) { + + // We can't fight if we don't have a target, are stun/mezzed or dead.. + // Stop attacking if the target is enraged + TEST_COMBATANTS(); + if (tar->IsEnraged() && !BehindMob(tar, GetX(), GetY())) { + return; + } + + // First, special attack per class (kick, backstab etc..) + TEST_COMBATANTS(); + DoClassAttacks(tar); + + TEST_COMBATANTS(); + if (attack_timer.Check()) { // Process primary weapon attacks + + Attack(tar, EQ::invslot::slotPrimary); + + TEST_COMBATANTS(); + TriggerDefensiveProcs(tar, EQ::invslot::slotPrimary, false); + + TEST_COMBATANTS(); + //TryWeaponProc(p_item, tar, EQ::invslot::slotPrimary); + TryCombatProcs(p_item, tar, EQ::invslot::slotPrimary); + // bool tripleSuccess = false; + + TEST_COMBATANTS(); + if (CanThisClassDoubleAttack()) { + + if (CheckBotDoubleAttack()) { + Attack(tar, EQ::invslot::slotPrimary, true); + } + + TEST_COMBATANTS(); + if (GetSpecialAbility(SPECATK_TRIPLE) && CheckBotDoubleAttack(true)) { + // tripleSuccess = true; + Attack(tar, EQ::invslot::slotPrimary, true); + } + + TEST_COMBATANTS(); + // quad attack, does this belong here?? + if (GetSpecialAbility(SPECATK_QUAD) && CheckBotDoubleAttack(true)) { + Attack(tar, EQ::invslot::slotPrimary, true); + } + } + + TEST_COMBATANTS(); + // Live AA - Flurry, Rapid Strikes ect (Flurry does not require Triple Attack). + int32 flurrychance = (aabonuses.FlurryChance + spellbonuses.FlurryChance + itembonuses.FlurryChance); + if (flurrychance) { + + if (zone->random.Int(0, 100) < flurrychance) { + + MessageString(Chat::NPCFlurry, YOU_FLURRY); + Attack(tar, EQ::invslot::slotPrimary, false); + + TEST_COMBATANTS(); + Attack(tar, EQ::invslot::slotPrimary, false); + } + } + + TEST_COMBATANTS(); + //int32 ExtraAttackChanceBonus = (spellbonuses.ExtraAttackChance + itembonuses.ExtraAttackChance + aabonuses.ExtraAttackChance); + auto ExtraAttackChanceBonus = + (spellbonuses.ExtraAttackChance[0] + itembonuses.ExtraAttackChance[0] + + aabonuses.ExtraAttackChance[0]); + if (ExtraAttackChanceBonus) { + + if (p_item && p_item->GetItem()->IsType2HWeapon()) { + + if (zone->random.Int(0, 100) < ExtraAttackChanceBonus) { + Attack(tar, EQ::invslot::slotPrimary, false); + } + } + } + } + + TEST_COMBATANTS(); + if (attack_dw_timer.Check() && CanThisClassDualWield()) { // Process secondary weapon attacks + + const EQ::ItemData* s_itemdata = nullptr; + // Can only dual wield without a weapon if you're a monk + if (s_item || (GetClass() == MONK)) { + + if (s_item) { + s_itemdata = s_item->GetItem(); + } + + int weapon_type = 0; // No weapon type. + bool use_fist = true; + if (s_itemdata) { + + weapon_type = s_itemdata->ItemType; + use_fist = false; + } + + if (use_fist || !s_itemdata->IsType2HWeapon()) { + + float DualWieldProbability = 0.0f; + + int32 Ambidexterity = (aabonuses.Ambidexterity + spellbonuses.Ambidexterity + itembonuses.Ambidexterity); + DualWieldProbability = ((GetSkill(EQ::skills::SkillDualWield) + GetLevel() + Ambidexterity) / 400.0f); // 78.0 max + + int32 DWBonus = (spellbonuses.DualWieldChance + itembonuses.DualWieldChance); + DualWieldProbability += (DualWieldProbability * float(DWBonus) / 100.0f); + + float random = zone->random.Real(0, 1); + if (random < DualWieldProbability) { // Max 78% of DW + + Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + + TEST_COMBATANTS(); + TryCombatProcs(s_item, tar, EQ::invslot::slotSecondary); + //TryWeaponProc(s_item, tar, EQ::invslot::slotSecondary); + + TEST_COMBATANTS(); + if (CanThisClassDoubleAttack() && CheckBotDoubleAttack()) { + + if (tar->GetHP() > -10) { + Attack(tar, EQ::invslot::slotSecondary); // Single attack with offhand + } + } + } + } + } + } + } + + if (GetAppearance() == eaDead) { + return; + } + } + + //#pragma endregion + + //#pragma region ENGAGED NOT AT COMBAT RANGE + + else { // To far away to fight (GetTarget() validity can be iffy below this point - including outer scopes) + + // This code actually gets processed when we are too far away from target and have not engaged yet, too + if (/*!GetCombatJitterFlag() && */AI_movement_timer->Check() && (!spellend_timer.Enabled() || GetClass() == BARD)) { // Pursue processing + + if (GetTarget() && !IsRooted()) { + + LogAI("Pursuing [{}] while engaged", GetTarget()->GetCleanName()); + Goal = GetTarget()->GetPosition(); + if (DistanceSquared(m_Position, Goal) <= leash_distance) { + RunTo(Goal.x, Goal.y, Goal.z); + } + else { + + WipeHateList(); + SetTarget(nullptr); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + } + + return; + } + else { + + if (IsMoving()) { + StopMoving(); + } + return; + } + } + + if (GetTarget() && GetTarget()->IsFeared() && !spellend_timer.Enabled() && AI_think_timer->Check()) { + + if (!IsFacingMob(GetTarget())) { + FaceTarget(GetTarget()); + } + + // This is a mob that is fleeing either because it has been feared or is low on hitpoints + AI_PursueCastCheck(); // This appears to always return true..can't trust for success/fail + + return; + } + } // End not in combat range + +//#pragma endregion + + if (!IsMoving() && !spellend_timer.Enabled()) { // This may actually need work... + + if (GetTarget() && AI_EngagedCastCheck()) { + BotMeditate(false); + } + else if (GetArchetype() == ARCHETYPE_CASTER) { + BotMeditate(true); + } + + return; + } + } + else { // Out-of-combat behavior + + SetAttackFlag(false); + SetAttackingFlag(false); + if (!bot_owner->GetBotPulling()) { + + SetPullingFlag(false); + SetReturningFlag(false); + } + + //#pragma region AUTO DEFEND + + // This is as close as I could get without modifying the aggro mechanics and making it an expensive process... + // 'class Client' doesn't make use of hate_list... + if (RuleB(Bots, AllowOwnerOptionAutoDefend) && bot_owner->GetBotOption(Client::booAutoDefend)) { + + if (!m_auto_defend_timer.Enabled()) { + + m_auto_defend_timer.Start(zone->random.Int(250, 1250)); // random timer to simulate 'awareness' (cuts down on scanning overhead) + return; + } + + if (m_auto_defend_timer.Check() && bot_owner->GetAggroCount()) { + + if (NOT_HOLDING && NOT_PASSIVE) { + + auto xhaters = bot_owner->GetXTargetAutoMgr(); + if (xhaters && !xhaters->empty()) { + + for (auto hater_iter : xhaters->get_list()) { + + if (!hater_iter.spawn_id) { + continue; + } + + if (bot_owner->GetBotPulling() && bot_owner->GetTarget() && hater_iter.spawn_id == bot_owner->GetTarget()->GetID()) { + continue; + } + + auto hater = entity_list.GetMob(hater_iter.spawn_id); + if (hater && !hater->IsMezzed() && DistanceSquared(hater->GetPosition(), bot_owner->GetPosition()) <= leash_distance) { + + // This is roughly equivilent to npc attacking a client pet owner + AddToHateList(hater, 1); + SetTarget(hater); + SetAttackingFlag(); + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 2)) { + + GetPet()->AddToHateList(hater, 1); + GetPet()->SetTarget(hater); + } + + m_auto_defend_timer.Disable(); + + return; + } + } + } + } + } + } + + //#pragma endregion + + SetTarget(nullptr); + + if (HasPet() && (GetClass() != ENCHANTER || GetPet()->GetPetType() != petAnimation || GetAA(aaAnimationEmpathy) >= 1)) { + + GetPet()->WipeHateList(); + GetPet()->SetTarget(nullptr); + } + + if (m_PlayerState & static_cast(PlayerState::Aggressive)) { + SendRemovePlayerState(PlayerState::Aggressive); + } + + //#pragma region OK TO IDLE + + // Ok to idle + if ((NOT_GUARDING && fm_distance <= GetFollowDistance()) || (GUARDING && DistanceSquared(GetPosition(), GetGuardPoint()) <= GetFollowDistance())) { + + if (!IsMoving() && AI_think_timer->Check() && !spellend_timer.Enabled()) { + + if (NOT_PASSIVE) { + + if (!AI_IdleCastCheck() && !IsCasting() && GetClass() != BARD) { + BotMeditate(true); + } + } + else { + + if (GetClass() != BARD) { + BotMeditate(true); + } + } + + return; + } + } + + // Non-engaged movement checks + if (AI_movement_timer->Check() && (!IsCasting() || GetClass() == BARD)) { + + if (GUARDING) { + Goal = GetGuardPoint(); + } + else { + Goal = follow_mob->GetPosition(); + } + float destination_distance = DistanceSquared(GetPosition(), Goal); + + if ((!bot_owner->GetBotPulling() || PULLING_BOT) && (destination_distance > GetFollowDistance())) { + + if (!IsRooted()) { + + if (rest_timer.Enabled()) { + rest_timer.Disable(); + } + + bool running = true; + + if (destination_distance < GetFollowDistance() + BOT_FOLLOW_DISTANCE_WALK) { + running = false; + } + + if (running) { + RunTo(Goal.x, Goal.y, Goal.z); + } + else { + WalkTo(Goal.x, Goal.y, Goal.z); + } + + return; + } + } + else { + + if (IsMoving()) { + + StopMoving(); + return; + } + } + } + + // Basically, bard bots get a chance to cast idle spells while moving + if (GetClass() == BARD && IsMoving() && NOT_PASSIVE) { + + if (!spellend_timer.Enabled() && AI_think_timer->Check()) { + + AI_IdleCastCheck(); + return; + } + } + + //#pragma endregion + + } + +#undef TEST_COMBATANTS +#undef PULLING_BOT +#undef NOT_PULLING_BOT +#undef GUARDING +#undef NOT_GUARDING +#undef HOLDING +#undef NOT_HOLDING +#undef PASSIVE +#undef NOT_PASSIVE +} + +// AI Processing for a Bot object's pet if Bot is a member of a raid +void Bot::PetAIProcess_Raid() { + if (!HasPet() || !GetPet() || !GetPet()->IsNPC()) + return; + + Mob* BotOwner = this->GetBotOwner(); + NPC* botPet = this->GetPet()->CastToNPC(); + if (!botPet->GetOwner() || !botPet->GetID() || !botPet->GetOwnerID()) { + Kill(); + return; + } + + if (!botPet->IsAIControlled() || botPet->GetAttackTimer().Check(false) || botPet->IsCasting() || !botPet->GetOwner()->IsBot()) + return; + + if (IsEngaged()) { + if (botPet->IsRooted()) + botPet->SetTarget(hate_list.GetClosestEntOnHateList(botPet)); + else + botPet->SetTarget(hate_list.GetEntWithMostHateOnList(botPet)); + + // Let's check if we have a los with our target. + // If we don't, our hate_list is wiped. + // It causes some cpu stress but without it, it was causing the bot/pet to aggro behind wall, floor etc... + if (!botPet->CheckLosFN(botPet->GetTarget()) || botPet->GetTarget()->IsMezzed() || !botPet->IsAttackAllowed(GetTarget())) { + botPet->WipeHateList(); + botPet->SetTarget(botPet->GetOwner()); + return; + } + + botPet->FaceTarget(botPet->GetTarget()); + bool is_combat_range = botPet->CombatRange(botPet->GetTarget()); + // Ok, we're engaged, each class type has a special AI + // Only melee class will go to melee. Casters and healers will stay behind, following the leader by default. + // I should probably make the casters staying in place so they can cast.. + + // Ok, we 're a melee or any other class lvl<12. Yes, because after it becomes hard to go in melee for casters.. even for bots.. + if (is_combat_range) { + botPet->GetAIMovementTimer()->Check(); + if (botPet->IsMoving()) { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + botPet->SetRunAnimSpeed(0); + } + } + + if (!botPet->IsMoving()) { + float newX = 0; + float newY = 0; + float newZ = 0; + bool petHasAggro = false; + if (botPet->GetTarget() && botPet->GetTarget()->GetHateTop() && botPet->GetTarget()->GetHateTop() == botPet) + petHasAggro = true; + + if (botPet->GetClass() == ROGUE && !petHasAggro && !botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) { + // Move the rogue to behind the mob + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + else if (GetTarget() == botPet->GetTarget() && !petHasAggro && !botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) { + // If the bot owner and the bot are fighting the same mob, then move the pet to the rear arc of the mob + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + else if (DistanceSquaredNoZ(botPet->GetPosition(), botPet->GetTarget()->GetPosition()) < botPet->GetTarget()->GetSize()) { + // Let's try to adjust our melee range so we don't appear to be bunched up + bool isBehindMob = false; + bool moveBehindMob = false; + if (botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY())) + isBehindMob = true; + + if (!isBehindMob && !petHasAggro) + moveBehindMob = true; + + if (botPet->PlotPositionAroundTarget(botPet->GetTarget(), newX, newY, newZ, moveBehindMob)) { + botPet->RunTo(newX, newY, newZ); + return; + } + } + } + + // we can't fight if we don't have a target, are stun/mezzed or dead.. + if (botPet->GetTarget() && !botPet->IsStunned() && !botPet->IsMezzed() && (botPet->GetAppearance() != eaDead)) { + // check the delay on the attack + if (botPet->GetAttackTimer().Check()) { + // Stop attacking while we are on a front arc and the target is enraged + if (!botPet->BehindMob(botPet->GetTarget(), botPet->GetX(), botPet->GetY()) && botPet->GetTarget()->IsEnraged()) + return; + + if (botPet->Attack(GetTarget(), EQ::invslot::slotPrimary)) // try the main hand + if (botPet->GetTarget()) { + // We're a pet so we re able to dual attack + int32 RandRoll = zone->random.Int(0, 99); + if (botPet->CanThisClassDoubleAttack() && (RandRoll < (botPet->GetLevel() + NPCDualAttackModifier))) { + if (botPet->Attack(botPet->GetTarget(), EQ::invslot::slotPrimary)) {} + } + } + + if (botPet->GetOwner()->IsBot()) { + int aa_chance = 0; + int aa_skill = 0; + // Magician AA + aa_skill += botPet->GetOwner()->GetAA(aaElementalAlacrity); + // Necromancer AA + aa_skill += botPet->GetOwner()->GetAA(aaQuickeningofDeath); + // Beastlord AA + aa_skill += botPet->GetOwner()->GetAA(aaWardersAlacrity); + if (aa_skill >= 1) + aa_chance += ((aa_skill > 5 ? 5 : aa_skill) * 4); + + if (aa_skill >= 6) + aa_chance += ((aa_skill - 5 > 3 ? 3 : aa_skill - 5) * 7); + + if (aa_skill >= 9) + aa_chance += ((aa_skill - 8 > 3 ? 3 : aa_skill - 8) * 3); + + if (aa_skill >= 12) + aa_chance += ((aa_skill - 11) * 1); + + + //aa_chance += botPet->GetOwner()->GetAA(aaCompanionsAlacrity) * 3; + + if (zone->random.Int(1, 100) < aa_chance) + Flurry(nullptr); + } + + // Ok now, let's check pet's offhand. + if (botPet->GetAttackDWTimer().Check() && botPet->GetOwnerID() && botPet->GetOwner() && ((botPet->GetOwner()->GetClass() == MAGICIAN) || (botPet->GetOwner()->GetClass() == NECROMANCER) || (botPet->GetOwner()->GetClass() == SHADOWKNIGHT) || (botPet->GetOwner()->GetClass() == BEASTLORD))) { + if (botPet->GetOwner()->GetLevel() >= 24) { + float DualWieldProbability = ((botPet->GetSkill(EQ::skills::SkillDualWield) + botPet->GetLevel()) / 400.0f); + DualWieldProbability -= zone->random.Real(0, 1); + if (DualWieldProbability < 0) { + botPet->Attack(botPet->GetTarget(), EQ::invslot::slotSecondary); + if (botPet->CanThisClassDoubleAttack()) { + int32 RandRoll = zone->random.Int(0, 99); + if (RandRoll < (botPet->GetLevel() + 20)) + botPet->Attack(botPet->GetTarget(), EQ::invslot::slotSecondary); + } + } + } + } + if (!botPet->GetOwner()) + return; + + // Special attack + botPet->DoClassAttacks(botPet->GetTarget()); + } + // See if the pet can cast any spell + botPet->AI_EngagedCastCheck(); + } + } + else { + // Now, if we cannot reach our target + if (!botPet->HateSummon()) { + if (botPet->GetTarget() && botPet->AI_PursueCastCheck()) {} + else if (botPet->GetTarget() && botPet->GetAIMovementTimer()->Check()) { + botPet->SetRunAnimSpeed(0); + if (!botPet->IsRooted()) { + LogAI("Pursuing [{}] while engaged", botPet->GetTarget()->GetCleanName()); + botPet->RunTo(botPet->GetTarget()->GetX(), botPet->GetTarget()->GetY(), botPet->GetTarget()->GetZ()); + return; + } + else { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + StopNavigation(); + botPet->StopNavigation(); + } + } + } + } + } + } + else { + // Ok if we're not engaged, what's happening.. + if (botPet->GetTarget() != botPet->GetOwner()) + botPet->SetTarget(botPet->GetOwner()); + + if (!IsMoving()) + botPet->AI_IdleCastCheck(); + + if (botPet->GetAIMovementTimer()->Check()) { + switch (pStandingPetOrder) { + case SPO_Follow: { + float dist = DistanceSquared(botPet->GetPosition(), botPet->GetTarget()->GetPosition()); + botPet->SetRunAnimSpeed(0); + if (dist > 184) { + botPet->RunTo(botPet->GetTarget()->GetX(), botPet->GetTarget()->GetY(), botPet->GetTarget()->GetZ()); + return; + } + else { + botPet->SetHeading(botPet->GetTarget()->GetHeading()); + if (moved) { + moved = false; + StopNavigation(); + botPet->StopNavigation(); + } + } + break; + } + case SPO_Sit: + botPet->SetAppearance(eaSitting); + break; + case SPO_Guard: + botPet->NextGuardPosition(); + break; + } + } + } +} + +std::vector Raid::GetRaidGroupMembers(uint32 gid) +{ + std::vector raid_group_members; + raid_group_members.clear(); + + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + { + if (members[i].member && members[i].GroupNumber == gid) + { + raid_group_members.push_back(members[i]); + } + } + return raid_group_members; +} + +bool Bot::AICastSpell_Raid(Mob* tar, uint8 iChance, uint32 iSpellTypes) { + + // Bot AI Raid + + Raid* raid = entity_list.GetRaidByBotName(this->GetName()); + uint32 r_group = raid->GetGroup(GetName()); + if (!raid) + return false; + + if (!tar) { + return false; + } + + if (!AI_HasSpells()) + return false; + + if (iChance < 100) { + if (zone->random.Int(0, 100) > iChance) { + return false; + } + } + + if (tar->GetAppearance() == eaDead) { + if ((tar->IsClient() && tar->CastToClient()->GetFeigned()) || tar->IsBot()) { + // do nothing + } + else { + return false; + } + } + + uint8 botClass = GetClass(); + uint8 botLevel = GetLevel(); + + bool checked_los = false; //we do not check LOS until we are absolutely sure we need to, and we only do it once. + + bool castedSpell = false; + + BotSpell botSpell; + botSpell.SpellId = 0; + botSpell.SpellIndex = 0; + botSpell.ManaCost = 0; + + switch (iSpellTypes) { + case SpellType_Mez: { + if (tar->GetBodyType() != BT_Giant) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + //TODO + //Check if single target or AoE mez is best + //if (TARGETS ON MT IS => 3 THEN botSpell = AoEMez) + //if (TARGETS ON MT IS <= 2 THEN botSpell = BestMez) + + botSpell = GetBestBotSpellForMez(this); + + if (botSpell.SpellId == 0) + break; + + Mob* addMob = GetFirstIncomingMobToMez(this, botSpell); + + if (!addMob) { + //Say("!addMob."); + break; + } + + if (!(!addMob->IsImmuneToSpell(botSpell.SpellId, this) && addMob->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) + break; + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, addMob, botSpell.ManaCost); + + if (castedSpell) + BotGroupSay(this, "Attempting to mez %s.", addMob->GetCleanName()); + //raid->RaidBotGroupSay(this, 0, 100, "Attempting to mez %s.", addMob->GetCleanName()); + } + break; + } + case SpellType_Heal: { + if (tar->DontHealMeBefore() < Timer::GetCurrentTime()) { + uint8 hpr = (uint8)tar->GetHPRatio(); + bool hasAggro = false; + bool isPrimaryHealer = false; + + if (this->IsRaidGrouped()) { + isPrimaryHealer = IsGroupHealer(); + } + + if (hpr < 95 || (tar->IsClient() && (hpr < 95)) || (botClass == BARD)) { + if (tar->GetClass() == NECROMANCER) { + // Give necromancers a chance to go lifetap something or cleric can spend too much mana on a necro + if (hpr >= 40) { + break; + } + } + + if (tar->GetClass() == SHAMAN) { + // Give shaman the chance to canni without wasting the cleric's mana + if (hpr >= 80) { + break; + } + } + + // Evaluate the situation + if ((IsEngaged()) && ((botClass == CLERIC) || (botClass == DRUID) || (botClass == SHAMAN) || (botClass == PALADIN))) { + if (tar->GetTarget() && tar->GetTarget()->GetHateTop() && tar->GetTarget()->GetHateTop() == tar) { + hasAggro = true; + } + + if (hpr < 35) { + botSpell = GetBestBotSpellForFastHeal(this); + } + else if (hpr >= 35 && hpr < 70) { + if (GetNumberNeedingHealedInRaidGroup(60, false) >= 3) + botSpell = GetBestBotSpellForGroupHeal(this); + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForPercentageHeal(this); + } + else if (hpr >= 70 && hpr < 95) { + if (GetNumberNeedingHealedInRaidGroup(80, false) >= 3) + botSpell = GetBestBotSpellForGroupHealOverTime(this); + + if (hasAggro) + botSpell = GetBestBotSpellForPercentageHeal(this); + } + else { + if (!tar->FindType(SE_HealOverTime)) + botSpell = GetBestBotSpellForHealOverTime(this); + } + } + else if ((botClass == CLERIC) || (botClass == DRUID) || (botClass == SHAMAN) || (botClass == PALADIN)) { + if (GetNumberNeedingHealedInRaidGroup(40, true) >= 2) { + botSpell = GetBestBotSpellForGroupCompleteHeal(this); + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForGroupHeal(this); + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForGroupHealOverTime(this); + + if (hpr < 40) { + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForPercentageHeal(this); + } + } + else if (GetNumberNeedingHealedInRaidGroup(60, true) >= 2) { + botSpell = GetBestBotSpellForGroupHeal(this); + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForGroupHealOverTime(this); + + if (hpr < 40) { + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForPercentageHeal(this); + } + } + else if (hpr < 40) + botSpell = GetBestBotSpellForPercentageHeal(this); + else if (hpr >= 40 && hpr < 75) + botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + else { + if (hpr < 90 && !tar->FindType(SE_HealOverTime)) + botSpell = GetBestBotSpellForHealOverTime(this); + } + } + else { + float hpRatioToCast = 0.0f; + + switch (this->GetBotStance()) { + case EQ::constants::stanceEfficient: + case EQ::constants::stanceAggressive: + hpRatioToCast = isPrimaryHealer ? 90.0f : 50.0f; + break; + case EQ::constants::stanceBalanced: + hpRatioToCast = isPrimaryHealer ? 95.0f : 75.0f; + break; + case EQ::constants::stanceReactive: + hpRatioToCast = isPrimaryHealer ? 100.0f : 90.0f; + break; + case EQ::constants::stanceBurn: + case EQ::constants::stanceBurnAE: + hpRatioToCast = isPrimaryHealer ? 75.0f : 25.0f; + break; + default: + hpRatioToCast = isPrimaryHealer ? 100.0f : 0.0f; + break; + } + + //If we're at specified mana % or below, don't heal as hybrid + if (tar->GetHPRatio() <= hpRatioToCast) + botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + } + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForRegularSingleTargetHeal(this); + + if (botSpell.SpellId == 0) + botSpell = GetFirstBotSpellForSingleTargetHeal(this); + + if (botSpell.SpellId == 0 && botClass == BARD) { + botSpell = GetFirstBotSpellBySpellType(this, SpellType_Heal); + } + + // If there is still no spell id, then there isn't going to be one so we are done + if (botSpell.SpellId == 0) + break; + + // Can we cast this spell on this target? + if (!(spells[botSpell.SpellId].target_type == ST_GroupTeleport || spells[botSpell.SpellId].target_type == ST_Target || tar == this) + && !(tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) + break; + + uint32 TempDontHealMeBeforeTime = tar->DontHealMeBefore(); + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontHealMeBeforeTime); + + if (castedSpell) { + /*if(TempDontHealMeBeforeTime != tar->DontHealMeBefore()) + tar->SetDontHealMeBefore(TempDontHealMeBeforeTime); + + // For non-HoT heals, do a 4 second delay + // TODO: Replace this code with logic that calculates the delay based on number of clerics in rotation + // and ignores heals for anyone except the main tank + if(!IsHealOverTimeSpell(botSpell.SpellId)) { + if(IsCompleteHealSpell(botSpell.SpellId)) { + // Complete Heal 4 second rotation + tar->SetDontHealMeBefore(Timer::GetCurrentTime() + 4000); + } + else { + tar->SetDontHealMeBefore(Timer::GetCurrentTime() + 1000); + } + }*/ + if (botClass != BARD) { + if (IsGroupSpell(botSpell.SpellId)) { + if (IsRaidGrouped()) { + uint32 r_group = raid->GetGroup(GetName()); + BotGroupSay(this, "Casting %s.", spells[botSpell.SpellId].name); + //raid->RaidBotGroupSay(this, 0, 100, "Casting %s.", spells[botSpell.SpellId].name); + std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); + //for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { + for (int i = 0; i < raid_group_members.size(); ++i){ + if (raid_group_members.at(i).member && !raid_group_members.at(i).member->qglobal) { + raid_group_members.at(i).member->SetDontHealMeBefore(Timer::GetCurrentTime() + 1000); + } + } + } + } + else { + if (tar != this) //we don't need spam of bots healing themselves + BotGroupSay(this, "Casting %s on %s", spells[botSpell.SpellId].name, tar->GetCleanName()); + tar->SetDontHealMeBefore(Timer::GetCurrentTime() + 2000); + } + } + } + } + } + break; + } + case SpellType_Root: { + if (!tar->IsRooted() && tar->DontRootMeBefore() < Timer::GetCurrentTime()) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + // TODO: If there is a ranger in the group then don't allow root spells + + botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + + if (botSpell.SpellId == 0) + break; + + if (tar->CanBuffStack(botSpell.SpellId, botLevel, true) == 0) + break; + + uint32 TempDontRootMeBefore = tar->DontRootMeBefore(); + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontRootMeBefore); + + if (TempDontRootMeBefore != tar->DontRootMeBefore()) + tar->SetDontRootMeBefore(TempDontRootMeBefore); + } + break; + } + case SpellType_Buff: { + if (tar->DontBuffMeBefore() < Timer::GetCurrentTime()) { + std::list buffSpellList = GetBotSpellsBySpellType(this, SpellType_Buff); + + for (std::list::iterator itr = buffSpellList.begin(); itr != buffSpellList.end(); ++itr) { + BotSpell selectedBotSpell = *itr; + + if (selectedBotSpell.SpellId == 0) + continue; + + // no buffs with illusions.. use #bot command to cast illusions + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_Illusion) && tar != this) + continue; + + //no teleport spells use #bot command to cast teleports + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_Teleport) || IsEffectInSpell(selectedBotSpell.SpellId, SE_Succor)) + continue; + + // can not cast buffs for your own pet only on another pet that isn't yours + if ((spells[selectedBotSpell.SpellId].target_type == ST_Pet) && (tar != this->GetPet())) + continue; + + // Validate target + + if (!((spells[selectedBotSpell.SpellId].target_type == ST_Target || spells[selectedBotSpell.SpellId].target_type == ST_Pet || tar == this || + spells[selectedBotSpell.SpellId].target_type == ST_Group || spells[selectedBotSpell.SpellId].target_type == ST_GroupTeleport || + (botClass == BARD && spells[selectedBotSpell.SpellId].target_type == ST_AEBard)) + && !tar->IsImmuneToSpell(selectedBotSpell.SpellId, this) + && (tar->CanBuffStack(selectedBotSpell.SpellId, botLevel, true) >= 0))) { + continue; + } + + // Put the zone levitate and movement check here since bots are able to bypass the client casting check + if ((IsEffectInSpell(selectedBotSpell.SpellId, SE_Levitate) && !zone->CanLevitate()) + || (IsEffectInSpell(selectedBotSpell.SpellId, SE_MovementSpeed) && !zone->CanCastOutdoor())) { + if (botClass != BARD || !IsSpellUsableThisZoneType(selectedBotSpell.SpellId, zone->GetZoneType())) { + continue; + } + } + + switch (tar->GetArchetype()) + { + case ARCHETYPE_CASTER: + //TODO: probably more caster specific spell effects in here + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_AttackSpeed) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ATK) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_STR) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ReverseDS)) + { + continue; + } + break; + case ARCHETYPE_MELEE: + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_IncreaseSpellHaste) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ManaPool) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_CastingLevel) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ManaRegen_v2) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_CurrentMana)) + { + continue; + } + break; + case ARCHETYPE_HYBRID: + //Hybrids get all buffs + default: + break; + } + + if (botClass == ENCHANTER && IsEffectInSpell(selectedBotSpell.SpellId, SE_Rune)) + { + float manaRatioToCast = 75.0f; + + switch (this->GetBotStance()) { + case EQ::constants::stanceEfficient: + manaRatioToCast = 90.0f; + break; + case EQ::constants::stanceBalanced: + case EQ::constants::stanceAggressive: + manaRatioToCast = 75.0f; + break; + case EQ::constants::stanceReactive: + case EQ::constants::stanceBurn: + case EQ::constants::stanceBurnAE: + manaRatioToCast = 50.0f; + break; + default: + manaRatioToCast = 75.0f; + break; + } + + //If we're at specified mana % or below, don't rune as enchanter + if (this->GetManaRatio() <= manaRatioToCast) + break; + } + + if (CheckSpellRecastTimers(this, itr->SpellIndex)) + { + + uint32 TempDontBuffMeBefore = tar->DontBuffMeBefore(); + + castedSpell = AIDoSpellCast(selectedBotSpell.SpellIndex, tar, selectedBotSpell.ManaCost, &TempDontBuffMeBefore); + + if (TempDontBuffMeBefore != tar->DontBuffMeBefore()) + tar->SetDontBuffMeBefore(TempDontBuffMeBefore); + } + + if (castedSpell) + break; + } + } + break; + } + case SpellType_Escape: { + uint8 hpr = (uint8)GetHPRatio(); + bool mayGetAggro = false; + +#ifdef IPC + if (hpr <= 5 || (IsNPC() && CastToNPC()->IsInteractive() && tar != this)) +#else + if (hpr > 15 && ((botClass == WIZARD) || (botClass == ENCHANTER) || (botClass == RANGER))) + mayGetAggro = HasOrMayGetAggro(); //classes have hate reducing spells + + if (hpr <= 15 || mayGetAggro) +#endif + { + botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + + if (botSpell.SpellId == 0) + break; + + if (IsInvulnerabilitySpell(botSpell.SpellId)) + tar = this; //target self for invul type spells + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + break; + } + case SpellType_Nuke: { + if ((tar->GetHPRatio() <= 95.0f) || ((botClass == BARD) || (botClass == SHAMAN) || (botClass == ENCHANTER))) + { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + if (botClass == CLERIC || botClass == ENCHANTER) + { + float manaRatioToCast = 75.0f; + + switch (this->GetBotStance()) { + case EQ::constants::stanceEfficient: + manaRatioToCast = 90.0f; + break; + case EQ::constants::stanceBalanced: + manaRatioToCast = 75.0f; + break; + case EQ::constants::stanceReactive: + case EQ::constants::stanceAggressive: + manaRatioToCast = 50.0f; + break; + case EQ::constants::stanceBurn: + case EQ::constants::stanceBurnAE: + manaRatioToCast = 25.0f; + break; + default: + manaRatioToCast = 50.0f; + break; + } + + //If we're at specified mana % or below, don't nuke as cleric or enchanter + if (this->GetManaRatio() <= manaRatioToCast) + break; + } + + if (botClass == MAGICIAN || botClass == SHADOWKNIGHT || botClass == NECROMANCER || botClass == PALADIN || botClass == RANGER || botClass == DRUID || botClass == CLERIC) { + if (tar->GetBodyType() == BT_Undead || tar->GetBodyType() == BT_SummonedUndead || tar->GetBodyType() == BT_Vampire) + botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Undead); + else if (tar->GetBodyType() == BT_Summoned || tar->GetBodyType() == BT_Summoned2 || tar->GetBodyType() == BT_Summoned3) + botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Summoned); + } + + if (botClass == PALADIN || botClass == DRUID || botClass == CLERIC || botClass == ENCHANTER || botClass == WIZARD) { + if (botSpell.SpellId == 0) { + uint8 stunChance = (tar->IsCasting() ? 30 : 15); + + if (botClass == PALADIN) + stunChance = 50; + + if (!tar->GetSpecialAbility(UNSTUNABLE) && !tar->IsStunned() && (zone->random.Int(1, 100) <= stunChance)) { + botSpell = GetBestBotSpellForStunByTargetType(this, ST_Target); + } + } + } + + if (botClass == WIZARD && botSpell.SpellId == 0) { + botSpell = GetBestBotWizardNukeSpellByTargetResists(this, tar); + } + + if (botSpell.SpellId == 0) + botSpell = GetBestBotSpellForNukeByTargetType(this, ST_Target); + + if (botSpell.SpellId == 0) + break; + + if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0))) + break; + + if (IsFearSpell(botSpell.SpellId)) { + // don't let fear cast if the npc isn't snared or rooted + if (tar->GetSnaredAmount() == -1) { + if (!tar->IsRooted()) + break; + } + } + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + break; + } + case SpellType_Dispel: { + if (tar->GetHPRatio() > 95.0f) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + + if (botSpell.SpellId == 0) + break; + + // TODO: Check target to see if there is anything to dispel + + if (tar->CountDispellableBuffs() > 0) { + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + } + break; + } + case SpellType_Pet: { + //keep mobs from recasting pets when they have them. + if (!IsPet() && !GetPetID() && !IsBotCharmer()) { + if (botClass == WIZARD) { + auto buffs_max = GetMaxBuffSlots(); + auto my_buffs = GetBuffs(); + int familiar_buff_slot = -1; + if (buffs_max && my_buffs) { + for (int index = 0; index < buffs_max; ++index) { + if (IsEffectInSpell(my_buffs[index].spellid, SE_Familiar)) { + MakePet(my_buffs[index].spellid, spells[my_buffs[index].spellid].teleport_zone); + familiar_buff_slot = index; + break; + } + } + } + if (GetPetID()) + break; + + if (familiar_buff_slot >= 0) { + BuffFadeBySlot(familiar_buff_slot); + break; + } + + botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); + } + else if (botClass == MAGICIAN) { + botSpell = GetBestBotMagicianPetSpell(this); + } + else { + botSpell = GetFirstBotSpellBySpellType(this, SpellType_Pet); + } + + if (botSpell.SpellId == 0) + break; + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + break; + } + case SpellType_InCombatBuff: { + + if (botClass == SHAMAN) { + checked_los = true; + + std::list inCombatBuffList = GetBotSpellsBySpellType(this, SpellType_InCombatBuff); + + for (std::list::iterator itr = inCombatBuffList.begin(); itr != inCombatBuffList.end(); ++itr) { + BotSpell selectedBotSpell = *itr; + + if (selectedBotSpell.SpellId == 0) + continue; + + if (CheckSpellRecastTimers(this, itr->SpellIndex)) + { + if (!(!tar->IsImmuneToSpell(selectedBotSpell.SpellId, this) && (spells[selectedBotSpell.SpellId].buff_duration < 1 || tar->CanBuffStack(selectedBotSpell.SpellId, botLevel, true) >= 0))) + continue; + + //short duration buffs or other buffs only to be cast during combat. + if (IsSelfConversionSpell(selectedBotSpell.SpellId)) { + if (GetManaRatio() > 90.0f || GetHPRatio() < 50.0f || GetHPRatio() < (GetManaRatio() + 10.0f)) + break; //don't cast if low hp, lots of mana, or if mana is higher than hps + } + + castedSpell = AIDoSpellCast(selectedBotSpell.SpellIndex, tar, selectedBotSpell.ManaCost); + } + + if (castedSpell) + break; + } + } + else if (botClass == BARD) { + if (tar->DontBuffMeBefore() < Timer::GetCurrentTime()) { + std::list inCombatBuffList = GetBotSpellsBySpellType(this, SpellType_InCombatBuff); + + for (std::list::iterator itr = inCombatBuffList.begin(); itr != inCombatBuffList.end(); ++itr) { + BotSpell selectedBotSpell = *itr; + + if (selectedBotSpell.SpellId == 0) + continue; + + if (CheckSpellRecastTimers(this, itr->SpellIndex)) { + uint32 TempDontBuffMeBefore = tar->DontBuffMeBefore(); + + // no buffs with illusions.. use #bot command to cast illusions + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_Illusion) && tar != this) + continue; + + //no teleport spells use #bot command to cast teleports + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_Teleport) || IsEffectInSpell(selectedBotSpell.SpellId, SE_Succor)) + continue; + + // can not cast buffs for your own pet only on another pet that isn't yours + if ((spells[selectedBotSpell.SpellId].target_type == ST_Pet) && (tar != this->GetPet())) + continue; + + // Validate target + + if (!((spells[selectedBotSpell.SpellId].target_type == ST_Target || spells[selectedBotSpell.SpellId].target_type == ST_Pet || tar == this || + spells[selectedBotSpell.SpellId].target_type == ST_Group || spells[selectedBotSpell.SpellId].target_type == ST_GroupTeleport || + (botClass == BARD && spells[selectedBotSpell.SpellId].target_type == ST_AEBard)) + && !tar->IsImmuneToSpell(selectedBotSpell.SpellId, this) + && (tar->CanBuffStack(selectedBotSpell.SpellId, botLevel, true) >= 0))) { + continue; + } + + // Put the zone levitate and movement check here since bots are able to bypass the client casting check + if ((IsEffectInSpell(selectedBotSpell.SpellId, SE_Levitate) && !zone->CanLevitate()) + || (IsEffectInSpell(selectedBotSpell.SpellId, SE_MovementSpeed) && !zone->CanCastOutdoor())) { + if (!IsSpellUsableThisZoneType(selectedBotSpell.SpellId, zone->GetZoneType())) { + continue; + } + } + + if (!IsGroupSpell(selectedBotSpell.SpellId)) { + //Only check archetype if song is not a group spell + switch (tar->GetArchetype()) { + case ARCHETYPE_CASTER: + //TODO: probably more caster specific spell effects in here + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_AttackSpeed) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ATK) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_STR) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ReverseDS)) + { + continue; + } + break; + case ARCHETYPE_MELEE: + if (IsEffectInSpell(selectedBotSpell.SpellId, SE_IncreaseSpellHaste) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ManaPool) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_CastingLevel) || IsEffectInSpell(selectedBotSpell.SpellId, SE_ManaRegen_v2) || + IsEffectInSpell(selectedBotSpell.SpellId, SE_CurrentMana)) + { + continue; + } + break; + case ARCHETYPE_HYBRID: + //Hybrids get all buffs + default: + break; + } + } + + castedSpell = AIDoSpellCast(selectedBotSpell.SpellIndex, tar, selectedBotSpell.ManaCost, &TempDontBuffMeBefore); + + if (TempDontBuffMeBefore != tar->DontBuffMeBefore()) + tar->SetDontBuffMeBefore(TempDontBuffMeBefore); + } + + if (castedSpell) + break; + } + } + } + break; + } + case SpellType_Lifetap: { + if (GetHPRatio() < 90.0f) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + + if (botSpell.SpellId == 0) + break; + + if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0))) + break; + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + break; + } + case SpellType_Snare: { + if (tar->DontSnareMeBefore() < Timer::GetCurrentTime()) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + botSpell = GetFirstBotSpellBySpellType(this, iSpellTypes); + + if (botSpell.SpellId == 0) + break; + + if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) + break; + + uint32 TempDontSnareMeBefore = tar->DontSnareMeBefore(); + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontSnareMeBefore); + + if (TempDontSnareMeBefore != tar->DontSnareMeBefore()) + tar->SetDontSnareMeBefore(TempDontSnareMeBefore); + } + break; + } + case SpellType_DOT: { + if ((tar->GetHPRatio() <= 98.0f) && (tar->DontDotMeBefore() < Timer::GetCurrentTime()) && (tar->GetHPRatio() > 15.0f)) { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + if (GetClass() == BARD) { + std::list dotList = GetPrioritizedBotSpellsBySpellType(this, SpellType_DOT); + + const int maxDotSelect = 5; + int dotSelectCounter = 0; + + for (std::list::iterator itr = dotList.begin(); itr != dotList.end(); ++itr) { + BotSpell selectedBotSpell = *itr; + + if (selectedBotSpell.SpellId == 0) + continue; + + if (CheckSpellRecastTimers(this, itr->SpellIndex)) + { + + if (!(!tar->IsImmuneToSpell(selectedBotSpell.SpellId, this) && tar->CanBuffStack(selectedBotSpell.SpellId, botLevel, true) >= 0)) + continue; + + uint32 TempDontDotMeBefore = tar->DontDotMeBefore(); + + castedSpell = AIDoSpellCast(selectedBotSpell.SpellIndex, tar, selectedBotSpell.ManaCost, &TempDontDotMeBefore); + + if (TempDontDotMeBefore != tar->DontDotMeBefore()) + tar->SetDontDotMeBefore(TempDontDotMeBefore); + } + + dotSelectCounter++; + + if ((dotSelectCounter == maxDotSelect) || castedSpell) + break; + } + } + else { + std::list dotList = GetBotSpellsBySpellType(this, SpellType_DOT); + + const int maxDotSelect = 5; + int dotSelectCounter = 0; + + for (std::list::iterator itr = dotList.begin(); itr != dotList.end(); ++itr) { + BotSpell selectedBotSpell = *itr; + + if (selectedBotSpell.SpellId == 0) + continue; + + if (CheckSpellRecastTimers(this, itr->SpellIndex)) + { + + if (!(!tar->IsImmuneToSpell(selectedBotSpell.SpellId, this) && tar->CanBuffStack(selectedBotSpell.SpellId, botLevel, true) >= 0)) + continue; + + uint32 TempDontDotMeBefore = tar->DontDotMeBefore(); + + castedSpell = AIDoSpellCast(selectedBotSpell.SpellIndex, tar, selectedBotSpell.ManaCost, &TempDontDotMeBefore); + + if (TempDontDotMeBefore != tar->DontDotMeBefore()) + tar->SetDontDotMeBefore(TempDontDotMeBefore); + } + + dotSelectCounter++; + + if ((dotSelectCounter == maxDotSelect) || castedSpell) + break; + } + } + } + break; + } + case SpellType_Slow: { + if (tar->GetHPRatio() <= 99.0f) { + + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + switch (botClass) { + case BARD: { + // probably needs attackable check + std::list botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_Slow); + for (auto iter : botSongList) { + if (!iter.SpellId) + continue; + if (!CheckSpellRecastTimers(this, iter.SpellIndex)) + continue; + if (spells[iter.SpellId].zone_type != -1 && zone->GetZoneType() != -1 && spells[iter.SpellId].zone_type != zone->GetZoneType()) // is this bit or index? + continue; + if (spells[iter.SpellId].target_type != ST_Target) + continue; + if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) + continue; + + castedSpell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); + if (castedSpell) + break; + } + + break; + } + case ENCHANTER: { + botSpell = GetBestBotSpellForMagicBasedSlow(this); + break; + } + case SHAMAN: + case BEASTLORD: { + botSpell = GetBestBotSpellForDiseaseBasedSlow(this); + + if (botSpell.SpellId == 0 || ((tar->GetMR() - 50) < (tar->GetDR() + spells[botSpell.SpellId].resist_difficulty))) + botSpell = GetBestBotSpellForMagicBasedSlow(this); + break; + } + } + + if (botSpell.SpellId == 0) + break; + + if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0)) + break; + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + + if (castedSpell && GetClass() != BARD) + BotGroupSay(this, "Attempting to slow %s.", tar->GetCleanName()); + } + break; + } + case SpellType_Debuff: { + if ((tar->GetHPRatio() <= 99.0f) || ((botClass == BARD) || (botClass == SHAMAN) || (botClass == ENCHANTER) || (botClass == DRUID)) && (tar->GetHPRatio() > 40.0f)) + { + if (!checked_los) { + if (!CheckLosFN(tar)) + break; //cannot see target... we assume that no spell is going to work since we will only be casting detrimental spells in this call + + checked_los = true; + } + + botSpell = GetBestBotSpellForResistDebuff(this, tar); + + if (botSpell.SpellId == 0) + botSpell = GetDebuffBotSpell(this, tar); + + if (botSpell.SpellId == 0) + break; + + if (!(!tar->IsImmuneToSpell(botSpell.SpellId, this) && (tar->CanBuffStack(botSpell.SpellId, botLevel, true) >= 0))) + break; + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost); + } + break; + } + case SpellType_Cure: { + if (GetNeedsCured(tar) && (tar->DontCureMeBefore() < Timer::GetCurrentTime()) && !(GetNumberNeedingHealedInRaidGroup(25, false) > 0) && !(GetNumberNeedingHealedInRaidGroup(40, false) > 2)) + { + botSpell = GetBestBotSpellForCure(this, tar); + + if (botSpell.SpellId == 0) + break; + + uint32 TempDontCureMeBeforeTime = tar->DontCureMeBefore(); + + castedSpell = AIDoSpellCast(botSpell.SpellIndex, tar, botSpell.ManaCost, &TempDontCureMeBeforeTime); + + if (castedSpell) { + if (botClass != BARD) { + if (IsGroupSpell(botSpell.SpellId)) { + if (this->IsRaidGrouped()) { + if (r_group) { + std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); + for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { + if (iter->member && !iter->member->qglobal) { + if (TempDontCureMeBeforeTime != tar->DontCureMeBefore()) + iter->member->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); + } + } + } + } + } + else { + if (TempDontCureMeBeforeTime != tar->DontCureMeBefore()) + tar->SetDontCureMeBefore(Timer::GetCurrentTime() + 4000); + } + } + } + } + break; + } + case SpellType_Resurrect: + break; + case SpellType_HateRedux: { + // assumed group member at this point + if (GetClass() == BARD) { + std::list botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_HateRedux); + for (auto iter : botSongList) { + if (!iter.SpellId) + continue; + if (!CheckSpellRecastTimers(this, iter.SpellIndex)) + continue; + if (spells[iter.SpellId].zone_type != -1 && zone->GetZoneType() != -1 && spells[iter.SpellId].zone_type != zone->GetZoneType()) // is this bit or index? + continue; + if (spells[iter.SpellId].target_type != ST_Target) + continue; + if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) + continue; + + castedSpell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); + if (castedSpell) { + BotGroupSay(this, "Attempting to reduce hate on %s.", tar->GetCleanName()); + break; + } + } + } + + break; + } + case SpellType_InCombatBuffSong: { + if (GetClass() != BARD || tar != this) // In-Combat songs can be cast Out-of-Combat in preparation for battle + break; + + std::list botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_InCombatBuffSong); + for (auto iter : botSongList) { + if (!iter.SpellId) + continue; + if (!CheckSpellRecastTimers(this, iter.SpellIndex)) + continue; + if (spells[iter.SpellId].zone_type != -1 && zone->GetZoneType() != -1 && spells[iter.SpellId].zone_type != zone->GetZoneType()) // is this bit or index? + continue; + switch (spells[iter.SpellId].target_type) { + case ST_AEBard: + case ST_AECaster: + case ST_GroupTeleport: + case ST_Group: + case ST_Self: + break; + default: + continue; + } + if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) + continue; + + castedSpell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); + if (castedSpell) + break; + } + + break; + } + case SpellType_OutOfCombatBuffSong: { + if (GetClass() != BARD || tar != this || IsEngaged()) // Out-of-Combat songs can not be cast in combat + break; + + std::list botSongList = GetPrioritizedBotSpellsBySpellType(this, SpellType_OutOfCombatBuffSong); + for (auto iter : botSongList) { + if (!iter.SpellId) + continue; + if (!CheckSpellRecastTimers(this, iter.SpellIndex)) + continue; + if (spells[iter.SpellId].zone_type != -1 && zone->GetZoneType() != -1 && spells[iter.SpellId].zone_type != zone->GetZoneType()) // is this bit or index? + continue; + switch (spells[iter.SpellId].target_type) { + case ST_AEBard: + case ST_AECaster: + case ST_GroupTeleport: + case ST_Group: + case ST_Self: + break; + default: + continue; + } + if (tar->CanBuffStack(iter.SpellId, botLevel, true) < 0) + continue; + + castedSpell = AIDoSpellCast(iter.SpellIndex, tar, iter.ManaCost); + if (castedSpell) + break; + } + + break; + } + case SpellType_PreCombatBuff: { + break; + } + case SpellType_PreCombatBuffSong: { + break; + } + default: + break; + } + + return castedSpell; +} + +void Raid::RaidBotGroupSay(Bot* b, uint8 language, uint8 lang_skill, const char* msg, ...) +{ + if (!b) + return; + + char buf[1000]; + va_list ap; + va_start(ap, msg); + vsnprintf(buf, 1000, msg, ap); + va_end(ap); + + uint32 groupToUse = GetGroup(b->GetName()); + + if (groupToUse > 11) + return; + + auto pack = new ServerPacket(ServerOP_RaidGroupSay, sizeof(ServerRaidMessage_Struct) + strlen(msg) + 1); + ServerRaidMessage_Struct* rga = (ServerRaidMessage_Struct*)pack->pBuffer; + rga->rid = GetID(); + rga->gid = groupToUse; + rga->language = language; + rga->lang_skill = lang_skill; + strn0cpy(rga->from, b->GetName(), 64); + + strcpy(rga->message, buf); // this is safe because we are allocating enough space for the entire msg above + + worldserver.SendPacket(pack); + //safe_delete(pack); +} + +uint8 Bot::GetNumberNeedingHealedInRaidGroup(uint8 hpr, bool includePets) { + uint8 needHealed = 0; + Raid* raid = nullptr; + raid = entity_list.GetRaidByBotName(this->GetName()); + uint32 r_group = raid->GetGroup(this->GetName()); + std::vector raid_group_members = raid->GetRaidGroupMembers(r_group); + //for (std::vector::iterator iter = raid_group_members.begin(); iter != raid_group_members.end(); ++iter) { + for (int i = 0; i< raid_group_members.size(); ++i) { + if (raid_group_members.at(i).member && !raid_group_members.at(i).member->qglobal) { + if (raid_group_members.at(i).member->GetHPRatio() <= hpr) + needHealed++; + + if (includePets) { + if (raid_group_members.at(i).member->GetPet() && raid_group_members.at(i).member->GetPet()->GetHPRatio() <= hpr) + needHealed++; + } + } + } + return needHealed; +} +#endif diff --git a/zone/bot_raid.h b/zone/bot_raid.h new file mode 100644 index 000000000..66d55bab8 --- /dev/null +++ b/zone/bot_raid.h @@ -0,0 +1,47 @@ +/* EQEMu: Everquest Server Emulator + Copyright (C) 2001-2016 EQEMu Development Team (http://eqemulator.org) + + 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; version 2 of the License. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY except by those people which sell it, which + are required to give you total support for your newly bought product; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +#ifndef BOT_RAID_H +#define BOT_RAID_H + +#ifdef BOTS + +#include "bot_structs.h" +#include "mob.h" +#include "client.h" +#include "pets.h" +#include "heal_rotation.h" +#include "groups.h" +#include "corpse.h" +#include "zonedb.h" +#include "zone_store.h" +#include "string_ids.h" +#include "../common/misc_functions.h" +#include "../common/global_define.h" +#include "guild_mgr.h" +#include "worldserver.h" + +#include + +extern WorldServer worldserver; + +//void Bot::PetAIProcess_Raid(); + +#endif // BOTS + +#endif // BOT_RAID_H diff --git a/zone/botspellsai.cpp b/zone/botspellsai.cpp index 2653af032..0cd6a2d52 100644 --- a/zone/botspellsai.cpp +++ b/zone/botspellsai.cpp @@ -3,7 +3,7 @@ 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; version 2 of the License. + the Free Software Foundation; version 2 ogroupf the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY except by those people which sell it, which @@ -31,6 +31,13 @@ bool Bot::AICastSpell(Mob* tar, uint8 iChance, uint32 iSpellTypes) { + // Bot AI + Raid* raid = entity_list.GetRaidByBotName(this->GetName()); + if (raid) { + return AICastSpell_Raid(tar, iChance, iSpellTypes); + //return true; + } + if (!tar) { return false; } diff --git a/zone/client.cpp b/zone/client.cpp index 6324db2a0..de602ada7 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -807,9 +807,10 @@ void Client::QueuePacket(const EQApplicationPacket* app, bool ack_req, CLIENT_CO // todo: save packets for later use AddPacket(app, ack_req); } - else - if(eqs) - eqs->QueuePacket(app, ack_req); + else if (eqs) + { + eqs->QueuePacket(app, ack_req); + } } void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CONN_STATUS required_state) { @@ -820,7 +821,7 @@ void Client::FastQueuePacket(EQApplicationPacket** app, bool ack_req, CLIENT_CON return; } else { - if(eqs) + if(eqs) eqs->FastQueuePacket((EQApplicationPacket **)app, ack_req); else if (app && (*app)) delete *app; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index ee66f02b2..c3760cdbe 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -25,6 +25,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #include #include #include +#include "bot.h" #ifdef _WINDOWS #define snprintf _snprintf @@ -72,6 +73,7 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #ifdef BOTS #include "bot.h" +#include "bot_command.h" #endif extern QueryServ* QServ; @@ -588,6 +590,31 @@ void Client::CompleteConnect() if (raid) { SetRaidGrouped(true); raid->LearnMembers(); +#ifdef BOTS + std::list bots_list; + database.botdb.LoadBotsList(this->CharacterID(), bots_list); + std::vector r_members = raid->GetMembers(); + for (const RaidMember& iter : r_members) { + if (iter.membername) { + for (const BotsAvailableList& b_iter : bots_list) + { + if (strcmp(iter.membername, b_iter.Name) == 0) + { + char buffer[71] = "^spawn "; + strcat(buffer, iter.membername); + bot_command_real_dispatch(this, buffer); + Bot* b = entity_list.GetBotByBotName(iter.membername); + if (b) + { + b->SetRaidGrouped(true); + b->p_raid_instance = raid; + //b->SetFollowID(this->GetID()); + } + } + } + } + } +#endif raid->VerifyRaid(); raid->GetRaidDetails(); /* @@ -6966,7 +6993,12 @@ void Client::Handle_OP_GroupInvite2(const EQApplicationPacket *app) } #ifdef BOTS else if (Invitee->IsBot()) { - Bot::ProcessBotGroupInvite(this, std::string(Invitee->GetName())); + Client* inviter = entity_list.GetClientByName(gis->inviter_name); + //Bot* invitee = entity_list.GetBotByBotName(gis->invitee_name); + if (inviter->IsRaidGrouped()) + Bot::ProcessRaidInvite(Invitee->CastToBot(), inviter); + else + Bot::ProcessBotGroupInvite(this, std::string(Invitee->GetName())); } #endif } @@ -11405,7 +11437,7 @@ void Client::Handle_OP_QueryUCSServerStatus(const EQApplicationPacket *app) } } -void Client::Handle_OP_RaidCommand(const EQApplicationPacket *app) +void Client::Handle_OP_RaidCommand(const EQApplicationPacket* app) { if (app->size < sizeof(RaidGeneral_Struct)) { LogError("Wrong size: OP_RaidCommand, size=[{}], expected at least [{}]", app->size, sizeof(RaidGeneral_Struct)); @@ -11413,218 +11445,256 @@ void Client::Handle_OP_RaidCommand(const EQApplicationPacket *app) return; } - RaidGeneral_Struct *raid_command_packet = (RaidGeneral_Struct*)app->pBuffer; + RaidGeneral_Struct* raid_command_packet = (RaidGeneral_Struct*)app->pBuffer; switch (raid_command_packet->action) { - case RaidCommandInviteIntoExisting: - case RaidCommandInvite: { + case RaidCommandInviteIntoExisting: + case RaidCommandInvite: { - Client *player_to_invite = entity_list.GetClientByName(raid_command_packet->player_name); +#ifdef BOTS - if (!player_to_invite) - break; + Bot* player_to_invite = nullptr; + Client* player_to_invite_owner = nullptr; - Group *player_to_invite_group = player_to_invite->GetGroup(); + if (entity_list.GetBotByBotName(raid_command_packet->player_name)) { + Bot* player_to_invite = entity_list.GetBotByBotName(raid_command_packet->player_name); + Client* player_to_invite_owner = player_to_invite->GetOwner()->CastToClient(); + Group* player_to_invite_group = player_to_invite->GetGroup(); - if (player_to_invite->HasRaid()) { - Message(Chat::Red, "%s is already in a raid.", player_to_invite->GetName()); + if (!player_to_invite) { break; } - if (player_to_invite_group && player_to_invite_group->IsGroupMember(this)) { - MessageString(Chat::Red, ALREADY_IN_PARTY); + //Not allowed: Invite a bot that is already within a raid. + if (player_to_invite->IsRaidGrouped()) { + MessageString(Chat::White, ALREADY_IN_RAID, GetName()); //must invite members not in raid... + return; + } + + // Not allowed: Invite a bot that is not owned by the invitor + if (player_to_invite->IsBot() && + player_to_invite->CastToBot()->GetOwner()->CastToClient()->CharacterID() != + player_to_invite_owner->CharacterID()) { + Message(Chat::Red, "%s is not your Bot. You can only invite your Bots, or players grouped with bots.", player_to_invite->GetName()); + } + + // Not allowed: Invite a bot that is in a group but the bot is not the group leader + if (player_to_invite_group && !player_to_invite_group->IsLeader(player_to_invite->CastToMob())) { + Message(Chat::Red, "You can only invite group leaders or ungrouped bots. Try %s instead.", player_to_invite_group->GetLeader()->GetName()); break; } - if (player_to_invite_group && !player_to_invite_group->IsLeader(player_to_invite)) { - Message(Chat::Red, "You can only invite an ungrouped player or group leader to join your raid."); + Bot::ProcessRaidInvite(player_to_invite, player_to_invite_owner); + break; + } + else + { +#endif + Client* player_to_invite = entity_list.GetClientByName(raid_command_packet->player_name); + + if (!player_to_invite) + break; + + Group* player_to_invite_group = player_to_invite->GetGroup(); + + if (player_to_invite->HasRaid()) { + Message(Chat::Red, "%s is already in a raid.", player_to_invite->GetName()); + break; + } + + if (player_to_invite_group && player_to_invite_group->IsGroupMember(this)) { + MessageString(Chat::Red, ALREADY_IN_PARTY); + break; + } + // Not allowed: Invite a client that is in a group but not the groupleader + if (player_to_invite_group && !player_to_invite_group->IsLeader(player_to_invite)) { + Message(Chat::Red, "You can only invite an ungrouped player or group leader to join your raid."); + break; + } + + /* Send out invite to the client */ + auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidGeneral_Struct)); + RaidGeneral_Struct* raid_command = (RaidGeneral_Struct*)outapp->pBuffer; + + strn0cpy(raid_command->leader_name, raid_command_packet->leader_name, 64); + strn0cpy(raid_command->player_name, raid_command_packet->player_name, 64); + + raid_command->parameter = 0; + raid_command->action = 20; + + player_to_invite->QueuePacket(outapp); + + safe_delete(outapp); + break; } +#ifdef BOTS + } +#endif - /* Send out invite to the client */ - auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidGeneral_Struct)); - RaidGeneral_Struct *raid_command = (RaidGeneral_Struct*)outapp->pBuffer; + case RaidCommandAcceptInvite: { + Client* player_accepting_invite = entity_list.GetClientByName(raid_command_packet->player_name); - strn0cpy(raid_command->leader_name, raid_command_packet->leader_name, 64); - strn0cpy(raid_command->player_name, raid_command_packet->player_name, 64); +#ifdef BOTS + // If the accepting client is in a group with a Bot or the invitor is in a group with a Bot, send the invite to Bot:ProcessRaidInvite + // instead of remaining here. + Bot* b = nullptr; + Client* invitee = entity_list.GetClientByName(raid_command_packet->leader_name); + Client* invitor = entity_list.GetClientByName(raid_command_packet->player_name); + Group* g_invitee = invitee->GetGroup(); + Group* g_invitor = invitor->GetGroup(); - raid_command->parameter = 0; - raid_command->action = 20; + if (invitee && invitee->IsRaidGrouped()) { + invitor->MessageString(Chat::White, ALREADY_IN_RAID, GetName()); //group failed, must invite members not in raid... + return; + } - player_to_invite->QueuePacket(outapp); - - safe_delete(outapp); + bool invitor_has_bot = false; + bool invitee_has_bot = false; + if (g_invitor && g_invitor->IsLeader(invitor)) + { + for (int x = 0; x < 6; x++) + { + if (g_invitor->members[x] && g_invitor->members[x]->IsBot()) + { + b = entity_list.GetBotByBotName(g_invitor->members[x]->GetName()); + invitee_has_bot = true; + } + } + } + if (g_invitee && g_invitee->IsLeader(invitee)) + { + for (int x = 0; x < 6; x++) + { + if (g_invitee->members[x] && g_invitee->members[x]->IsBot()) + { + b = entity_list.GetBotByBotName(g_invitee->members[x]->GetName()); + invitee_has_bot = true; + break; + } + } + } + if (invitor_has_bot || invitee_has_bot) { + Bot::ProcessRaidInvite(invitee, invitor); //two clients with one or both having groups with bots break; } - case RaidCommandAcceptInvite: { - Client *player_accepting_invite = entity_list.GetClientByName(raid_command_packet->player_name); - if (player_accepting_invite) { - if (IsRaidGrouped()) { - player_accepting_invite->MessageString(Chat::White, ALREADY_IN_RAID, GetName()); //group failed, must invite members not in raid... - return; + else if (invitee && invitee->IsBot()) + { + Bot::ProcessRaidInvite(b, player_accepting_invite); //client inviting a bot + break; + } + +#endif + if (player_accepting_invite) { + if (IsRaidGrouped()) { + player_accepting_invite->MessageString(Chat::White, ALREADY_IN_RAID, GetName()); //group failed, must invite members not in raid... + return; + } + Raid* raid = entity_list.GetRaidByClient(player_accepting_invite); + if (raid) { + raid->VerifyRaid(); + Group* group = GetGroup(); + if (group) { + if (group->GroupCount() + raid->RaidCount() > MAX_RAID_MEMBERS) { + player_accepting_invite->Message(Chat::Red, "Invite failed, group invite would create a raid larger than the maximum number of members allowed."); + return; + } } - Raid *raid = entity_list.GetRaidByClient(player_accepting_invite); - if (raid) { - raid->VerifyRaid(); - Group *group = GetGroup(); - if (group) { - if (group->GroupCount() + raid->RaidCount() > MAX_RAID_MEMBERS) { - player_accepting_invite->Message(Chat::Red, "Invite failed, group invite would create a raid larger than the maximum number of members allowed."); - return; - } + else { + if (1 + raid->RaidCount() > MAX_RAID_MEMBERS) { + player_accepting_invite->Message(Chat::Red, "Invite failed, member invite would create a raid larger than the maximum number of members allowed."); + return; } - else { - if (1 + raid->RaidCount() > MAX_RAID_MEMBERS) { - player_accepting_invite->Message(Chat::Red, "Invite failed, member invite would create a raid larger than the maximum number of members allowed."); - return; - } - } - if (group) {//add us all - uint32 free_group_id = raid->GetFreeGroup(); - Client *addClient = nullptr; - for (int x = 0; x < 6; x++) { - if (group->members[x]) { - Client *c = nullptr; - if (group->members[x]->IsClient()) - c = group->members[x]->CastToClient(); - else - continue; + } + if (group) {//add us all + uint32 free_group_id = raid->GetFreeGroup(); + Client* addClient = nullptr; + for (int x = 0; x < 6; x++) { + if (group->members[x]) { + Client* c = nullptr; + if (group->members[x]->IsClient()) + c = group->members[x]->CastToClient(); + else + continue; - if (!addClient) - { - addClient = c; - raid->SetGroupLeader(addClient->GetName()); - } + if (!addClient) + { + addClient = c; + raid->SetGroupLeader(addClient->GetName()); + } - raid->SendRaidCreate(c); - raid->SendMakeLeaderPacketTo(raid->leadername, c); - if (group->IsLeader(group->members[x])) - raid->AddMember(c, free_group_id, false, true); - else - raid->AddMember(c, free_group_id); - raid->SendBulkRaid(c); - if (raid->IsLocked()) { - raid->SendRaidLockTo(c); - } + raid->SendRaidCreate(c); + raid->SendMakeLeaderPacketTo(raid->leadername, c); + if (group->IsLeader(group->members[x])) + raid->AddMember(c, free_group_id, false, true); + else + raid->AddMember(c, free_group_id); + raid->SendBulkRaid(c); + if (raid->IsLocked()) { + raid->SendRaidLockTo(c); } } - group->JoinRaidXTarget(raid); - group->DisbandGroup(true); - raid->GroupUpdate(free_group_id); } - else { - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(this); - raid->SendBulkRaid(this); - if (raid->IsLocked()) { - raid->SendRaidLockTo(this); - } + group->JoinRaidXTarget(raid); + group->DisbandGroup(true); + raid->GroupUpdate(free_group_id); + } + else { + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(this); + raid->SendBulkRaid(this); + if (raid->IsLocked()) { + raid->SendRaidLockTo(this); } } - else + } + else + { + Group* player_invited_group = player_accepting_invite->GetGroup(); + Group* group = GetGroup(); + if (group) //if our target has a group { - Group *player_invited_group = player_accepting_invite->GetGroup(); - Group *group = GetGroup(); - if (group) //if our target has a group - { - raid = new Raid(player_accepting_invite); - entity_list.AddRaid(raid); - raid->SetRaidDetails(); + raid = new Raid(player_accepting_invite); + entity_list.AddRaid(raid); + raid->SetRaidDetails(); - uint32 raid_free_group_id = raid->GetFreeGroup(); + uint32 raid_free_group_id = raid->GetFreeGroup(); - /* If we already have a group then cycle through adding us... */ - if (player_invited_group) { - Client *client_to_be_leader = nullptr; - for (int x = 0; x < 6; x++) { - if (player_invited_group->members[x]) { - if (!client_to_be_leader) { - if (player_invited_group->members[x]->IsClient()) { - client_to_be_leader = player_invited_group->members[x]->CastToClient(); - raid->SetGroupLeader(client_to_be_leader->GetName()); - } - } - if (player_invited_group->IsLeader(player_invited_group->members[x])) { - Client *c = nullptr; - - if (player_invited_group->members[x]->IsClient()) - c = player_invited_group->members[x]->CastToClient(); - else - continue; - - raid->SendRaidCreate(c); - raid->SendMakeLeaderPacketTo(raid->leadername, c); - raid->AddMember(c, raid_free_group_id, true, true, true); - raid->SendBulkRaid(c); - - if (raid->IsLocked()) { - raid->SendRaidLockTo(c); - } - } - else { - Client *c = nullptr; - - if (player_invited_group->members[x]->IsClient()) - c = player_invited_group->members[x]->CastToClient(); - else - continue; - - raid->SendRaidCreate(c); - raid->SendMakeLeaderPacketTo(raid->leadername, c); - raid->AddMember(c, raid_free_group_id); - raid->SendBulkRaid(c); - - if (raid->IsLocked()) { - raid->SendRaidLockTo(c); - } - } - } - } - player_invited_group->JoinRaidXTarget(raid, true); - player_invited_group->DisbandGroup(true); - raid->GroupUpdate(raid_free_group_id); - raid_free_group_id = raid->GetFreeGroup(); - } - else { - raid->SendRaidCreate(player_accepting_invite); - raid->AddMember(player_accepting_invite, 0xFFFFFFFF, true, false, true); - } - - Client *client_to_add = nullptr; - /* Add client to an existing group */ + /* If we already have a group then cycle through adding us... */ + if (player_invited_group) { + Client* client_to_be_leader = nullptr; for (int x = 0; x < 6; x++) { - if (group->members[x]) { - if (!client_to_add) { - if (group->members[x]->IsClient()) { - client_to_add = group->members[x]->CastToClient(); - raid->SetGroupLeader(client_to_add->GetName()); + if (player_invited_group->members[x]) { + if (!client_to_be_leader) { + if (player_invited_group->members[x]->IsClient()) { + client_to_be_leader = player_invited_group->members[x]->CastToClient(); + raid->SetGroupLeader(client_to_be_leader->GetName()); } } - if (group->IsLeader(group->members[x])) { - Client *c = nullptr; + if (player_invited_group->IsLeader(player_invited_group->members[x])) { + Client* c = nullptr; - if (group->members[x]->IsClient()) - c = group->members[x]->CastToClient(); + if (player_invited_group->members[x]->IsClient()) + c = player_invited_group->members[x]->CastToClient(); else continue; raid->SendRaidCreate(c); raid->SendMakeLeaderPacketTo(raid->leadername, c); - raid->AddMember(c, raid_free_group_id, false, true); + raid->AddMember(c, raid_free_group_id, true, true, true); raid->SendBulkRaid(c); if (raid->IsLocked()) { raid->SendRaidLockTo(c); } } - else - { - Client *c = nullptr; + else { + Client* c = nullptr; - if (group->members[x]->IsClient()) - c = group->members[x]->CastToClient(); + if (player_invited_group->members[x]->IsClient()) + c = player_invited_group->members[x]->CastToClient(); else continue; @@ -11639,396 +11709,536 @@ void Client::Handle_OP_RaidCommand(const EQApplicationPacket *app) } } } - group->JoinRaidXTarget(raid); - group->DisbandGroup(true); - + player_invited_group->JoinRaidXTarget(raid, true); + player_invited_group->DisbandGroup(true); raid->GroupUpdate(raid_free_group_id); + raid_free_group_id = raid->GetFreeGroup(); } - /* Target does not have a group */ else { - if (player_invited_group) { + raid->SendRaidCreate(player_accepting_invite); + raid->AddMember(player_accepting_invite, 0xFFFFFFFF, true, false, true); + } - raid = new Raid(player_accepting_invite); + Client* client_to_add = nullptr; + /* Add client to an existing group */ + for (int x = 0; x < 6; x++) { + if (group->members[x]) { + if (!client_to_add) { + if (group->members[x]->IsClient()) { + client_to_add = group->members[x]->CastToClient(); + raid->SetGroupLeader(client_to_add->GetName()); + } + } + if (group->IsLeader(group->members[x])) { + Client* c = nullptr; - entity_list.AddRaid(raid); - raid->SetRaidDetails(); - Client *addClientig = nullptr; - for (int x = 0; x < 6; x++) { - if (player_invited_group->members[x]) { - if (!addClientig) { - if (player_invited_group->members[x]->IsClient()) { - addClientig = player_invited_group->members[x]->CastToClient(); - raid->SetGroupLeader(addClientig->GetName()); - } + if (group->members[x]->IsClient()) + c = group->members[x]->CastToClient(); + else + continue; + + raid->SendRaidCreate(c); + raid->SendMakeLeaderPacketTo(raid->leadername, c); + raid->AddMember(c, raid_free_group_id, false, true); + raid->SendBulkRaid(c); + + if (raid->IsLocked()) { + raid->SendRaidLockTo(c); + } + } + else + { + Client* c = nullptr; + + if (group->members[x]->IsClient()) + c = group->members[x]->CastToClient(); + else + continue; + + raid->SendRaidCreate(c); + raid->SendMakeLeaderPacketTo(raid->leadername, c); + raid->AddMember(c, raid_free_group_id); + raid->SendBulkRaid(c); + + if (raid->IsLocked()) { + raid->SendRaidLockTo(c); + } + } + } + } + group->JoinRaidXTarget(raid); + group->DisbandGroup(true); + + raid->GroupUpdate(raid_free_group_id); + } + /* Target does not have a group */ + else { + if (player_invited_group) { + + raid = new Raid(player_accepting_invite); + + entity_list.AddRaid(raid); + raid->SetRaidDetails(); + Client* addClientig = nullptr; + for (int x = 0; x < 6; x++) { + if (player_invited_group->members[x]) { + if (!addClientig) { + if (player_invited_group->members[x]->IsClient()) { + addClientig = player_invited_group->members[x]->CastToClient(); + raid->SetGroupLeader(addClientig->GetName()); } - if (player_invited_group->IsLeader(player_invited_group->members[x])) { - Client *c = nullptr; + } + if (player_invited_group->IsLeader(player_invited_group->members[x])) { + Client* c = nullptr; - if (player_invited_group->members[x]->IsClient()) - c = player_invited_group->members[x]->CastToClient(); - else - continue; - - raid->SendRaidCreate(c); - raid->SendMakeLeaderPacketTo(raid->leadername, c); - raid->AddMember(c, 0, true, true, true); - raid->SendBulkRaid(c); - - if (raid->IsLocked()) { - raid->SendRaidLockTo(c); - } - } + if (player_invited_group->members[x]->IsClient()) + c = player_invited_group->members[x]->CastToClient(); else - { - Client *c = nullptr; - if (player_invited_group->members[x]->IsClient()) - c = player_invited_group->members[x]->CastToClient(); - else - continue; + continue; - raid->SendRaidCreate(c); - raid->SendMakeLeaderPacketTo(raid->leadername, c); - raid->AddMember(c, 0); - raid->SendBulkRaid(c); - if (raid->IsLocked()) { - raid->SendRaidLockTo(c); - } + raid->SendRaidCreate(c); + raid->SendMakeLeaderPacketTo(raid->leadername, c); + raid->AddMember(c, 0, true, true, true); + raid->SendBulkRaid(c); + + if (raid->IsLocked()) { + raid->SendRaidLockTo(c); + } + } + else + { + Client* c = nullptr; + if (player_invited_group->members[x]->IsClient()) + c = player_invited_group->members[x]->CastToClient(); + else + continue; + + raid->SendRaidCreate(c); + raid->SendMakeLeaderPacketTo(raid->leadername, c); + raid->AddMember(c, 0); + raid->SendBulkRaid(c); + if (raid->IsLocked()) { + raid->SendRaidLockTo(c); } } } - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->SendBulkRaid(this); - player_invited_group->JoinRaidXTarget(raid, true); - raid->AddMember(this); - player_invited_group->DisbandGroup(true); - raid->GroupUpdate(0); - if (raid->IsLocked()) { - raid->SendRaidLockTo(this); - } } - else { // neither has a group - raid = new Raid(player_accepting_invite); - entity_list.AddRaid(raid); - raid->SetRaidDetails(); - raid->SendRaidCreate(player_accepting_invite); - raid->SendRaidCreate(this); - raid->SendMakeLeaderPacketTo(raid->leadername, this); - raid->AddMember(player_accepting_invite, 0xFFFFFFFF, true, false, true); - raid->SendBulkRaid(this); - raid->AddMember(this); - if (raid->IsLocked()) { - raid->SendRaidLockTo(this); - } + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->SendBulkRaid(this); + player_invited_group->JoinRaidXTarget(raid, true); + raid->AddMember(this); + player_invited_group->DisbandGroup(true); + raid->GroupUpdate(0); + if (raid->IsLocked()) { + raid->SendRaidLockTo(this); + } + } + else { // neither has a group + raid = new Raid(player_accepting_invite); + entity_list.AddRaid(raid); + raid->SetRaidDetails(); + raid->SendRaidCreate(player_accepting_invite); + raid->SendRaidCreate(this); + raid->SendMakeLeaderPacketTo(raid->leadername, this); + raid->AddMember(player_accepting_invite, 0xFFFFFFFF, true, false, true); + raid->SendBulkRaid(this); + raid->AddMember(this); + if (raid->IsLocked()) { + raid->SendRaidLockTo(this); } } } } - break; } - case RaidCommandDisband: { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - uint32 group = raid->GetGroup(raid_command_packet->leader_name); + break; + } + case RaidCommandDisband: { + Raid* raid = entity_list.GetRaidByClient(this); + Client* c_to_disband = entity_list.GetClientByName(raid_command_packet->leader_name); + Client* c_doing_disband = entity_list.GetClientByName(raid_command_packet->player_name); +#ifdef BOTS + Bot* b_to_disband = entity_list.GetBotByBotName(raid_command_packet->leader_name); +#endif - if (group < 12) { - uint32 i = raid->GetPlayerIndex(raid_command_packet->leader_name); - if (raid->members[i].IsGroupLeader) { //assign group leader to someone else - for (int x = 0; x < MAX_RAID_MEMBERS; x++) { - if (strlen(raid->members[x].membername) > 0 && i != x) { - if (raid->members[x].GroupNumber == group) { - raid->SetGroupLeader(raid_command_packet->leader_name, false); - raid->SetGroupLeader(raid->members[x].membername); - raid->UpdateGroupAAs(group); - break; - } - } - } + if (raid) { + uint32 group = raid->GetGroup(raid_command_packet->leader_name); +#ifdef BOTS + //Added to remove all bots if the Bot_Owner is removed from the Raid + //Does not camp the Bots, just removes from the raid + std::vector raid_members_bots; + if (c_to_disband) + { + // Determine if the client has any BOTS in the raid + uint32 owner_id = c_to_disband->CharacterID(); + for (int i = 0; i < MAX_RAID_MEMBERS; ++i) + { + if (raid->members[i].member && raid->members[i].member->IsBot() && raid->members[i].member->CastToBot()->GetOwner()->CastToClient()->CharacterID() == owner_id) + { + raid_members_bots.emplace_back(raid->members[i].member->CastToBot()); } - if (raid->members[i].IsRaidLeader) { - for (int x = 0; x < MAX_RAID_MEMBERS; x++) { - if (strlen(raid->members[x].membername) > 0 && strcmp(raid->members[x].membername, raid->members[i].membername) != 0) - { - raid->SetRaidLeader(raid->members[i].membername, raid->members[x].membername); - raid->UpdateRaidAAs(); - raid->SendAllRaidLeadershipAA(); + } + // If any of the bots are a group leader then re-create the botgroup on disband, dropping any clients + for (auto bot_iter : raid_members_bots) { + if (bot_iter && raid->IsRaidMember(bot_iter->GetName()) && raid->IsGroupLeader(bot_iter->GetName())) + { + // Remove the entire BOT group in this case + uint32 gid = raid->GetGroup(bot_iter->GetName()); + + std::vector r_group_members = raid->GetRaidGroupMembers(gid); + Group* group_inst = new Group(bot_iter); + entity_list.AddGroup(group_inst); + database.SetGroupID(bot_iter->GetCleanName(), group_inst->GetID(), bot_iter->GetBotID()); + database.SetGroupLeaderName(group_inst->GetID(), bot_iter->GetCleanName()); + for (auto member_iter : r_group_members) { + if (!member_iter.member->IsClient() && strcmp(member_iter.membername, bot_iter->GetName()) == 0) + bot_iter->SetFollowID(owner_id); + + else + Bot::AddBotToGroup(member_iter.member->CastToBot(), group_inst); + raid->RemoveMember(bot_iter->GetName()); + } + } + else if (bot_iter && raid->IsRaidMember(bot_iter->GetName())) + { + raid->RemoveMember(bot_iter->GetName()); + } + } + } + else if (b_to_disband) + { + uint32 gid = raid->GetGroup(b_to_disband->GetName()); + if (gid < 12 && raid->IsGroupLeader(b_to_disband->GetName())) + { + // If any of the bots are a group leader then re-create the botgroup on disband, dropping any clients + std::vector r_group_members = raid->GetRaidGroupMembers(gid); + uint32 owner_id = b_to_disband->CastToBot()->GetOwner()->CastToClient()->CharacterID(); + if (raid->IsGroupLeader(b_to_disband->GetName())) + { + // Remove the entire BOT group in this case + Group* group_inst = new Group(b_to_disband); + entity_list.AddGroup(group_inst); + database.SetGroupID(b_to_disband->GetCleanName(), group_inst->GetID(), b_to_disband->GetBotID()); + database.SetGroupLeaderName(group_inst->GetID(), b_to_disband->GetCleanName()); + for (auto member_iter : r_group_members) { + if (!member_iter.member->IsClient() && strcmp(member_iter.membername, b_to_disband->GetName()) == 0) + b_to_disband->SetFollowID(owner_id); + else + Bot::AddBotToGroup(member_iter.member->CastToBot(), group_inst); + raid->RemoveMember(member_iter.member->CastToBot()->GetName()); + } + } + break; + } + else if (gid <12 && raid->GetGroupLeader(gid)->IsBot()) + { + c_doing_disband->Message(Chat::Yellow, "%s is in a Bot Group. Please disband %s instead to remove the entire Bot group.", + raid_command_packet->leader_name, raid->GetGroupLeader(gid)->CastToBot()->GetName()); + break; + } + } +#endif + if (group < 12) { + uint32 i = raid->GetPlayerIndex(raid_command_packet->leader_name); + if (raid->members[i].IsGroupLeader) { //assign group leader to someone else + for (int x = 0; x < MAX_RAID_MEMBERS; x++) { + if (strlen(raid->members[x].membername) > 0 && i != x) { + if (raid->members[x].GroupNumber == group) { + raid->SetGroupLeader(raid_command_packet->leader_name, false); + raid->SetGroupLeader(raid->members[x].membername); + raid->UpdateGroupAAs(group); break; } } } - } - raid->RemoveMember(raid_command_packet->leader_name); - Client *c = entity_list.GetClientByName(raid_command_packet->leader_name); - if (c) - raid->SendGroupDisband(c); - else { - auto pack = - new ServerPacket(ServerOP_RaidGroupDisband, sizeof(ServerRaidGeneralAction_Struct)); - ServerRaidGeneralAction_Struct* rga = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - rga->rid = GetID(); - rga->zoneid = zone->GetZoneID(); - rga->instance_id = zone->GetInstanceID(); - strn0cpy(rga->playername, raid_command_packet->leader_name, 64); - worldserver.SendPacket(pack); - safe_delete(pack); } - //r->SendRaidGroupRemove(ri->leader_name, grp); - raid->GroupUpdate(group);// break - //} - } - break; - } - case RaidCommandMoveGroup: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - /* Moving to group */ - if (raid_command_packet->parameter < 12) { - uint8 group_count = raid->GroupCount(raid_command_packet->parameter); - - if (group_count < 6) { - Client *c = entity_list.GetClientByName(raid_command_packet->leader_name); - uint32 old_group = raid->GetGroup(raid_command_packet->leader_name); - if (raid_command_packet->parameter == old_group) //don't rejoin grp if we order to join same group. + if (raid->members[i].IsRaidLeader) { + for (int x = 0; x < MAX_RAID_MEMBERS; x++) { + if (strlen(raid->members[x].membername) > 0 && strcmp(raid->members[x].membername, raid->members[i].membername) != 0) + { + raid->SetRaidLeader(raid->members[i].membername, raid->members[x].membername); + raid->UpdateRaidAAs(); + raid->SendAllRaidLeadershipAA(); break; - - if (raid->members[raid->GetPlayerIndex(raid_command_packet->leader_name)].IsGroupLeader) { - raid->SetGroupLeader(raid_command_packet->leader_name, false); - - /* We were the leader of our old group */ - if (old_group < 12) { - /* Assign new group leader if we can */ - for (int x = 0; x < MAX_RAID_MEMBERS; x++) { - if (raid->members[x].GroupNumber == old_group) { - if (strcmp(raid_command_packet->leader_name, raid->members[x].membername) != 0 && strlen(raid_command_packet->leader_name) > 0) { - raid->SetGroupLeader(raid->members[x].membername); - raid->UpdateGroupAAs(old_group); - - Client *client_to_update = entity_list.GetClientByName(raid->members[x].membername); - if (client_to_update) { - raid->SendRaidRemove(raid->members[x].membername, client_to_update); - raid->SendRaidCreate(client_to_update); - raid->SendMakeLeaderPacketTo(raid->leadername, client_to_update); - raid->SendRaidAdd(raid->members[x].membername, client_to_update); - raid->SendBulkRaid(client_to_update); - if (raid->IsLocked()) { - raid->SendRaidLockTo(client_to_update); - } - } - else { - auto pack = new ServerPacket(ServerOP_RaidChangeGroup, sizeof(ServerRaidGeneralAction_Struct)); - ServerRaidGeneralAction_Struct *raid_command_packet = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - - raid_command_packet->rid = raid->GetID(); - raid_command_packet->zoneid = zone->GetZoneID(); - raid_command_packet->instance_id = zone->GetInstanceID(); - strn0cpy(raid_command_packet->playername, raid->members[x].membername, 64); - - worldserver.SendPacket(pack); - - safe_delete(pack); - } - break; - } - } - } - } } - if (group_count == 0) { - raid->SetGroupLeader(raid_command_packet->leader_name); - raid->UpdateGroupAAs(raid_command_packet->parameter); - } - - raid->MoveMember(raid_command_packet->leader_name, raid_command_packet->parameter); - if (c) { - raid->SendGroupDisband(c); - } - else { - auto pack = new ServerPacket(ServerOP_RaidGroupDisband, sizeof(ServerRaidGeneralAction_Struct)); - ServerRaidGeneralAction_Struct* raid_command = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - raid_command->rid = raid->GetID(); - raid_command->zoneid = zone->GetZoneID(); - raid_command->instance_id = zone->GetInstanceID(); - strn0cpy(raid_command->playername, raid_command_packet->leader_name, 64); - worldserver.SendPacket(pack); - safe_delete(pack); - } - - /* Send group update to our new group */ - raid->GroupUpdate(raid_command_packet->parameter); - - /* If our old was a group send update there too */ - if (old_group < 12) - raid->GroupUpdate(old_group); - } } - /* Move player to ungrouped bank */ - else { - Client *c = entity_list.GetClientByName(raid_command_packet->leader_name); - uint32 oldgrp = raid->GetGroup(raid_command_packet->leader_name); + } + raid->RemoveMember(raid_command_packet->leader_name); + Client* c = entity_list.GetClientByName(raid_command_packet->leader_name); + if (c) { + + raid->SendGroupDisband(c); + } + else { + auto pack = + new ServerPacket(ServerOP_RaidGroupDisband, sizeof(ServerRaidGeneralAction_Struct)); + ServerRaidGeneralAction_Struct* rga = (ServerRaidGeneralAction_Struct*)pack->pBuffer; + rga->rid = GetID(); + rga->zoneid = zone->GetZoneID(); + rga->instance_id = zone->GetInstanceID(); + strn0cpy(rga->playername, raid_command_packet->leader_name, 64); + worldserver.SendPacket(pack); + safe_delete(pack); + } + //r->SendRaidGroupRemove(ri->leader_name, grp); + raid->GroupUpdate(group);// break + if (!raid->RaidCount()) + raid->DisbandRaid(); + } + break; + } + case RaidCommandMoveGroup: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + /* Moving to group */ + if (raid_command_packet->parameter < 12) { + uint8 group_count = raid->GroupCount(raid_command_packet->parameter); + + if (group_count < 6) { + Client* c = entity_list.GetClientByName(raid_command_packet->leader_name); + uint32 old_group = raid->GetGroup(raid_command_packet->leader_name); + if (raid_command_packet->parameter == old_group) //don't rejoin grp if we order to join same group. + break; + if (raid->members[raid->GetPlayerIndex(raid_command_packet->leader_name)].IsGroupLeader) { raid->SetGroupLeader(raid_command_packet->leader_name, false); - for (int x = 0; x < MAX_RAID_MEMBERS; x++) { - if (raid->members[x].GroupNumber == oldgrp && strlen(raid->members[x].membername) > 0 && strcmp(raid->members[x].membername, raid_command_packet->leader_name) != 0){ - raid->SetGroupLeader(raid->members[x].membername); - raid->UpdateGroupAAs(oldgrp); + /* We were the leader of our old group */ + if (old_group < 12) { + /* Assign new group leader if we can */ + for (int x = 0; x < MAX_RAID_MEMBERS; x++) { + if (raid->members[x].GroupNumber == old_group) { + if (strcmp(raid_command_packet->leader_name, raid->members[x].membername) != 0 && strlen(raid_command_packet->leader_name) > 0) { + raid->SetGroupLeader(raid->members[x].membername); + raid->UpdateGroupAAs(old_group); - Client *client_leaving_group = entity_list.GetClientByName(raid->members[x].membername); - if (client_leaving_group) { - raid->SendRaidRemove(raid->members[x].membername, client_leaving_group); - raid->SendRaidCreate(client_leaving_group); - raid->SendMakeLeaderPacketTo(raid->leadername, client_leaving_group); - raid->SendRaidAdd(raid->members[x].membername, client_leaving_group); - raid->SendBulkRaid(client_leaving_group); - if (raid->IsLocked()) { - raid->SendRaidLockTo(client_leaving_group); + Client* client_to_update = entity_list.GetClientByName(raid->members[x].membername); + if (client_to_update) { + raid->SendRaidRemove(raid->members[x].membername, client_to_update); + raid->SendRaidCreate(client_to_update); + raid->SendMakeLeaderPacketTo(raid->leadername, client_to_update); + raid->SendRaidAdd(raid->members[x].membername, client_to_update); + raid->SendBulkRaid(client_to_update); + if (raid->IsLocked()) { + raid->SendRaidLockTo(client_to_update); + } + } + else { + auto pack = new ServerPacket(ServerOP_RaidChangeGroup, sizeof(ServerRaidGeneralAction_Struct)); + ServerRaidGeneralAction_Struct* raid_command_packet = (ServerRaidGeneralAction_Struct*)pack->pBuffer; + + raid_command_packet->rid = raid->GetID(); + raid_command_packet->zoneid = zone->GetZoneID(); + raid_command_packet->instance_id = zone->GetInstanceID(); + strn0cpy(raid_command_packet->playername, raid->members[x].membername, 64); + + worldserver.SendPacket(pack); + + safe_delete(pack); + } + break; } } - else { - auto pack = new ServerPacket( ServerOP_RaidChangeGroup, sizeof(ServerRaidGeneralAction_Struct)); - ServerRaidGeneralAction_Struct *raid_command = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - - raid_command->rid = raid->GetID(); - strn0cpy(raid_command->playername, raid->members[x].membername, 64); - raid_command->zoneid = zone->GetZoneID(); - raid_command->instance_id = zone->GetInstanceID(); - - worldserver.SendPacket(pack); - safe_delete(pack); - } - break; } } } - raid->MoveMember(raid_command_packet->leader_name, 0xFFFFFFFF); + if (group_count == 0) { + raid->SetGroupLeader(raid_command_packet->leader_name); + raid->UpdateGroupAAs(raid_command_packet->parameter); + } + + raid->MoveMember(raid_command_packet->leader_name, raid_command_packet->parameter); if (c) { raid->SendGroupDisband(c); } else { auto pack = new ServerPacket(ServerOP_RaidGroupDisband, sizeof(ServerRaidGeneralAction_Struct)); ServerRaidGeneralAction_Struct* raid_command = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - raid_command->rid = raid->GetID(); raid_command->zoneid = zone->GetZoneID(); raid_command->instance_id = zone->GetInstanceID(); strn0cpy(raid_command->playername, raid_command_packet->leader_name, 64); - worldserver.SendPacket(pack); - safe_delete(pack); } - raid->GroupUpdate(oldgrp); + /* Send group update to our new group */ + raid->GroupUpdate(raid_command_packet->parameter); + + /* If our old was a group send update there too */ + if (old_group < 12) + raid->GroupUpdate(old_group); + } } + /* Move player to ungrouped bank */ + else { + Client* c = entity_list.GetClientByName(raid_command_packet->leader_name); + uint32 oldgrp = raid->GetGroup(raid_command_packet->leader_name); + if (raid->members[raid->GetPlayerIndex(raid_command_packet->leader_name)].IsGroupLeader) { + raid->SetGroupLeader(raid_command_packet->leader_name, false); + for (int x = 0; x < MAX_RAID_MEMBERS; x++) { + if (raid->members[x].GroupNumber == oldgrp && strlen(raid->members[x].membername) > 0 && strcmp(raid->members[x].membername, raid_command_packet->leader_name) != 0) { - Client *client_moved = entity_list.GetClientByName(raid_command_packet->leader_name); + raid->SetGroupLeader(raid->members[x].membername); + raid->UpdateGroupAAs(oldgrp); - if (client_moved && client_moved->GetRaid()) { - client_moved->GetRaid()->SendHPManaEndPacketsTo(client_moved); - client_moved->GetRaid()->SendHPManaEndPacketsFrom(client_moved); + Client* client_leaving_group = entity_list.GetClientByName(raid->members[x].membername); + if (client_leaving_group) { + raid->SendRaidRemove(raid->members[x].membername, client_leaving_group); + raid->SendRaidCreate(client_leaving_group); + raid->SendMakeLeaderPacketTo(raid->leadername, client_leaving_group); + raid->SendRaidAdd(raid->members[x].membername, client_leaving_group); + raid->SendBulkRaid(client_leaving_group); + if (raid->IsLocked()) { + raid->SendRaidLockTo(client_leaving_group); + } + } + else { + auto pack = new ServerPacket(ServerOP_RaidChangeGroup, sizeof(ServerRaidGeneralAction_Struct)); + ServerRaidGeneralAction_Struct* raid_command = (ServerRaidGeneralAction_Struct*)pack->pBuffer; - Log(Logs::General, Logs::HPUpdate, - "Client::Handle_OP_RaidCommand :: %s sending and recieving HP/Mana/End updates", - client_moved->GetCleanName() - ); - } + raid_command->rid = raid->GetID(); + strn0cpy(raid_command->playername, raid->members[x].membername, 64); + raid_command->zoneid = zone->GetZoneID(); + raid_command->instance_id = zone->GetInstanceID(); - break; - } - case RaidCommandRaidLock: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - if (!raid->IsLocked()) - raid->LockRaid(true); - else - raid->SendRaidLockTo(this); - } - break; - } - case RaidCommandRaidUnlock: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) - { - if (raid->IsLocked()) - raid->LockRaid(false); - else - raid->SendRaidUnlockTo(this); - } - break; - } - case RaidCommandLootType2: - case RaidCommandLootType: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - Message(Chat::Yellow, "Loot type changed to: %d.", raid_command_packet->parameter); - raid->ChangeLootType(raid_command_packet->parameter); - } - break; - } - - case RaidCommandAddLooter2: - case RaidCommandAddLooter: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - Message(Chat::Yellow, "Adding %s as a raid looter.", raid_command_packet->leader_name); - raid->AddRaidLooter(raid_command_packet->leader_name); - } - break; - } - - case RaidCommandRemoveLooter2: - case RaidCommandRemoveLooter: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - Message(Chat::Yellow, "Removing %s as a raid looter.", raid_command_packet->leader_name); - raid->RemoveRaidLooter(raid_command_packet->leader_name); - } - break; - } - - case RaidCommandMakeLeader: - { - Raid *raid = entity_list.GetRaidByClient(this); - if (raid) { - if (strcmp(raid->leadername, GetName()) == 0) { - raid->SetRaidLeader(GetName(), raid_command_packet->leader_name); - raid->UpdateRaidAAs(); - raid->SendAllRaidLeadershipAA(); + worldserver.SendPacket(pack); + safe_delete(pack); + } + break; + } + } } + raid->MoveMember(raid_command_packet->leader_name, 0xFFFFFFFF); + if (c) { + raid->SendGroupDisband(c); + } + else { + auto pack = new ServerPacket(ServerOP_RaidGroupDisband, sizeof(ServerRaidGeneralAction_Struct)); + ServerRaidGeneralAction_Struct* raid_command = (ServerRaidGeneralAction_Struct*)pack->pBuffer; + + raid_command->rid = raid->GetID(); + raid_command->zoneid = zone->GetZoneID(); + raid_command->instance_id = zone->GetInstanceID(); + strn0cpy(raid_command->playername, raid_command_packet->leader_name, 64); + + worldserver.SendPacket(pack); + + safe_delete(pack); + } + + raid->GroupUpdate(oldgrp); } - break; } - case RaidCommandSetMotd: + Client* client_moved = entity_list.GetClientByName(raid_command_packet->leader_name); + + if (client_moved && client_moved->GetRaid()) { + client_moved->GetRaid()->SendHPManaEndPacketsTo(client_moved); + client_moved->GetRaid()->SendHPManaEndPacketsFrom(client_moved); + + Log(Logs::General, Logs::HPUpdate, + "Client::Handle_OP_RaidCommand :: %s sending and recieving HP/Mana/End updates", + client_moved->GetCleanName() + ); + } + + break; + } + case RaidCommandRaidLock: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + if (!raid->IsLocked()) + raid->LockRaid(true); + else + raid->SendRaidLockTo(this); + } + break; + } + case RaidCommandRaidUnlock: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { - Raid *raid = entity_list.GetRaidByClient(this); - if (!raid) - break; - // we don't use the RaidGeneral here! - RaidMOTD_Struct *motd = (RaidMOTD_Struct *)app->pBuffer; - raid->SetRaidMOTD(std::string(motd->motd)); - raid->SaveRaidMOTD(); - raid->SendRaidMOTDToWorld(); - break; + if (raid->IsLocked()) + raid->LockRaid(false); + else + raid->SendRaidUnlockTo(this); } + break; + } + case RaidCommandLootType2: + case RaidCommandLootType: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + Message(Chat::Yellow, "Loot type changed to: %d.", raid_command_packet->parameter); + raid->ChangeLootType(raid_command_packet->parameter); + } + break; + } - default: { - Message(Chat::Red, "Raid command (%d) NYI", raid_command_packet->action); - break; + case RaidCommandAddLooter2: + case RaidCommandAddLooter: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + Message(Chat::Yellow, "Adding %s as a raid looter.", raid_command_packet->leader_name); + raid->AddRaidLooter(raid_command_packet->leader_name); } + break; + } + + case RaidCommandRemoveLooter2: + case RaidCommandRemoveLooter: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + Message(Chat::Yellow, "Removing %s as a raid looter.", raid_command_packet->leader_name); + raid->RemoveRaidLooter(raid_command_packet->leader_name); + } + break; + } + + case RaidCommandMakeLeader: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (raid) { + if (strcmp(raid->leadername, GetName()) == 0) { + raid->SetRaidLeader(GetName(), raid_command_packet->leader_name); + raid->UpdateRaidAAs(); + raid->SendAllRaidLeadershipAA(); + } + } + break; + } + + case RaidCommandSetMotd: + { + Raid* raid = entity_list.GetRaidByClient(this); + if (!raid) + break; + // we don't use the RaidGeneral here! + RaidMOTD_Struct* motd = (RaidMOTD_Struct*)app->pBuffer; + raid->SetRaidMOTD(std::string(motd->motd)); + raid->SaveRaidMOTD(); + raid->SendRaidMOTDToWorld(); + break; + } + + default: { + Message(Chat::Red, "Raid command (%d) NYI", raid_command_packet->action); + break; + } } } - + void Client::Handle_OP_RandomReq(const EQApplicationPacket *app) { if (app->size != sizeof(RandomReq_Struct)) { diff --git a/zone/entity.cpp b/zone/entity.cpp index 1f786cb4c..a98dbfba5 100644 --- a/zone/entity.cpp +++ b/zone/entity.cpp @@ -2137,17 +2137,17 @@ Raid *EntityList::GetRaidByID(uint32 id) return nullptr; } -Raid *EntityList::GetRaidByClient(Client* client) +Raid* EntityList::GetRaidByClient(Client* client) { if (client->p_raid_instance) { return client->p_raid_instance; } - std::list::iterator iterator; + std::list::iterator iterator; iterator = raid_list.begin(); while (iterator != raid_list.end()) { - for (auto &member : (*iterator)->members) { + for (auto& member : (*iterator)->members) { if (member.member) { if (member.member == client) { client->p_raid_instance = *iterator; @@ -2161,6 +2161,50 @@ Raid *EntityList::GetRaidByClient(Client* client) return nullptr; } +#ifdef BOTS +Raid* EntityList::GetRaidByBotName(const char* name) +{ + + std::list::iterator iterator; + iterator = raid_list.begin(); + + while (iterator != raid_list.end()) { + for (auto& member : (*iterator)->members) { + if (member.membername) { + if (strcmp(member.membername, name) == 0) { + //client->p_raid_instance = *iterator; + return *iterator; + } + } + } + + ++iterator; + } + + return nullptr; +} +#endif + +#ifdef BOTS +Raid* EntityList::GetRaidByBot(Bot* bot) +{ + + std::list::iterator iterator; + iterator = raid_list.begin(); + + while (iterator != raid_list.end()) { + for (auto& member : (*iterator)->members) { + if (member.member && member.member->CastToBot() == bot) { + bot->p_raid_instance = *iterator; + return *iterator; + } + } + ++iterator; + } + return nullptr; +} +#endif + Raid *EntityList::GetRaidByMob(Mob *mob) { diff --git a/zone/entity.h b/zone/entity.h index f06e26119..06e2ca401 100644 --- a/zone/entity.h +++ b/zone/entity.h @@ -200,6 +200,10 @@ public: Raid *GetRaidByClient(Client* client); Raid *GetRaidByID(uint32 id); Raid *GetRaidByLeaderName(const char *leader); +#ifdef BOTS + Raid* GetRaidByBotName(const char* name); + Raid* GetRaidByBot(Bot* bot); +#endif Corpse *GetCorpseByOwner(Client* client); Corpse *GetCorpseByOwnerWithinRange(Client* client, Mob* center, int range); diff --git a/zone/exp.cpp b/zone/exp.cpp index aad0f141b..2badce051 100644 --- a/zone/exp.cpp +++ b/zone/exp.cpp @@ -1097,7 +1097,11 @@ void Raid::SplitExp(uint32 exp, Mob* other) { return; for (unsigned int x = 0; x < MAX_RAID_MEMBERS; x++) { +#ifdef BOTS + if (members[x].member != nullptr && !members[x].IsBot) // If Group Member is Client +#else if (members[x].member != nullptr) // If Group Member is Client +#endif { Client *cmember = members[x].member; // add exp + exp cap diff --git a/zone/groups.cpp b/zone/groups.cpp index c82e56406..5b365b158 100644 --- a/zone/groups.cpp +++ b/zone/groups.cpp @@ -248,10 +248,24 @@ bool Group::AddMember(Mob* newmember, const char *NewMemberName, uint32 Characte uint32 i = 0; for (i = 0; i < MAX_GROUP_MEMBERS; ++i) { - if(!strcasecmp(membername[i], NewMemberName)) +#ifdef BOTSS + if (newmember->IsBot() && !newmember->HasGroup() && !strcasecmp(membername[i], NewMemberName)) // Mitch + { + //Bot::RemoveBotFromGroup(newmember->CastToBot(), members[0]->GetGroup()); + //Group::DelMember(newmember); + memset(membername[i], 0, 64); + members[i] = nullptr; + } + else if (!strcasecmp(membername[i], NewMemberName)) { return false; } +#else + if (!strcasecmp(membername[i], NewMemberName)) + { + return false; + } +#endif } // Put them in the group @@ -1140,7 +1154,13 @@ void Group::TeleportGroup(Mob* sender, uint32 zoneID, uint16 instance_id, float } bool Group::LearnMembers() { - std::string query = StringFormat("SELECT name FROM group_id WHERE groupid = %lu", (unsigned long)GetID()); + //std::string query = StringFormat("SELECT name FROM group_id WHERE groupid = %lu", (unsigned long)GetID()); + std::string query = StringFormat("SELECT name FROM group_id " + "WHERE group_id.groupid = %lu AND group_id.name NOT " + "IN(SELECT group_leaders.leadername FROM group_leaders WHERE gid = %lu)" + , (unsigned long)GetID() + , (unsigned long)GetID()); + auto results = database.QueryDatabase(query); if (!results.Success()) return false; @@ -1155,7 +1175,7 @@ bool Group::LearnMembers() { return false; } - int memberIndex = 0; + int memberIndex = 1; //starts at 1 becasuse leader [0] is done specifically for(auto row = results.begin(); row != results.end(); ++row) { if(!row[0]) continue; @@ -1165,7 +1185,24 @@ bool Group::LearnMembers() { memberIndex++; } + // for leader only [0] /Mitch + query = StringFormat("SELECT leadername FROM group_leaders WHERE group_leaders.gid = %lu", (unsigned long)GetID()); + auto results2 = database.QueryDatabase(query); + if (!results2.Success()) + return false; + if (results2.RowCount() == 0) { + LogError( + "Error getting group leader for group [{}]: [{}]", + (unsigned long)GetID(), + results2.ErrorMessage().c_str() + ); + + return false; + } + auto row2 = results2.begin(); + members[0] = nullptr; + strn0cpy(membername[0], row2[0], 64); return true; } @@ -1176,6 +1213,22 @@ void Group::VerifyGroup() { Only called every once in a while (on member re-join for now). */ + // To do + // Reset the membername array to match the group_id database records? + // When doing this manually, it seem to have resolved the issue. + // Only want to do this when the database Name does not match the array + // Could this be done from the LearnGroup method? + // reset the members and membername array for this group + // Mitch + for (int i = 0; i < MAX_GROUP_MEMBERS; i++) { + members[i] = nullptr; + memset(membername[i],'\0',64); + //membername[i][0] == '\0'); + } + // repopulate the membername array from the database to ensure the local zone instance has accurate information + + Group::LearnMembers(); + uint32 i; for (i = 0; i < MAX_GROUP_MEMBERS; i++) { if (membername[i][0] == '\0') { @@ -1196,6 +1249,7 @@ void Group::VerifyGroup() { continue; } + if(them != nullptr && members[i] != them) { //our pointer is out of date... not so good. #if EQDEBUG >= 5 LogDebug("Member of group [{}] named [{}] had an out of date pointer!!", (unsigned long)GetID(), membername[i]); diff --git a/zone/main.cpp b/zone/main.cpp index f92ea9ea8..3dcba3b86 100644 --- a/zone/main.cpp +++ b/zone/main.cpp @@ -88,6 +88,7 @@ volatile bool RunLoops = true; #endif extern volatile bool is_zone_loaded; +extern bool Critical = false; EntityList entity_list; WorldServer worldserver; @@ -578,7 +579,7 @@ int main(int argc, char** argv) { EQ::Timer process_timer(loop_fn); process_timer.Start(32, true); - + EQ::EventLoop::Get().Run(); entity_list.Clear(); diff --git a/zone/mob.cpp b/zone/mob.cpp index dc0032256..bbc0c5cb3 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -3987,7 +3987,7 @@ void Mob::QuestJournalledSay(Client *QuestInitiator, const char *str, Journal::O const char *Mob::GetCleanName() { - if (!strlen(clean_name)) { + if (!strlen(clean_name)) { CleanMobName(GetName(), clean_name); } diff --git a/zone/raids.cpp b/zone/raids.cpp index db57a4eed..e66092867 100644 --- a/zone/raids.cpp +++ b/zone/raids.cpp @@ -24,6 +24,7 @@ #include "groups.h" #include "mob.h" #include "raids.h" +#include "bot.h" #include "worldserver.h" @@ -95,13 +96,22 @@ void Raid::AddMember(Client *c, uint32 group, bool rleader, bool groupleader, bo if(!c) return; +#ifdef BOTS std::string query = StringFormat("INSERT INTO raid_members SET raidid = %lu, charid = %lu, " "groupid = %lu, _class = %d, level = %d, name = '%s', " - "isgroupleader = %d, israidleader = %d, islooter = %d", + "isgroupleader = %d, israidleader = %d, islooter = %d, isbot = 0", (unsigned long)GetID(), (unsigned long)c->CharacterID(), (unsigned long)group, c->GetClass(), c->GetLevel(), c->GetName(), groupleader, rleader, looter); - auto results = database.QueryDatabase(query); +#else + std::string query = StringFormat("INSERT INTO raid_members SET raidid = %lu, charid = %lu, " + "groupid = %lu, _class = %d, level = %d, name = '%s', " + "isgroupleader = %d, israidleader = %d, islooter = %d", + (unsigned long)GetID(), (unsigned long)c->CharacterID(), + (unsigned long)group, c->GetClass(), c->GetLevel(), + c->GetName(), groupleader, rleader, looter); +#endif + auto results = database.QueryDatabase(query); if(!results.Success()) { LogError("Error inserting into raid members: [{}]", results.ErrorMessage().c_str()); @@ -109,6 +119,14 @@ void Raid::AddMember(Client *c, uint32 group, bool rleader, bool groupleader, bo LearnMembers(); VerifyRaid(); + +#ifdef BOTS + if (rleader) { + database.SetRaidGroupLeaderInfo(group, GetID()); + UpdateRaidAAs(); + } + else +#endif if (rleader) { database.SetRaidGroupLeaderInfo(RAID_GROUPLESS, GetID()); UpdateRaidAAs(); @@ -163,12 +181,65 @@ void Raid::AddMember(Client *c, uint32 group, bool rleader, bool groupleader, bo safe_delete(pack); } +#ifdef BOTS +void Raid::AddBot(Bot* b, uint32 group, bool rleader, bool groupleader, bool looter) { + if (!b) + return; + + std::string query = StringFormat("INSERT INTO raid_members SET raidid = %lu, charid = %lu, " + "groupid = %lu, _class = %d, level = %d, name = '%s', " + "isgroupleader = %d, israidleader = %d, islooter = %d, isbot = 1", + (unsigned long)GetID(), (unsigned long)b->GetBotID(), + (unsigned long)group, b->GetClass(), b->GetLevel(), + b->GetName(), groupleader, rleader, looter); + auto results = database.QueryDatabase(query); + + if (!results.Success()) { + LogError("Error inserting into raid members: [{}]", results.ErrorMessage().c_str()); + } + + LearnMembers(); + VerifyRaid(); + + if (group < 12) //Jan22 + GroupUpdate(group); //Jan22 + else // get raid AAs, GroupUpdate will handles it otherwise Jan 22 + SendGroupLeadershipAA(b->GetOwner()->CastToClient(), RAID_GROUPLESS); //Is this needed for bots? Jan 22 + SendRaidAddAll(b->GetName()); + + b->SetRaidGrouped(true); + b->p_raid_instance = this; + + + auto pack = new ServerPacket(ServerOP_RaidAdd, sizeof(ServerRaidGeneralAction_Struct)); + ServerRaidGeneralAction_Struct* rga = (ServerRaidGeneralAction_Struct*)pack->pBuffer; + rga->rid = GetID(); + strn0cpy(rga->playername, b->GetName(), 64); + rga->zoneid = zone->GetZoneID(); + rga->instance_id = zone->GetInstanceID(); + worldserver.SendPacket(pack); + safe_delete(pack); +} +#endif + + void Raid::RemoveMember(const char *characterName) { std::string query = StringFormat("DELETE FROM raid_members where name='%s'", characterName); auto results = database.QueryDatabase(query); Client *client = entity_list.GetClientByName(characterName); +#ifdef BOTS + Bot* bot = entity_list.GetBotByBotName(characterName); + + if (bot) { + bot->SetFollowID(bot->GetOwner()->CastToClient()->GetID()); + bot->SetGrouped(false); + bot->SetTarget(nullptr); + bot->SetRaidGrouped(false); + } +#endif + disbandCheck = true; SendRaidRemoveAll(characterName); SendRaidDisband(client); @@ -983,12 +1054,12 @@ void Raid::SendRaidAdd(const char *who, Client *to) void Raid::SendRaidAddAll(const char *who) { - for(int x = 0; x < MAX_RAID_MEMBERS; x++) + for (int x = 0; x < MAX_RAID_MEMBERS; x++) { - if(strcmp(members[x].membername, who) == 0) + if (strcmp(members[x].membername, who) == 0 ) { auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidAddMember_Struct)); - RaidAddMember_Struct *ram = (RaidAddMember_Struct*)outapp->pBuffer; + RaidAddMember_Struct* ram = (RaidAddMember_Struct*)outapp->pBuffer; ram->raidGen.action = raidAdd; ram->raidGen.parameter = members[x].GroupNumber; strcpy(ram->raidGen.leader_name, members[x].membername); @@ -999,6 +1070,7 @@ void Raid::SendRaidAddAll(const char *who) QueuePacket(outapp); safe_delete(outapp); return; + } } } @@ -1111,11 +1183,16 @@ void Raid::SendBulkRaid(Client *to) if(!to) return; +#ifdef BOTS + if (members[GetPlayerIndex(to)].IsBot) + return; +#endif + for(int x = 0; x < MAX_RAID_MEMBERS; x++) { - if(strlen(members[x].membername) > 0 && (strcmp(members[x].membername, to->GetName()) != 0)) //don't send ourself + if(members[x].member && strlen(members[x].membername) > 0 && (strcmp(members[x].membername, to->GetName()) != 0)) //don't send ourself { - SendRaidAdd(members[x].membername, to); + SendRaidAdd(members[x].membername, to); } } } @@ -1124,7 +1201,11 @@ void Raid::QueuePacket(const EQApplicationPacket *app, bool ack_req) { for(int x = 0; x < MAX_RAID_MEMBERS; x++) { +#ifdef BOTS + if(members[x].member && !members[x].IsBot) +#else if(members[x].member) +#endif { members[x].member->QueuePacket(app, ack_req); } @@ -1133,6 +1214,12 @@ void Raid::QueuePacket(const EQApplicationPacket *app, bool ack_req) void Raid::SendMakeLeaderPacket(const char *who) //30 { + +#ifdef BOTS + if (entity_list.GetBotByBotName(who) && members[GetPlayerIndex(who)].IsBot) + return; +#endif + auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidLeadershipUpdate_Struct)); RaidLeadershipUpdate_Struct *rg = (RaidLeadershipUpdate_Struct*)outapp->pBuffer; rg->action = raidMakeLeader; @@ -1148,6 +1235,11 @@ void Raid::SendMakeLeaderPacketTo(const char *who, Client *to) if(!to) return; +#ifdef BOTS + if (members[GetPlayerIndex(who)].IsBot) + return; +#endif + auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidLeadershipUpdate_Struct)); RaidLeadershipUpdate_Struct *rg = (RaidLeadershipUpdate_Struct*)outapp->pBuffer; rg->action = raidMakeLeader; @@ -1178,6 +1270,11 @@ void Raid::SendGroupUpdate(Client *to) if(!to) return; +#ifdef BOTS + if (members[GetPlayerIndex(to)].IsBot) + return; +#endif + auto outapp = new EQApplicationPacket(OP_GroupUpdate, sizeof(GroupUpdate2_Struct)); GroupUpdate2_Struct* gu = (GroupUpdate2_Struct*)outapp->pBuffer; gu->action = groupActUpdate; @@ -1224,7 +1321,7 @@ void Raid::GroupUpdate(uint32 gid, bool initial) { if(strlen(members[x].membername) > 0){ if(members[x].GroupNumber == gid){ - if(members[x].member) { + if (members[x].member) { SendGroupUpdate(members[x].member); SendGroupLeadershipAA(members[x].member, gid); } @@ -1366,6 +1463,11 @@ void Raid::SendRaidMOTDToWorld() void Raid::SendGroupLeadershipAA(Client *c, uint32 gid) { +#ifdef BOTS + if (members[GetPlayerIndex(c)].IsBot) + return; +#endif + auto outapp = new EQApplicationPacket(OP_RaidUpdate, sizeof(RaidLeadershipUpdate_Struct)); RaidLeadershipUpdate_Struct *rlaa = (RaidLeadershipUpdate_Struct *)outapp->pBuffer; rlaa->action = raidSetLeaderAbilities; @@ -1381,17 +1483,26 @@ void Raid::SendGroupLeadershipAA(Client *c, uint32 gid) void Raid::SendGroupLeadershipAA(uint32 gid) { for (uint32 i = 0; i < MAX_RAID_MEMBERS; i++) +#ifdef BOTS + if (members[i].member && members[i].GroupNumber == gid && !members[i].IsBot) +#else if (members[i].member && members[i].GroupNumber == gid) +#endif SendGroupLeadershipAA(members[i].member, gid); } void Raid::SendAllRaidLeadershipAA() { for (uint32 i = 0; i < MAX_RAID_MEMBERS; i++) +#ifdef BOTS + if (members[i].member && !members[i].IsBot) +#else if (members[i].member) - SendGroupLeadershipAA(members[i].member, members[i].GroupNumber); +#endif + SendGroupLeadershipAA(members[i].member, members[i].GroupNumber); } + void Raid::LockRaid(bool lockFlag) { std::string query = StringFormat("UPDATE raid_details SET locked = %d WHERE raidid = %lu", @@ -1457,11 +1568,18 @@ bool Raid::LearnMembers() { memset(members, 0, (sizeof(RaidMember)*MAX_RAID_MEMBERS)); +#ifdef BOTS std::string query = StringFormat("SELECT name, groupid, _class, level, " - "isgroupleader, israidleader, islooter " + "isgroupleader, israidleader, islooter, isbot " "FROM raid_members WHERE raidid = %lu", (unsigned long)GetID()); - auto results = database.QueryDatabase(query); +#else + std::string query = StringFormat("SELECT name, groupid, _class, level, " + "isgroupleader, israidleader, islooter " + "FROM raid_members WHERE raidid = %lu", + (unsigned long)GetID()); +#endif + auto results = database.QueryDatabase(query); if (!results.Success()) return false; @@ -1489,7 +1607,10 @@ bool Raid::LearnMembers() members[index].IsGroupLeader = atoi(row[4]); members[index].IsRaidLeader = atoi(row[5]); members[index].IsLooter = atoi(row[6]); - ++index; +#ifdef BOTS + members[index].IsBot = atoi(row[7]); +#endif + ++index; } return true; @@ -1504,14 +1625,32 @@ void Raid::VerifyRaid() } else{ Client *c = entity_list.GetClientByName(members[x].membername); +#ifdef BOTS + Bot* b = entity_list.GetBotByBotName(members[x].membername); +#endif if(c){ members[x].member = c; +#ifdef BOTS + members[x].IsBot = false; +#endif } - else{ +#ifdef BOTS + else if(b){ + //Raid requires client* we are forcing it here to be a BOT. Care is needed here as any client function that + //does not exist within the Bot Class will likely corrupt memory for the member object. Good practice to test the IsBot + //attribute before calling a client function or casting to client. + members[x].member = b->CastToClient(); + members[x].IsBot = true; //Used to identify those members who are Bots + } +#endif + else { members[x].member = nullptr; +#ifdef BOTS + members[x].IsBot = false; +#endif } } - if(members[x].IsRaidLeader){ + if(members[x].IsRaidLeader){ if(strlen(members[x].membername) > 0){ SetLeader(members[x].member); strn0cpy(leadername, members[x].membername, 64); @@ -1561,7 +1700,11 @@ void Raid::SendHPManaEndPacketsTo(Client *client) EQApplicationPacket outapp(OP_MobManaUpdate, sizeof(MobManaUpdate_Struct)); for(int x = 0; x < MAX_RAID_MEMBERS; x++) { - if(members[x].member) { +#ifdef BOTS + if(members[x].member && !members[x].IsBot) { +#else + if (members[x].member) { +#endif if((members[x].member != client) && (members[x].GroupNumber == group_id)) { members[x].member->CreateHPPacket(&hp_packet); @@ -1603,7 +1746,11 @@ void Raid::SendHPManaEndPacketsFrom(Mob *mob) mob->CreateHPPacket(&hpapp); for(int x = 0; x < MAX_RAID_MEMBERS; x++) { - if(members[x].member) { +#ifdef BOTS + if(members[x].member && !members[x].IsBot) { +#else + if (members[x].member) { +#endif if(!mob->IsClient() || ((members[x].member != mob->CastToClient()) && (members[x].GroupNumber == group_id))) { members[x].member->QueuePacket(&hpapp, false); if (members[x].member->ClientVersion() >= EQ::versions::ClientVersion::SoD) { @@ -1636,7 +1783,11 @@ void Raid::SendManaPacketFrom(Mob *mob) EQApplicationPacket outapp(OP_MobManaUpdate, sizeof(MobManaUpdate_Struct)); for (int x = 0; x < MAX_RAID_MEMBERS; x++) { +#ifdef BOTS + if (members[x].member && !members[x].IsBot) { +#else if (members[x].member) { +#endif if (!mob->IsClient() || ((members[x].member != mob->CastToClient()) && (members[x].GroupNumber == group_id))) { if (members[x].member->ClientVersion() >= EQ::versions::ClientVersion::SoD) { outapp.SetOpcode(OP_MobManaUpdate); @@ -1663,7 +1814,11 @@ void Raid::SendEndurancePacketFrom(Mob *mob) EQApplicationPacket outapp(OP_MobManaUpdate, sizeof(MobManaUpdate_Struct)); for (int x = 0; x < MAX_RAID_MEMBERS; x++) { +#ifdef BTOS + if (members[x].member && !members[x].IsBot) { +#else if (members[x].member) { +#endif if (!mob->IsClient() || ((members[x].member != mob->CastToClient()) && (members[x].GroupNumber == group_id))) { if (members[x].member->ClientVersion() >= EQ::versions::ClientVersion::SoD) { outapp.SetOpcode(OP_MobEnduranceUpdate); @@ -1770,9 +1925,18 @@ void Raid::CheckGroupMentor(uint32 group_id, Client *c) void Raid::SetDirtyAutoHaters() { for (int i = 0; i < MAX_RAID_MEMBERS; ++i) +#ifdef BOTS + if (members[i].member && members[i].IsBot) + { + members[i].member->CastToBot()->SetDirtyAutoHaters(); + } + else if (members[i].member && !members[i].IsBot) +#else if (members[i].member) +#endif + { members[i].member->SetDirtyAutoHaters(); - + } } void Raid::QueueClients(Mob *sender, const EQApplicationPacket *app, bool ack_required /*= true*/, bool ignore_sender /*= true*/, float distance /*= 0*/, bool group_only /*= true*/) { @@ -1791,6 +1955,10 @@ void Raid::QueueClients(Mob *sender, const EQApplicationPacket *app, bool ack_re if (!members[i].member->IsClient()) continue; +#ifdef BOTS + if (members[i].IsBot) + continue; +#endif if (ignore_sender && members[i].member == sender) continue; @@ -1853,3 +2021,17 @@ bool Raid::DoesAnyMemberHaveExpeditionLockout( return Expedition::HasLockoutByCharacterName(raid_member.membername, expedition_name, event_name); }); } +#ifdef BOTS +Mob* Raid::GetRaidMainAssistOneByName(const char* name) +{ + Raid* raid = entity_list.GetRaidByBotName(name); + std::vector raid_members = raid->GetMembers(); + + for (RaidMember iter : raid_members) + { + if (iter.IsRaidMainAssistOne) + return iter.member->CastToMob(); + } + return nullptr; +} +#endif diff --git a/zone/raids.h b/zone/raids.h index e1d1cadb3..141fd17ca 100644 --- a/zone/raids.h +++ b/zone/raids.h @@ -88,6 +88,10 @@ struct RaidMember{ bool IsGroupLeader; bool IsRaidLeader; bool IsLooter; +#ifdef BOTS + bool IsBot = false; + bool IsRaidMainAssistOne = false; +#endif }; struct GroupMentor { @@ -112,6 +116,11 @@ public: bool IsRaid() { return true; } void AddMember(Client *c, uint32 group = 0xFFFFFFFF, bool rleader=false, bool groupleader=false, bool looter=false); +#ifdef BOTS + void AddBot(Bot* b, uint32 group = 0xFFFFFFFF, bool rleader=false, bool groupleader=false, bool looter=false); + void RaidBotGroupSay(Bot* b, uint8 language, uint8 lang_skill, const char* msg, ...); //Not yet implemented + Mob* GetRaidMainAssistOneByName(const char* name); +#endif void RemoveMember(const char *c); void DisbandRaid(); void MoveMember(const char *name, uint32 newGroup); @@ -121,6 +130,7 @@ public: bool IsGroupLeader(const char *who); bool IsRaidMember(const char *name); void UpdateLevel(const char *name, int newLevel); + uint32 GetFreeGroup(); uint8 GroupCount(uint32 gid); @@ -241,6 +251,7 @@ public: bool DoesAnyMemberHaveExpeditionLockout(const std::string& expedition_name, const std::string& event_name, int max_check_count = 0); std::vector GetMembers() const; + std::vector GetRaidGroupMembers(uint32 gid); RaidMember members[MAX_RAID_MEMBERS]; char leadername[64]; diff --git a/zone/spawn2.cpp b/zone/spawn2.cpp index d314137bd..3b6d1b94a 100644 --- a/zone/spawn2.cpp +++ b/zone/spawn2.cpp @@ -154,7 +154,7 @@ bool Spawn2::Process() { return true; } - if (timer.Check()) { + if (timer.Check() && zone->spawn2_timer.Enabled()) { timer.Disable(); LogSpawns("Spawn2 [{}]: Timer has triggered", spawn2_id); diff --git a/zone/zone.cpp b/zone/zone.cpp index f796b8416..7e0f2fc06 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -1742,6 +1742,7 @@ bool Zone::Depop(bool StartSpawnTimer) { itr = npctable.begin(); delete itr->second; itr->second = nullptr; + npctable.erase(itr); }