From 2ce89939860740d1731589ec7b5a43bacfe3be5b Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:37:02 -0700 Subject: [PATCH 01/17] Correct Loot rolling behavior (#2190) # Pull Request This fixes the loot rolling behavior issue created by #2068 . Introduce the ability for enchanter bots to disenchant items they dont need, and roll need on recipes they also need. Make it so ITEM_USAGE_AH ensures the item is not BOP. Try to reduce the call for item_usage in CalculateRollVote by passing usage if available. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? -- Add a new check that downgrades greed rolls to desired levels, or bools for the other two options. - Describe the **cheapest implementation** that produces an acceptable result? -- As implemented. - Describe the **runtime cost** when this logic executes across many bots? -- Same as before. Item usage is the heaviest part, and that hasnt changed to accommodate this. --- ## How to Test the Changes - multiple bots in a group with group loot on, do a dungeon or something. One bot should be an enchanter. ## Complexity & Impact Does this change add new decision branches? - - [ ] No - - [x] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [X] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [X] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [X] Yes (**explain why**) - - - Corrects the looting behavior to original design. If this introduces more advanced or AI-heavy logic: - - [ ] Lightweight mode remains the default - - [X] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- conf/playerbots.conf.dist | 19 ++++++- src/Ai/Base/Actions/LootRollAction.cpp | 78 +++++++++++--------------- src/Ai/Base/Actions/LootRollAction.h | 2 +- src/Ai/Base/Value/ItemUsageValue.cpp | 3 +- src/PlayerbotAIConfig.cpp | 5 +- src/PlayerbotAIConfig.h | 5 +- 6 files changed, 60 insertions(+), 52 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 0f9572621..52e80e680 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -298,9 +298,24 @@ AiPlayerbot.TwoRoundsGearInit = 0 # Default: 0 (disabled) AiPlayerbot.FreeMethodLoot = 0 -# Bots' loot roll level (0 = pass, 1 = greed, 2 = need) +# Bots' Roll level bots will use for items they Need (0 = pass, 1 = greed, 2 = need) # Default: 1 (greed) -AiPlayerbot.LootRollLevel = 1 +AiPlayerbot.LootNeedRollLevel = 1 + +# Enable bots to roll GREED on items (global toggle) +# If disabled, bots will PASS instead of GREED on all items +# Default: 0 (disabled - bots only NEED or PASS) +AiPlayerbot.LootGreedRollLevel = 0 + +# Enable bots to roll on recipes. Will NEED on learnable profession recipes they don't already know +# Bots will roll GREED on BoE recipes they can't learn if LootRollGreed is enabled. +# Default: 0 (disabled) +AiPlayerbot.LootRollRecipe = 0 + +# Bots with enchanting will roll DISENCHANT instead of GREED on disenchantable items +# If disabled, bots will GREED on disenchantable items instead +# Default: 0 (disabled) +AiPlayerbot.LootRollDisenchant = 0 # # diff --git a/src/Ai/Base/Actions/LootRollAction.cpp b/src/Ai/Base/Actions/LootRollAction.cpp index a3bf13041..749f47b9d 100644 --- a/src/Ai/Base/Actions/LootRollAction.cpp +++ b/src/Ai/Base/Actions/LootRollAction.cpp @@ -22,10 +22,10 @@ bool LootRollAction::Execute(Event /*event*/) std::vector rolls = group->GetRolls(); for (Roll*& roll : rolls) { - if (roll->playerVote.find(bot->GetGUID())->second != NOT_EMITED_YET) - { + auto voteItr = roll->playerVote.find(bot->GetGUID()); + if (voteItr == roll->playerVote.end() || voteItr->second != NOT_EMITED_YET) continue; - } + ObjectGuid guid = roll->itemGUID; uint32 itemId = roll->itemid; int32 randomProperty = 0; @@ -41,27 +41,22 @@ bool LootRollAction::Execute(Event /*event*/) std::string itemUsageParam; if (randomProperty != 0) - { itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); - } else - { itemUsageParam = std::to_string(itemId); - } + ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); // Armor Tokens are classed as MISC JUNK (Class 15, Subclass 0), luckily no other items I found have class bits and epic quality. if (proto->Class == ITEM_CLASS_MISC && proto->SubClass == ITEM_SUBCLASS_JUNK && proto->Quality == ITEM_QUALITY_EPIC) { if (CanBotUseToken(proto, bot)) - { vote = NEED; // Eligible for "Need" - } else - { vote = GREED; // Not eligible, so "Greed" - } } + else if (usage == ITEM_USAGE_DISENCHANT) + vote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; else { switch (proto->Class) @@ -69,40 +64,34 @@ bool LootRollAction::Execute(Event /*event*/) case ITEM_CLASS_WEAPON: case ITEM_CLASS_ARMOR: if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE || usage == ITEM_USAGE_BAD_EQUIP) - { vote = NEED; - } else if (usage != ITEM_USAGE_NONE) - { vote = GREED; - } + break; + case ITEM_CLASS_RECIPE: + if (!sPlayerbotAIConfig.lootRollRecipe) + vote = PASS; + else if (usage == ITEM_USAGE_SKILL) + vote = NEED; // Bot can learn this recipe + else if (proto->Bonding != BIND_WHEN_PICKED_UP) + vote = GREED; // BoE recipe bot can't learn - GREED for AH/trade break; default: if (StoreLootAction::IsLootAllowed(itemId, botAI)) - vote = CalculateRollVote(proto); // Ensure correct Need/Greed behavior + vote = CalculateRollVote(proto, usage); break; } } - if (sPlayerbotAIConfig.lootRollLevel == 0) + if (vote == NEED) { + if (sPlayerbotAIConfig.lootNeedRollLevel == 0 || RollUniqueCheck(proto, bot)) + vote = PASS; + else if (sPlayerbotAIConfig.lootNeedRollLevel == 1) + vote = GREED; + } + else if (vote == GREED && !sPlayerbotAIConfig.lootGreedRollLevel) vote = PASS; - } - else if (sPlayerbotAIConfig.lootRollLevel == 1) - { - // Level 1 = "greed" mode: bots greed on useful items but never need - // Only downgrade NEED to GREED, preserve GREED votes as-is - if (vote == NEED) - { - if (RollUniqueCheck(proto, bot)) - { - vote = PASS; - } - else - { - vote = GREED; - } - } - } + switch (group->GetLootMethod()) { case MASTER_LOOT: @@ -120,11 +109,14 @@ bool LootRollAction::Execute(Event /*event*/) return false; } -RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) +RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto, ItemUsage usage) { - std::ostringstream out; - out << proto->ItemId; - ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + if (usage == ITEM_USAGE_NONE) + { + std::ostringstream out; + out << proto->ItemId; + usage = AI_VALUE2(ItemUsage, "item usage", out.str()); + } RollVote needVote = PASS; switch (usage) @@ -137,11 +129,13 @@ RollVote LootRollAction::CalculateRollVote(ItemTemplate const* proto) break; case ITEM_USAGE_SKILL: case ITEM_USAGE_USE: - case ITEM_USAGE_DISENCHANT: case ITEM_USAGE_AH: case ITEM_USAGE_VENDOR: needVote = GREED; break; + case ITEM_USAGE_DISENCHANT: + needVote = sPlayerbotAIConfig.lootRollDisenchant ? DISENCHANT : GREED; + break; default: break; } @@ -195,9 +189,7 @@ bool CanBotUseToken(ItemTemplate const* proto, Player* bot) // Check if the bot's class is allowed to use the token if (proto->AllowableClass & botClassMask) - { return true; // Bot's class is eligible to use this token - } return false; // Bot's class cannot use this token } @@ -213,13 +205,9 @@ bool RollUniqueCheck(ItemTemplate const* proto, Player* bot) // Determine if the unique item is already equipped bool isEquipped = (totalItemCount > bagItemCount); if (isEquipped && proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE)) - { return true; // Unique Item is already equipped - } else if (proto->HasFlag(ITEM_FLAG_UNIQUE_EQUIPPABLE) && (bagItemCount > 1)) - { return true; // Unique item already in bag, don't roll for it - } return false; // Item is not equipped or in bags, roll for it } diff --git a/src/Ai/Base/Actions/LootRollAction.h b/src/Ai/Base/Actions/LootRollAction.h index 13d895860..63d2c4b9d 100644 --- a/src/Ai/Base/Actions/LootRollAction.h +++ b/src/Ai/Base/Actions/LootRollAction.h @@ -22,7 +22,7 @@ public: bool Execute(Event event) override; protected: - RollVote CalculateRollVote(ItemTemplate const* proto); + RollVote CalculateRollVote(ItemTemplate const* proto, ItemUsage usage = ITEM_USAGE_NONE); }; bool CanBotUseToken(ItemTemplate const* proto, Player* bot); diff --git a/src/Ai/Base/Value/ItemUsageValue.cpp b/src/Ai/Base/Value/ItemUsageValue.cpp index 7e5aae87c..b651af956 100644 --- a/src/Ai/Base/Value/ItemUsageValue.cpp +++ b/src/Ai/Base/Value/ItemUsageValue.cpp @@ -153,9 +153,8 @@ ItemUsage ItemUsageValue::Calculate() // Need to add something like free bagspace or item value. if (proto->SellPrice > 0) { - if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound) + if (proto->Quality >= ITEM_QUALITY_NORMAL && !isSoulbound && proto->Bonding != BIND_WHEN_PICKED_UP) return ITEM_USAGE_AH; - else return ITEM_USAGE_VENDOR; } diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 45551ab1a..fb0ffad4d 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -620,7 +620,10 @@ bool PlayerbotAIConfig::Initialize() // SPP automation freeMethodLoot = sConfigMgr->GetOption("AiPlayerbot.FreeMethodLoot", false); - lootRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootRollLevel", 1); + lootNeedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootNeedRollLevel", 1); + lootRollRecipe = sConfigMgr->GetOption("AiPlayerbot.LootRollRecipe", false); + lootRollDisenchant = sConfigMgr->GetOption("AiPlayerbot.LootRollDisenchant", false); + lootGreedRollLevel = sConfigMgr->GetOption("AiPlayerbot.LootGreedRollLevel", false); autoPickReward = sConfigMgr->GetOption("AiPlayerbot.AutoPickReward", "yes"); autoEquipUpgradeLoot = sConfigMgr->GetOption("AiPlayerbot.AutoEquipUpgradeLoot", true); equipUpgradeThreshold = sConfigMgr->GetOption("AiPlayerbot.EquipUpgradeThreshold", 1.1f); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index efde98e10..7b9b2fe81 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -346,7 +346,10 @@ public: uint32 botActiveAloneSmartScaleWhenMaxLevel; bool freeMethodLoot; - int32 lootRollLevel; + int32 lootNeedRollLevel; + bool lootGreedRollLevel; + bool lootRollRecipe; + bool lootRollDisenchant; std::string autoPickReward; bool autoEquipUpgradeLoot; float equipUpgradeThreshold; From 473b2ab5c6f42c5450361f119a08a9642e6a68d4 Mon Sep 17 00:00:00 2001 From: XYUU Date: Fri, 20 Mar 2026 20:37:44 +0100 Subject: [PATCH 02/17] Fix: WLK shaman totem quest vs relic totems: avoid keeping 4 totem items when relic exists #2119 (#2197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Detects shaman relics (relic type, totem subclass) in bags/equipment. * Skips adding the four classic totem items (5175–5178) when a relic exists. * Cleans up any existing totem items from bags/equipment/bank when a relic exists, while keeping Ankh handling intact. ## Test plan Verified manually (local environment). --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar Co-authored-by: github-actions --- src/Bot/Factory/PlayerbotFactory.cpp | 32 ++++++++++++++++++++++------ src/Mgr/Item/ItemVisitors.h | 23 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 9e1b30a73..9bf987fef 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -3342,18 +3342,36 @@ void PlayerbotFactory::InitReagents() items.push_back({44615, 40}); // Devout Candle break; case CLASS_SHAMAN: - if (level >= 4) - items.push_back({5175, 1}); // Earth Totem - if (level >= 10) - items.push_back({5176, 1}); // Flame Totem - if (level >= 20) - items.push_back({5177, 1}); // Water Totem + { + HasRelicBySubclassVisitor relicVisitor(ITEM_SUBCLASS_ARMOR_TOTEM); + IterateItems(&relicVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP)); + bool hasRelic = relicVisitor.found; + + if (!hasRelic) + { + if (level >= 4) + items.push_back({5175, 1}); // Earth Totem + if (level >= 10) + items.push_back({5176, 1}); // Flame Totem + if (level >= 20) + items.push_back({5177, 1}); // Water Totem + } + else + { + ItemIds totemIds = {5175, 5176, 5177, 5178}; + FindItemByIdsVisitor totemVisitor(totemIds); + IterateItems(&totemVisitor, (IterateItemsMask)(ITERATE_ITEMS_IN_BAGS | ITERATE_ITEMS_IN_EQUIP | ITERATE_ITEMS_IN_BANK)); + for (Item* item : totemVisitor.GetResult()) + bot->DestroyItem(item->GetBagSlot(), item->GetSlot(), true); + } if (level >= 30) { - items.push_back({5178, 1}); // Air Totem + if (!hasRelic) + items.push_back({5178, 1}); // Air Totem items.push_back({17030, 20}); // Ankh } break; + } case CLASS_WARLOCK: items.push_back({6265, 5}); // Soul Shard break; diff --git a/src/Mgr/Item/ItemVisitors.h b/src/Mgr/Item/ItemVisitors.h index e42b14f89..930aa1f4a 100644 --- a/src/Mgr/Item/ItemVisitors.h +++ b/src/Mgr/Item/ItemVisitors.h @@ -66,6 +66,29 @@ private: Player* bot; }; +class HasRelicBySubclassVisitor : public IterateItemsVisitor +{ +public: + HasRelicBySubclassVisitor(uint32 subClass) : subClass(subClass) {} + + bool Visit(Item* item) override + { + ItemTemplate const* proto = item->GetTemplate(); + if (proto && proto->InventoryType == INVTYPE_RELIC && proto->SubClass == subClass) + { + found = true; + return false; + } + + return true; + } + + bool found = false; + +private: + uint32 subClass; +}; + class FindItemsByQualityVisitor : public IterateItemsVisitor { public: From 35a0282ca677bcb58c36e480fd31fa544764ece7 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:38:06 -0500 Subject: [PATCH 03/17] Add Sense Undead for Paladins (#2200) # Pull Request This PR adds the sense undead ability for Paladins, which they will keep active at all times. This is mildly useful because the associated minor glyph provides a 1% damage increase against undead while the ability is active. Sense undead is also added to InitClassSpells(). I understand that it is a trainer spell so would normally be covered by InitAvailableSpells(), but those playing with mod-individual-progression will not receive the spell through InitAvailableSpells() because it is removed from trainers by the mod (in TBC, a quest was required to obtain the spell). Finally, the minor glyph of sense undead is now added to the config as a default glyph for all PvE specs. It is not added for PvP specs because Forsaken do not count as undead so the glyph is useless in PvP. I also made some other tweaks to Paladin default minor glyphs that are not worth spending any time talking about. Edit: I also did some minor reformatting of code and replaced some numbers with existing constants. --- ## Design Philosophy We prioritize **stability, performance, and predictability** over behavioral realism. Complex player-mimicking logic is intentionally limited due to its negative impact on scalability, maintainability, and long-term robustness. Excessive processing overhead can lead to server hiccups, increased CPU usage, and degraded performance for all participants. Because every action and decision tree is executed **per bot and per trigger**, even small increases in logic complexity can scale poorly and negatively affect both players and world (random) bots. Bots are not expected to behave perfectly, and perfect simulation of human decision-making is not a project goal. Increased behavioral realism often introduces disproportionate cost, reduced predictability, and significantly higher maintenance overhead. Every additional branch of logic increases long-term responsibility. All decision paths must be tested, validated, and maintained continuously as the system evolves. If advanced or AI-intensive behavior is introduced, the **default configuration must remain the lightweight decision model**. More complex behavior should only be available as an **explicit opt-in option**, clearly documented as having a measurable performance cost. Principles: - **Stability before intelligence** A stable system is always preferred over a smarter one. - **Performance is a shared resource** Any increase in bot cost affects all players and all bots. - **Simple logic scales better than smart logic** Predictable behavior under load is more valuable than perfect decisions. - **Complexity must justify itself** If a feature cannot clearly explain its cost, it should not exist. - **Defaults must be cheap** Expensive behavior must always be optional and clearly communicated. - **Bots should look reasonable, not perfect** The goal is believable behavior, not human simulation. Before submitting, confirm that this change aligns with those principles. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? The implementation just checks if a Paladin has the sense undead aura, and if not, the Paladin will activate sense undead. It is simple and cheap. --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [ ] No - - [x] Yes (**describe and justify impact**) Infinitesimally Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Paladin bots will by default have sense undead enabled. There is no disadvantage to this. If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- conf/playerbots.conf.dist | 12 ++--- .../Class/Paladin/Action/PaladinActions.cpp | 5 +- src/Ai/Class/Paladin/Action/PaladinActions.h | 40 +++++++-------- .../Class/Paladin/PaladinAiObjectContext.cpp | 4 ++ .../GenericPaladinNonCombatStrategy.cpp | 17 ++++--- .../Class/Paladin/Trigger/PaladinTriggers.cpp | 5 ++ .../Class/Paladin/Trigger/PaladinTriggers.h | 50 +++++++++---------- src/Bot/Factory/PlayerbotFactory.cpp | 18 +------ 8 files changed, 71 insertions(+), 80 deletions(-) diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 52e80e680..c1c06b8b7 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -1406,28 +1406,28 @@ AiPlayerbot.PremadeSpecLink.1.5.80 = 0502300123-3-250031220223012521332113321 # AiPlayerbot.PremadeSpecName.2.0 = holy pve -AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43369,43365,41109 +AiPlayerbot.PremadeSpecGlyph.2.0 = 41106,43367,45741,43368,43365,41109 AiPlayerbot.PremadeSpecLink.2.0.60 = 50350151020013053100515221 AiPlayerbot.PremadeSpecLink.2.0.80 = 50350152220013053100515221-503201312 AiPlayerbot.PremadeSpecName.2.1 = prot pve -AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43369,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.1 = 41099,43367,43869,43368,43369,45745 AiPlayerbot.PremadeSpecLink.2.1.60 = -05005135203102311333112321 AiPlayerbot.PremadeSpecLink.2.1.80 = -05005135203102311333312321-502302012003 AiPlayerbot.PremadeSpecName.2.2 = ret pve -AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43369,43365,43869 +AiPlayerbot.PremadeSpecGlyph.2.2 = 41092,43367,41099,43368,43369,43869 AiPlayerbot.PremadeSpecLink.2.2.60 = --05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.65 = -05-05230051203331302133231131 AiPlayerbot.PremadeSpecLink.2.2.80 = 050501-05-05232051203331302133231331 AiPlayerbot.PremadeSpecName.2.3 = holy pvp -AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43366,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.3 = 41110,43367,45746,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.3.60 = 50332150300013050133215221 AiPlayerbot.PremadeSpecLink.2.3.80 = 50332150300013050133315221-5032013122 AiPlayerbot.PremadeSpecName.2.4 = prot pvp -AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43369,41101,43368,43365,45745 +AiPlayerbot.PremadeSpecGlyph.2.4 = 41092,43367,41101,43369,43365,45745 AiPlayerbot.PremadeSpecLink.2.4.60 = -15320130223122311323311321 AiPlayerbot.PremadeSpecLink.2.4.80 = -15320130223122321333312321-052300502 AiPlayerbot.PremadeSpecName.2.5 = ret pvp -AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43369,41102,43368,43365,45747 +AiPlayerbot.PremadeSpecGlyph.2.5 = 41095,43367,41102,43369,43365,45747 AiPlayerbot.PremadeSpecLink.2.5.60 = --05230250203331222133201321 AiPlayerbot.PremadeSpecLink.2.5.80 = -1532013022-05230250203331322133201321 diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.cpp b/src/Ai/Class/Paladin/Action/PaladinActions.cpp index d0bfbabbf..c1521bb1e 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.cpp +++ b/src/Ai/Class/Paladin/Action/PaladinActions.cpp @@ -472,9 +472,8 @@ Unit* CastRighteousDefenseAction::GetTarget() { Unit* current_target = AI_VALUE(Unit*, "current target"); if (!current_target) - { - return NULL; - } + return nullptr; + return current_target->GetVictim(); } diff --git a/src/Ai/Class/Paladin/Action/PaladinActions.h b/src/Ai/Class/Paladin/Action/PaladinActions.h index 3bacc8846..c58c3209d 100644 --- a/src/Ai/Class/Paladin/Action/PaladinActions.h +++ b/src/Ai/Class/Paladin/Action/PaladinActions.h @@ -91,9 +91,8 @@ public: class CastBlessingOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) : BuffOnPartyAction(botAI, name), name(name) - { - } + CastBlessingOnPartyAction(PlayerbotAI* botAI, std::string const name) + : BuffOnPartyAction(botAI, name), name(name) {} Value* GetTargetValue() override; @@ -154,9 +153,7 @@ public: class CastBlessingOfSanctuaryOnPartyAction : public BuffOnPartyAction { public: - CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") - { - } + CastBlessingOfSanctuaryOnPartyAction(PlayerbotAI* botAI) : BuffOnPartyAction(botAI, "blessing of sanctuary") {} std::string const getName() override { return "blessing of sanctuary on party"; } Value* GetTargetValue() override; @@ -173,18 +170,14 @@ class CastHolyShockOnPartyAction : public HealPartyMemberAction { public: CastHolyShockOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) - { - } + : HealPartyMemberAction(botAI, "holy shock", 25.0f, HealingManaEfficiency::LOW) {} }; class CastHolyLightOnPartyAction : public HealPartyMemberAction { public: CastHolyLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) - { - } + : HealPartyMemberAction(botAI, "holy light", 50.0f, HealingManaEfficiency::MEDIUM) {} }; class CastFlashOfLightAction : public CastHealingSpellAction @@ -197,9 +190,7 @@ class CastFlashOfLightOnPartyAction : public HealPartyMemberAction { public: CastFlashOfLightOnPartyAction(PlayerbotAI* botAI) - : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) - { - } + : HealPartyMemberAction(botAI, "flash of light", 15.0f, HealingManaEfficiency::HIGH) {} }; class CastLayOnHandsAction : public CastHealingSpellAction @@ -357,9 +348,7 @@ class CastHammerOfJusticeOnEnemyHealerAction : public CastSpellOnEnemyHealerActi { public: CastHammerOfJusticeOnEnemyHealerAction(PlayerbotAI* botAI) - : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") - { - } + : CastSpellOnEnemyHealerAction(botAI, "hammer of justice") {} }; class CastHammerOfJusticeSnareAction : public CastSnareSpellAction @@ -368,6 +357,12 @@ public: CastHammerOfJusticeSnareAction(PlayerbotAI* botAI) : CastSnareSpellAction(botAI, "hammer of justice") {} }; +class CastSenseUndeadAction : public CastBuffSpellAction +{ +public: + CastSenseUndeadAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "sense undead") {} +}; + class CastTurnUndeadAction : public CastBuffSpellAction { public: @@ -381,25 +376,25 @@ PROTECT_ACTION(CastBlessingOfProtectionProtectAction, "blessing of protection"); class CastDivinePleaAction : public CastBuffSpellAction { public: - CastDivinePleaAction(PlayerbotAI* ai) : CastBuffSpellAction(ai, "divine plea") {} + CastDivinePleaAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "divine plea") {} }; class ShieldOfRighteousnessAction : public CastMeleeSpellAction { public: - ShieldOfRighteousnessAction(PlayerbotAI* ai) : CastMeleeSpellAction(ai, "shield of righteousness") {} + ShieldOfRighteousnessAction(PlayerbotAI* botAI) : CastMeleeSpellAction(botAI, "shield of righteousness") {} }; class CastBeaconOfLightOnMainTankAction : public BuffOnMainTankAction { public: - CastBeaconOfLightOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "beacon of light", true) {} + CastBeaconOfLightOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "beacon of light", true) {} }; class CastSacredShieldOnMainTankAction : public BuffOnMainTankAction { public: - CastSacredShieldOnMainTankAction(PlayerbotAI* ai) : BuffOnMainTankAction(ai, "sacred shield", false) {} + CastSacredShieldOnMainTankAction(PlayerbotAI* botAI) : BuffOnMainTankAction(botAI, "sacred shield", false) {} }; class CastAvengingWrathAction : public CastBuffSpellAction @@ -428,4 +423,5 @@ public: bool Execute(Event event) override; bool isUseful() override; }; + #endif diff --git a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp index 45f676ec9..ed8f4931b 100644 --- a/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp +++ b/src/Ai/Class/Paladin/PaladinAiObjectContext.cpp @@ -132,6 +132,7 @@ public: &PaladinTriggerFactoryInternal::hammer_of_justice_on_enemy_target; creators["hammer of justice on snare target"] = &PaladinTriggerFactoryInternal::hammer_of_justice_on_snare_target; + creators["not sensing undead"] = &PaladinTriggerFactoryInternal::not_sensing_undead; creators["divine favor"] = &PaladinTriggerFactoryInternal::divine_favor; creators["turn undead"] = &PaladinTriggerFactoryInternal::turn_undead; creators["avenger's shield"] = &PaladinTriggerFactoryInternal::avenger_shield; @@ -151,6 +152,7 @@ public: } private: + static Trigger* not_sensing_undead(PlayerbotAI* botAI) { return new NotSensingUndeadTrigger(botAI); } static Trigger* turn_undead(PlayerbotAI* botAI) { return new TurnUndeadTrigger(botAI); } static Trigger* divine_favor(PlayerbotAI* botAI) { return new DivineFavorTrigger(botAI); } static Trigger* holy_shield(PlayerbotAI* botAI) { return new HolyShieldTrigger(botAI); } @@ -288,6 +290,7 @@ public: creators["hammer of justice on snare target"] = &PaladinAiObjectContextInternal::hammer_of_justice_on_snare_target; creators["divine favor"] = &PaladinAiObjectContextInternal::divine_favor; + creators["sense undead"] = &PaladinAiObjectContextInternal::sense_undead; creators["turn undead"] = &PaladinAiObjectContextInternal::turn_undead; creators["blessing of protection on party"] = &PaladinAiObjectContextInternal::blessing_of_protection_on_party; creators["righteous defense"] = &PaladinAiObjectContextInternal::righteous_defense; @@ -312,6 +315,7 @@ private: { return new CastBlessingOfProtectionProtectAction(botAI); } + static Action* sense_undead(PlayerbotAI* botAI) { return new CastSenseUndeadAction(botAI); } static Action* turn_undead(PlayerbotAI* botAI) { return new CastTurnUndeadAction(botAI); } static Action* divine_favor(PlayerbotAI* botAI) { return new CastDivineFavorAction(botAI); } static Action* righteous_fury(PlayerbotAI* botAI) { return new CastRighteousFuryAction(botAI); } diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp index 7f9919e06..670d7e629 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinNonCombatStrategy.cpp @@ -19,14 +19,15 @@ void GenericPaladinNonCombatStrategy::InitTriggers(std::vector& tr NonCombatStrategy::InitTriggers(triggers); triggers.push_back(new TriggerNode("party member dead", { NextAction("redemption", ACTION_CRITICAL_HEAL + 10) })); - triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", 25.0f) })); - triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", 26.0f) })); - triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", 27.0f) })); - triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", 28.0f) })); + triggers.push_back(new TriggerNode("party member almost full health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 5.0f) })); + triggers.push_back(new TriggerNode("party member medium health", { NextAction("flash of light on party", ACTION_MEDIUM_HEAL + 6.0f) })); + triggers.push_back(new TriggerNode("party member low health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 7.0f) })); + triggers.push_back(new TriggerNode("party member critical health", { NextAction("holy light on party", ACTION_MEDIUM_HEAL + 8.0f) })); + triggers.push_back(new TriggerNode("not sensing undead", { NextAction("sense undead", ACTION_IDLE + 1.0f) })); int specTab = AiFactory::GetPlayerSpecTab(botAI->GetBot()); - if (specTab == 0 || specTab == 1) // Holy or Protection - triggers.push_back(new TriggerNode("often", { NextAction("apply oil", 1.0f) })); - if (specTab == 2) // Retribution - triggers.push_back(new TriggerNode("often", { NextAction("apply stone", 1.0f) })); + if (specTab == PALADIN_TAB_HOLY || specTab == PALADIN_TAB_PROTECTION) + triggers.push_back(new TriggerNode("often", { NextAction("apply oil", ACTION_IDLE + 1.0f) })); + if (specTab == PALADIN_TAB_RETRIBUTION) + triggers.push_back(new TriggerNode("often", { NextAction("apply stone", ACTION_IDLE + 1.0f) })); } diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp index 6c333bc7e..71328c4dc 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.cpp @@ -30,3 +30,8 @@ bool BlessingTrigger::IsActive() return SpellTrigger::IsActive() && !botAI->HasAnyAuraOf(target, "blessing of might", "blessing of wisdom", "blessing of kings", "blessing of sanctuary", nullptr); } + +bool NotSensingUndeadTrigger::IsActive() +{ + return !botAI->HasAura("sense undead", bot); +} diff --git a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h index 7352dbc81..f33b66890 100644 --- a/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h +++ b/src/Ai/Class/Paladin/Trigger/PaladinTriggers.h @@ -77,9 +77,7 @@ class BlessingOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of kings,blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingTrigger : public BuffTrigger @@ -93,7 +91,8 @@ public: class HammerOfJusticeInterruptSpellTrigger : public InterruptSpellTrigger { public: - HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) : InterruptSpellTrigger(botAI, "hammer of justice") {} + HammerOfJusticeInterruptSpellTrigger(PlayerbotAI* botAI) + : InterruptSpellTrigger(botAI, "hammer of justice") {} }; class HammerOfJusticeSnareTrigger : public SnareTargetTrigger @@ -144,9 +143,7 @@ class CleanseCurePartyMemberDiseaseTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberDiseaseTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_DISEASE) {} }; class CleanseCurePoisonTrigger : public NeedCureTrigger @@ -159,9 +156,7 @@ class CleanseCurePartyMemberPoisonTrigger : public PartyMemberNeedCureTrigger { public: CleanseCurePartyMemberPoisonTrigger(PlayerbotAI* botAI) - : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) - { - } + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_POISON) {} }; class CleanseCureMagicTrigger : public NeedCureTrigger @@ -173,15 +168,15 @@ public: class CleanseCurePartyMemberMagicTrigger : public PartyMemberNeedCureTrigger { public: - CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) - { - } + CleanseCurePartyMemberMagicTrigger(PlayerbotAI* botAI) + : PartyMemberNeedCureTrigger(botAI, "cleanse", DISPEL_MAGIC) {} }; class HammerOfJusticeEnemyHealerTrigger : public InterruptEnemyHealerTrigger { public: - HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} + HammerOfJusticeEnemyHealerTrigger(PlayerbotAI* botAI) + : InterruptEnemyHealerTrigger(botAI, "hammer of justice") {} }; class DivineFavorTrigger : public BuffTrigger @@ -190,6 +185,14 @@ public: DivineFavorTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "divine favor") {} }; +class NotSensingUndeadTrigger : public BuffTrigger +{ +public: + NotSensingUndeadTrigger(PlayerbotAI* botAI) : BuffTrigger(botAI, "not sensing undead") {} + + bool IsActive() override; +}; + class TurnUndeadTrigger : public HasCcTargetTrigger { public: @@ -201,7 +204,8 @@ DEBUFF_TRIGGER(AvengerShieldTrigger, "avenger's shield"); class BeaconOfLightOnMainTankTrigger : public BuffOnMainTankTrigger { public: - BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) : BuffOnMainTankTrigger(ai, "beacon of light", true) {} + BeaconOfLightOnMainTankTrigger(PlayerbotAI* ai) + : BuffOnMainTankTrigger(ai, "beacon of light", true) {} }; class SacredShieldOnMainTankTrigger : public BuffOnMainTankTrigger @@ -213,34 +217,29 @@ public: class BlessingOfKingsOnPartyTrigger : public BuffOnPartyTrigger { public: - BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} + BlessingOfKingsOnPartyTrigger(PlayerbotAI* botAI) + : BuffOnPartyTrigger(botAI, "blessing of kings", 2 * 2000) {} }; class BlessingOfWisdomOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfWisdomOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingOfMightOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfMightOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of might,blessing of wisdom", 2 * 2000) {} }; class BlessingOfSanctuaryOnPartyTrigger : public BuffOnPartyTrigger { public: BlessingOfSanctuaryOnPartyTrigger(PlayerbotAI* botAI) - : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) - { - } + : BuffOnPartyTrigger(botAI, "blessing of sanctuary", 2 * 2000) {} }; class AvengingWrathTrigger : public BoostTrigger @@ -248,4 +247,5 @@ class AvengingWrathTrigger : public BoostTrigger public: AvengingWrathTrigger(PlayerbotAI* botAI) : BoostTrigger(botAI, "avenging wrath") {} }; + #endif diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index 9bf987fef..c0e6d80db 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -2553,17 +2553,15 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(7386, false); // Sunder Armor } if (level >= 30) - { bot->learnSpell(2458, false); // Berserker Stance - } break; case CLASS_PALADIN: bot->learnSpell(21084, true); bot->learnSpell(635, true); if (level >= 12) - { bot->learnSpell(7328, false); // Redemption - } + if (level >= 20) + bot->learnSpell(5502, false); // Sense Undead break; case CLASS_ROGUE: bot->learnSpell(1752, true); @@ -2605,17 +2603,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(686, true); bot->learnSpell(688, false); // summon imp if (level >= 10) - { bot->learnSpell(697, false); // summon voidwalker - } if (level >= 20) - { bot->learnSpell(712, false); // summon succubus - } if (level >= 30) - { bot->learnSpell(691, false); // summon felhunter - } break; case CLASS_DRUID: bot->learnSpell(5176, true); @@ -2632,17 +2624,11 @@ void PlayerbotFactory::InitClassSpells() bot->learnSpell(331, true); // bot->learnSpell(66747, true); // Totem of the Earthen Ring if (level >= 4) - { bot->learnSpell(8071, false); // stoneskin totem - } if (level >= 10) - { bot->learnSpell(3599, false); // searing totem - } if (level >= 20) - { bot->learnSpell(5394, false); // healing stream totem - } break; default: break; From 4c0cb30f0b092275d4b4ad1f93f662d953133241 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:38:27 -0700 Subject: [PATCH 04/17] New readme (#2202) # Pull Request Add references to wiki with me detailed installation information, and troubleshooting page based on frequently observed issues in support. Also included are https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting --------- Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- README.md | 50 +++++++++++--------------------------------------- 1 file changed, 11 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index c1c179eef..1b5205168 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,11 @@ We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can ## Installation -Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. +Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support. -**All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below. +> **Important:** All `mod-playerbots` installations require a custom fork of AzerothCore: [mod-playerbots/azerothcore-wotlk (Playerbot branch)](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). The standard AzerothCore repository will **not** work. -### Cloning the Repositories - -To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following: +### Quick Start ```bash git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot @@ -46,44 +44,18 @@ cd azerothcore-wotlk/modules git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master ``` -For more information, refer to the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. +Then build the server following the platform-specific instructions in our **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)**. -### Docker Installation +> **Testing branch:** A `test-staging` branch is available with the latest features and fixes before they are merged into `master`. To use it, clone with `--branch=test-staging` instead. Note that this branch may contain unstable or breaking changes — use it at your own risk and only if you are comfortable troubleshooting issues. -Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module: +### Detailed Guides -```bash -git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot -cd azerothcore-wotlk/modules -git clone https://github.com/mod-playerbots/mod-playerbots.git --branch=master -``` +| Guide | Description | +|---|---| +| **[Installation Guide](https://github.com/mod-playerbots/mod-playerbots/wiki/Installation-Guide)** | Full step-by-step instructions for clean installs, migrating from existing AzerothCore, Docker setup, adding modules, and updating | +| **[Troubleshooting](https://github.com/mod-playerbots/mod-playerbots/wiki/Troubleshooting)** | Solutions to the most common build errors, database issues, configuration mistakes, crashes, and platform-specific problems | -Afterwards, create a `docker-compose.override.yml` file in the `azerothcore-wotlk` directory. This override file allows for mounting the modules directory to the `ac-worldserver` service which is required for it to run. Put the following inside and save: - -```yml -services: - ac-worldserver: - volumes: - - ./modules:/azerothcore/modules:ro -``` - -Additionally, this override file can be used to set custom configuration settings for `ac-worldserver` and any modules you install as environment variables: - -```yml -services: - ac-worldserver: - environment: - AC_RATE_XP_KILL: "1" - AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "1" - volumes: - - ./modules:/azerothcore/modules:ro -``` - -For example, to double the experience gain rate per kill, take the setting `Rate.XP.Kill = 1` from [woldserver.conf](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/src/server/apps/worldserver/worldserver.conf.dist), convert it to an environment variable, and change it to the desired setting in the override file to get `AC_RATE_XP_KILL: "2"`. If you wanted to disable random bots from logging in automatically, take the `AiPlayerbot.RandomBotAutologin = 1` setting from [playerbots.conf](https://github.com/mod-playerbots/mod-playerbots/blob/master/conf/playerbots.conf.dist) and do the same to get `AC_AI_PLAYERBOT_RANDOM_BOT_AUTOLOGIN: "0"`. For more information on how to configure Azerothcore, Playerbots, and other module settings as environment variables in Docker Compose, see the "Configuring AzerothCore in Containers" section in the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) guide. - -Before building, consider setting the database password. One way to do this is to create a `.env` file in the root `azerothcore-wotlk` directory using the [template](https://github.com/mod-playerbots/azerothcore-wotlk/blob/Playerbot/conf/dist/env.docker). This file also allows you to set the user and group Docker uses for the services in case you run into any permissions issues, which are the most common cause for Docker installation problems. - -Use `docker compose up -d --build` to build and run the server. For more information, including how to create an account and taking backups, refer to the [Install With Docker](https://www.azerothcore.org/wiki/install-with-docker) page. +For additional references, see the [AzerothCore Installation Guide](https://www.azerothcore.org/wiki/installation) and [Installing a Module](https://www.azerothcore.org/wiki/installing-a-module) pages. ## Documentation From 4877dcc5731aa302ae149bd42d38552bfc49b1c3 Mon Sep 17 00:00:00 2001 From: Aldori Date: Fri, 20 Mar 2026 15:38:57 -0400 Subject: [PATCH 05/17] fix: ByteBufferException error (opcode: 149) (#2206) Fixes #2204 ## Pull Request Description Fixes an opcode 149 ByteBufferException when Questie-335 (or other addons that send addon messages) is used in a party with Playerbots. The issue was caused by addon-language packets reaching parsing logic they should not have reached. This change adjusts the early return for `LANG_ADDON` packets before further handling. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Moved the early return for `LANG_ADDON` packets in the outgoing packet handler. - Describe the **processing cost** when this logic executes across many bots. - Negligible. It's a simple conditional check with an early return. ## How to Test the Changes 1. Install and enable Questie-335. 2. Invite at least 1 Playerbot to a party. 3. Accept a quest, abandon a quest, or progress a quest objective such as kill credit or looting a quest item. 4. Verify the worldserver no longer logs opcode 149 ByteBufferException errors. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [ ] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers This is a small fix intended only to prevent addon language packets from reaching incompatible packet parsing logic. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index c0e679f3e..6c89400b1 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1119,6 +1119,9 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (guid1.IsEmpty() || p.size() > p.DEFAULT_SIZE) return; + if (lang == LANG_ADDON) + return; + if (p.GetOpcode() == SMSG_GM_MESSAGECHAT) { p >> textLen; @@ -1168,8 +1171,6 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) if (HasRealPlayerMaster() && guid1 != GetMaster()->GetGUID()) return; - if (lang == LANG_ADDON) - return; if (message.starts_with(sPlayerbotAIConfig.toxicLinksPrefix) && (GetChatHelper()->ExtractAllItemIds(message).size() > 0 || From 5c63aacd6050b7e1aa276290429cc6b89b595fb5 Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:39:24 -0600 Subject: [PATCH 06/17] Drop server initialization time message. Show number of bots set to login. (#2209) ## Pull Request Description The server initialization time on login is neither relevant to the typical user, nor is it accurate. It simply takes `maxRandomBots`, does some arbitrary multiplications and divisions on it, and declare that as the time it takes the server to load. It does not take into account any other of your server configurations nor your server capabilities. Here we exchange that message with one more relevant to the user, telling them the number of logged in bot (or set to be logged in with DisabledWithoutRealPlayer enabled). But honestly, even removing that whole snippet is a better idea than keeping the misleading message. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. Alternatively the whole snippet can be removed. ## How to Test the Changes Login and see the welcome messages. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Script/Playerbots.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Script/Playerbots.cpp b/src/Script/Playerbots.cpp index af28027c9..7db91eb40 100644 --- a/src/Script/Playerbots.cpp +++ b/src/Script/Playerbots.cpp @@ -112,13 +112,10 @@ public: if (sPlayerbotAIConfig.enabled || sPlayerbotAIConfig.randomBotAutologin) { - std::string roundedTime = - std::to_string(std::ceil((sPlayerbotAIConfig.maxRandomBots * 0.11 / 60) * 10) / 10.0); - roundedTime = roundedTime.substr(0, roundedTime.find('.') + 2); + std::string maxAllowedBotCount = std::to_string(sRandomPlayerbotMgr.GetMaxAllowedBotCount()); ChatHandler(player->GetSession()).SendSysMessage( - "|cff00ff00Playerbots:|r bot initialization at server startup takes about '" - + roundedTime + "' minutes."); + "|cff00ff00Playerbots:|r The server is configured with " + maxAllowedBotCount + " bots."); } } } From 957eca0263232001c02476ea2b61576a7dceed9f Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:39:53 -0700 Subject: [PATCH 07/17] Feat. Enable multi node flying, and refactor into travel manager (#2156) # Pull Request Feature - Enable multi node flying for bots - Bots currently only do node to node flying. This PR makes it so they can connect multiple noted. -- This is enabled by sending a vector containing the node sequence instead of a single destination node -- To minimize the run-time cost of searching for available nodes and connection, a cache of all possible connections is prepared at start up using a BFS search algorithm. Refactor - Move all world destination logic (cities, banks, inns) to existing Travel manager - Eliminate flightmastercache and integrate to new manager - replace SQLs calls with in-memory data search by core - Add in new map that stores creature areas by template. Clean up - Move other rpg files to related folder. (Next steps) The selection for where bots fly to should be smarter than it is. Instead of trying to determine where a bot can go, it should first decide where it should go, and then identify the correct way to get there. --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? - Describe the **cheapest implementation** that produces an acceptable result? - Describe the **runtime cost** when this logic executes across many bots? --- ## How to Test the Changes - Step-by-step instructions to test the change - Any required setup (e.g. multiple players, bots, specific configuration) - Expected behavior and how to verify it ## Complexity & Impact Does this change add new decision branches? - - [x[ No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) The call itself is fairly infrequent, and although now there are a greater number of paths available for the bots, I dont think it would be significant. ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Gemini first suggested the use of a BFS algorithm. This was rewritten by me to actually work as intended. Verification by additional logging not present in final code. Claude code converted the SQL filtering to the atrocious if statements found in PrepareDestinationCache, but after verifying them it works. If there are better ways to do this Im open to it. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- src/Ai/World/Rpg/Action/NewRpgAction.cpp | 5 +- src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp | 82 +-- src/Ai/World/Rpg/Action/NewRpgBaseAction.h | 2 +- .../Rpg/Action}/RpgAction.cpp | 0 .../Actions => World/Rpg/Action}/RpgAction.h | 0 .../Rpg/Action}/RpgSubActions.cpp | 0 .../Rpg/Action}/RpgSubActions.h | 0 src/Ai/World/Rpg/NewRpgInfo.cpp | 9 +- src/Ai/World/Rpg/NewRpgInfo.h | 5 +- .../{Base/Actions => World/Rpg}/RpgValues.h | 0 .../Rpg}/Strategy/RpgStrategy.cpp | 0 .../Rpg}/Strategy/RpgStrategy.h | 0 src/Bot/PlayerbotAI.cpp | 2 +- src/Bot/PlayerbotAI.h | 2 +- src/Bot/RandomPlayerbotMgr.cpp | 489 +---------------- src/Bot/RandomPlayerbotMgr.h | 17 - src/Db/FlightMasterCache.cpp | 39 -- src/Db/FlightMasterCache.h | 36 -- src/Mgr/Travel/TravelMgr.cpp | 500 ++++++++++++++++++ src/Mgr/Travel/TravelMgr.h | 43 ++ src/Mgr/Travel/TravelNode.cpp | 125 +++++ src/Mgr/Travel/TravelNode.h | 14 + src/PlayerbotAIConfig.cpp | 2 + 23 files changed, 721 insertions(+), 651 deletions(-) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgAction.h (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.cpp (100%) rename src/Ai/{Base/Actions => World/Rpg/Action}/RpgSubActions.h (100%) rename src/Ai/{Base/Actions => World/Rpg}/RpgValues.h (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.cpp (100%) rename src/Ai/{Base => World/Rpg}/Strategy/RpgStrategy.h (100%) delete mode 100644 src/Db/FlightMasterCache.cpp delete mode 100644 src/Db/FlightMasterCache.h diff --git a/src/Ai/World/Rpg/Action/NewRpgAction.cpp b/src/Ai/World/Rpg/Action/NewRpgAction.cpp index 6820c6460..58846b949 100644 --- a/src/Ai/World/Rpg/Action/NewRpgAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgAction.cpp @@ -231,7 +231,6 @@ bool NewRpgDoQuestAction::Execute(Event /*event*/) return false; auto& data = *dataPtr; uint32 questId = data.questId; - const Quest* quest = data.quest; uint8 questStatus = bot->GetQuestStatus(questId); switch (questStatus) { @@ -438,7 +437,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (bot->GetDistance(flightMaster) > INTERACTION_DISTANCE) return MoveFarTo(flightMaster); - std::vector nodes = {data.fromNode, data.toNode}; + std::vector nodes = data.path; botAI->RemoveShapeshift(); if (bot->IsMounted()) @@ -447,7 +446,7 @@ bool NewRpgTravelFlightAction::Execute(Event /*event*/) if (!bot->ActivateTaxiPathTo(nodes, flightMaster, 0)) { LOG_DEBUG("playerbots", "[New RPG] {} active taxi path {} (from {} to {}) failed", bot->GetName(), - flightMaster->GetEntry(), nodes[0], nodes[1]); + flightMaster->GetEntry(), nodes[0], nodes[nodes.size() - 1]); botAI->rpgInfo.ChangeToIdle(); } return true; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp index 75392418b..b5156d6c1 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.cpp @@ -3,7 +3,6 @@ #include "BroadcastHelper.h" #include "ChatHelper.h" #include "Creature.h" -#include "FlightMasterCache.h" #include "G3D/Vector2.h" #include "GameObject.h" #include "GossipDef.h" @@ -856,7 +855,7 @@ bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) { - const std::vector& locs = sRandomPlayerbotMgr.locsPerLevelCache[bot->GetLevel()]; + const std::vector& locs = sTravelMgr.GetLocsPerLevelCache(bot->GetLevel()); float hiRange = 500.0f; float loRange = 2500.0f; if (bot->GetLevel() < 5) @@ -914,9 +913,7 @@ WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot) WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) { - const std::vector& locs = IsAlliance(bot->getRace()) - ? sRandomPlayerbotMgr.allianceStarterPerLevelCache[bot->GetLevel()] - : sRandomPlayerbotMgr.hordeStarterPerLevelCache[bot->GetLevel()]; + const std::vector locs = sTravelMgr.GetTravelHubs(bot); bool inCity = false; @@ -957,70 +954,19 @@ WorldPosition NewRpgBaseAction::SelectRandomCampPos(Player* bot) return dest; } -bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode) +bool NewRpgBaseAction::SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path) { - Creature* nearestFlightMaster = FlightMasterCache::Instance().GetNearestFlightMaster(bot); - if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + flightMaster = sTravelMgr.GetNearestFlightMasterGuid(bot); + if (!flightMaster) return false; - fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), - nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), - bot->GetTeamId()); - - if (!fromNode) + std::vector> availablePaths = sTravelMgr.GetOptimalFlightDestinations(bot); + if (availablePaths.empty()) return false; - std::vector availableToNodes; - for (uint32 i = 1; i < sTaxiNodesStore.GetNumRows(); ++i) - { - if (fromNode == i) - continue; - - TaxiNodesEntry const* node = sTaxiNodesStore.LookupEntry(i); - - // check map - if (!node || node->map_id != bot->GetMapId() || - (!node->MountCreatureID[bot->GetTeamId() == TEAM_ALLIANCE ? 1 : 0])) // dk flight - continue; - - // check taxi node known - if (!bot->isTaxiCheater() && !bot->m_taxi.IsTaximaskNodeKnown(i)) - continue; - - // check distance by level - if (!botAI->CheckLocationDistanceByLevel(bot, WorldLocation(node->map_id, node->x, node->y, node->z), false)) - continue; - - // check path - uint32 path, cost; - sObjectMgr->GetTaxiPath(fromNode, i, path, cost); - if (!path) - continue; - - // check area level - uint32 nodeZoneId = bot->GetMap()->GetZoneId(bot->GetPhaseMask(), node->x, node->y, node->z); - bool capital = false; - if (AreaTableEntry const* zone = sAreaTableStore.LookupEntry(nodeZoneId)) - { - capital = zone->flags & AREA_FLAG_CAPITAL; - } - - auto itr = sRandomPlayerbotMgr.zone2LevelBracket.find(nodeZoneId); - if (!capital && itr == sRandomPlayerbotMgr.zone2LevelBracket.end()) - continue; - - if (!capital && (bot->GetLevel() < itr->second.low || bot->GetLevel() > itr->second.high)) - continue; - - availableToNodes.push_back(i); - } - if (availableToNodes.empty()) - return false; - - flightMaster = nearestFlightMaster->GetGUID(); - toNode = availableToNodes[urand(0, availableToNodes.size() - 1)]; + path = availablePaths[urand(0, availablePaths.size() - 1)]; LOG_DEBUG("playerbots", "[New RPG] Bot {} select random flight taxi node from:{} (node {}) to:{} ({} available)", - bot->GetName(), flightMaster.GetEntry(), fromNode, toNode, availableToNodes.size()); + bot->GetName(), flightMaster.GetEntry(), path[0], path[path.size() - 1], availablePaths.size()); return true; } @@ -1121,10 +1067,10 @@ bool NewRpgBaseAction::RandomChangeStatus(std::vector candidateSta case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - if (SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode)) + std::vector path; + if (SelectRandomFlightTaxiNode(flightMaster, path)) { - botAI->rpgInfo.ChangeToTravelFlight(flightMaster, fromNode, toNode); + botAI->rpgInfo.ChangeToTravelFlight(flightMaster, path); return true; } return false; @@ -1197,8 +1143,8 @@ bool NewRpgBaseAction::CheckRpgStatusAvailable(NewRpgStatus status) case RPG_TRAVEL_FLIGHT: { ObjectGuid flightMaster; - uint32 fromNode, toNode; - return SelectRandomFlightTaxiNode(flightMaster, fromNode, toNode); + std::vector path; + return SelectRandomFlightTaxiNode(flightMaster, path); } default: return false; diff --git a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h index 910e5f494..9cd939eb7 100644 --- a/src/Ai/World/Rpg/Action/NewRpgBaseAction.h +++ b/src/Ai/World/Rpg/Action/NewRpgBaseAction.h @@ -54,7 +54,7 @@ protected: bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector& poiInfo, bool toComplete = false); static WorldPosition SelectRandomGrindPos(Player* bot); static WorldPosition SelectRandomCampPos(Player* bot); - bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, uint32& fromNode, uint32& toNode); + bool SelectRandomFlightTaxiNode(ObjectGuid& flightMaster, std::vector& path); bool RandomChangeStatus(std::vector candidateStatus); bool CheckRpgStatusAvailable(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgAction.cpp b/src/Ai/World/Rpg/Action/RpgAction.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgAction.cpp rename to src/Ai/World/Rpg/Action/RpgAction.cpp diff --git a/src/Ai/Base/Actions/RpgAction.h b/src/Ai/World/Rpg/Action/RpgAction.h similarity index 100% rename from src/Ai/Base/Actions/RpgAction.h rename to src/Ai/World/Rpg/Action/RpgAction.h diff --git a/src/Ai/Base/Actions/RpgSubActions.cpp b/src/Ai/World/Rpg/Action/RpgSubActions.cpp similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.cpp rename to src/Ai/World/Rpg/Action/RpgSubActions.cpp diff --git a/src/Ai/Base/Actions/RpgSubActions.h b/src/Ai/World/Rpg/Action/RpgSubActions.h similarity index 100% rename from src/Ai/Base/Actions/RpgSubActions.h rename to src/Ai/World/Rpg/Action/RpgSubActions.h diff --git a/src/Ai/World/Rpg/NewRpgInfo.cpp b/src/Ai/World/Rpg/NewRpgInfo.cpp index 20db8b049..4e04ab086 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.cpp +++ b/src/Ai/World/Rpg/NewRpgInfo.cpp @@ -37,13 +37,12 @@ void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest) data = do_quest; } -void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode) +void NewRpgInfo::ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path) { startT = getMSTime(); TravelFlight flight; flight.fromFlightMaster = fromFlightMaster; - flight.fromNode = fromNode; - flight.toNode = toNode; + flight.path = std::move(path); flight.inFlight = false; data = flight; } @@ -150,8 +149,8 @@ std::string NewRpgInfo::ToString() { out << "TRAVEL_FLIGHT"; out << "\nfromFlightMaster: " << arg.fromFlightMaster.GetEntry(); - out << "\nfromNode: " << arg.fromNode; - out << "\ntoNode: " << arg.toNode; + out << "\nfromNode: " << arg.path[0]; + out << "\ntoNode: " << arg.path[arg.path.size() - 1]; out << "\ninFlight: " << arg.inFlight; } else diff --git a/src/Ai/World/Rpg/NewRpgInfo.h b/src/Ai/World/Rpg/NewRpgInfo.h index 5b6ae3cb9..c2349c14b 100644 --- a/src/Ai/World/Rpg/NewRpgInfo.h +++ b/src/Ai/World/Rpg/NewRpgInfo.h @@ -50,8 +50,7 @@ struct NewRpgInfo struct TravelFlight { ObjectGuid fromFlightMaster{}; - uint32 fromNode{0}; - uint32 toNode{0}; + std::vector path; bool inFlight{false}; }; // RPG_REST @@ -91,7 +90,7 @@ struct NewRpgInfo void ChangeToWanderNpc(); void ChangeToWanderRandom(); void ChangeToDoQuest(uint32 questId, const Quest* quest); - void ChangeToTravelFlight(ObjectGuid fromFlightMaster, uint32 fromNode, uint32 toNode); + void ChangeToTravelFlight(ObjectGuid fromFlightMaster, std::vector path); void ChangeToRest(); void ChangeToIdle(); bool CanChangeTo(NewRpgStatus status); diff --git a/src/Ai/Base/Actions/RpgValues.h b/src/Ai/World/Rpg/RpgValues.h similarity index 100% rename from src/Ai/Base/Actions/RpgValues.h rename to src/Ai/World/Rpg/RpgValues.h diff --git a/src/Ai/Base/Strategy/RpgStrategy.cpp b/src/Ai/World/Rpg/Strategy/RpgStrategy.cpp similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.cpp rename to src/Ai/World/Rpg/Strategy/RpgStrategy.cpp diff --git a/src/Ai/Base/Strategy/RpgStrategy.h b/src/Ai/World/Rpg/Strategy/RpgStrategy.h similarity index 100% rename from src/Ai/Base/Strategy/RpgStrategy.h rename to src/Ai/World/Rpg/Strategy/RpgStrategy.h diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 6c89400b1..800247c6f 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -6487,7 +6487,7 @@ ChatChannelSource PlayerbotAI::GetChatChannelSource(Player* bot, uint32 type, st return ChatChannelSource::SRC_UNDEFINED; } -bool PlayerbotAI::CheckLocationDistanceByLevel(Player* player, const WorldLocation& loc, bool fromStartUp) +bool PlayerbotAI::StarterLevelDistanceCheck(Player* player, const WorldLocation& loc, bool fromStartUp) { if (player->GetLevel() > 16) return true; diff --git a/src/Bot/PlayerbotAI.h b/src/Bot/PlayerbotAI.h index b3a51b15c..5d64bf159 100644 --- a/src/Bot/PlayerbotAI.h +++ b/src/Bot/PlayerbotAI.h @@ -556,7 +556,7 @@ public: bool IsSafe(WorldObject* obj); ChatChannelSource GetChatChannelSource(Player* bot, uint32 type, std::string channelName); - bool CheckLocationDistanceByLevel(Player* player, const WorldLocation &loc, bool fromStartUp = false); + bool StarterLevelDistanceCheck(Player* player, const WorldLocation &loc, bool fromStartUp = false); bool HasCheat(BotCheatMask mask) { diff --git a/src/Bot/RandomPlayerbotMgr.cpp b/src/Bot/RandomPlayerbotMgr.cpp index bc1ffaad1..3fea369a5 100644 --- a/src/Bot/RandomPlayerbotMgr.cpp +++ b/src/Bot/RandomPlayerbotMgr.cpp @@ -23,7 +23,6 @@ #include "DatabaseEnv.h" #include "Define.h" #include "FleeManager.h" -#include "FlightMasterCache.h" #include "GridNotifiers.h" #include "LFGMgr.h" #include "MapMgr.h" @@ -47,9 +46,7 @@ #include "World.h" #include "Cell.h" #include "GridNotifiers.h" -// Required for Cell because of poor AC implementation #include "CellImpl.h" -// Required for GridNotifiers because of poor AC implementation #include "GridNotifiersImpl.h" struct GuidClassRaceInfo @@ -59,48 +56,6 @@ struct GuidClassRaceInfo uint32 rRace; }; -enum class CityId : uint8 { - STORMWIND, IRONFORGE, DARNASSUS, EXODAR, - ORGRIMMAR, UNDERCITY, THUNDER_BLUFF, SILVERMOON_CITY, - SHATTRATH_CITY, DALARAN -}; - -enum class FactionId : uint8 { ALLIANCE, HORDE, NEUTRAL }; - -// Map of banker entry → city + faction -static const std::unordered_map> bankerToCity = { - {2455, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2456, {CityId::STORMWIND, FactionId::ALLIANCE}}, {2457, {CityId::STORMWIND, FactionId::ALLIANCE}}, - {2460, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {2461, {CityId::IRONFORGE, FactionId::ALLIANCE}}, {5099, {CityId::IRONFORGE, FactionId::ALLIANCE}}, - {4155, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4208, {CityId::DARNASSUS, FactionId::ALLIANCE}}, {4209, {CityId::DARNASSUS, FactionId::ALLIANCE}}, - {17773, {CityId::EXODAR, FactionId::ALLIANCE}}, {18350, {CityId::EXODAR, FactionId::ALLIANCE}}, {16710, {CityId::EXODAR, FactionId::ALLIANCE}}, - {3320, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3309, {CityId::ORGRIMMAR, FactionId::HORDE}}, {3318, {CityId::ORGRIMMAR, FactionId::HORDE}}, - {4549, {CityId::UNDERCITY, FactionId::HORDE}}, {2459, {CityId::UNDERCITY, FactionId::HORDE}}, {2458, {CityId::UNDERCITY, FactionId::HORDE}}, {4550, {CityId::UNDERCITY, FactionId::HORDE}}, - {2996, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8356, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, {8357, {CityId::THUNDER_BLUFF, FactionId::HORDE}}, - {17631, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17632, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {17633, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {16615, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16616, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, {16617, {CityId::SILVERMOON_CITY, FactionId::HORDE}}, - {19246, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {19034, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, FactionId::NEUTRAL}}, - {30604, {CityId::DALARAN, FactionId::NEUTRAL}}, {30605, {CityId::DALARAN, FactionId::NEUTRAL}}, {30607, {CityId::DALARAN, FactionId::NEUTRAL}}, - {28675, {CityId::DALARAN, FactionId::NEUTRAL}}, {28676, {CityId::DALARAN, FactionId::NEUTRAL}}, {28677, {CityId::DALARAN, FactionId::NEUTRAL}} -}; - -// Map of city → available banker entries -static const std::unordered_map> cityToBankers = { - {CityId::STORMWIND, {2455, 2456, 2457}}, - {CityId::IRONFORGE, {2460, 2461, 5099}}, - {CityId::DARNASSUS, {4155, 4208, 4209}}, - {CityId::EXODAR, {17773, 18350, 16710}}, - {CityId::ORGRIMMAR, {3320, 3309, 3318}}, - {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, - {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, - {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, - {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, - {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} -}; - -// Quick lookup map: banker entry → location -static std::unordered_map bankerEntryToLocation; - void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); } void activatePrintStatsThread() @@ -1718,7 +1673,7 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& z = 0.05f + ground; - if (!botAI->CheckLocationDistanceByLevel(bot, loc, true)) + if (!botAI->StarterLevelDistanceCheck(bot, loc, true)) continue; const LocaleConstant& locale = sWorld->GetDefaultDbcLocale(); @@ -1762,329 +1717,6 @@ void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector& // tlocs.size()); } -void RandomPlayerbotMgr::PrepareZone2LevelBracket() -{ - // Classic WoW - Low - level zones - zone2LevelBracket[1] = {5, 12}; // Dun Morogh - zone2LevelBracket[12] = {5, 12}; // Elwynn Forest - zone2LevelBracket[14] = {5, 12}; // Durotar - zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades - zone2LevelBracket[141] = {5, 12}; // Teldrassil - zone2LevelBracket[215] = {5, 12}; // Mulgore - zone2LevelBracket[3430] = {5, 12}; // Eversong Woods - zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle - - // Classic WoW - Mid - level zones - zone2LevelBracket[17] = {10, 25}; // Barrens - zone2LevelBracket[38] = {10, 20}; // Loch Modan - zone2LevelBracket[40] = {10, 21}; // Westfall - zone2LevelBracket[130] = {10, 23}; // Silverpine Forest - zone2LevelBracket[148] = {10, 21}; // Darkshore - zone2LevelBracket[3433] = {10, 22}; // Ghostlands - zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle - - // Classic WoW - High - level zones - zone2LevelBracket[10] = {19, 33}; // Duskwood - zone2LevelBracket[11] = {21, 30}; // Wetlands - zone2LevelBracket[44] = {16, 28}; // Redridge Mountains - zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills - zone2LevelBracket[331] = {18, 33}; // Ashenvale - zone2LevelBracket[400] = {24, 36}; // Thousand Needles - zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains - - // Classic WoW - Higher - level zones - zone2LevelBracket[3] = {36, 46}; // Badlands - zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows - zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh - zone2LevelBracket[16] = {45, 52}; // Azshara - zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale - zone2LevelBracket[45] = {30, 42}; // Arathi Highlands - zone2LevelBracket[47] = {42, 51}; // Hinterlands - zone2LevelBracket[51] = {45, 51}; // Searing Gorge - zone2LevelBracket[357] = {40, 52}; // Feralas - zone2LevelBracket[405] = {30, 41}; // Desolace - zone2LevelBracket[440] = {41, 52}; // Tanaris - - // Classic WoW - Top - level zones - zone2LevelBracket[4] = {52, 57}; // Blasted Lands - zone2LevelBracket[28] = {50, 60}; // Western Plaguelands - zone2LevelBracket[46] = {51, 60}; // Burning Steppes - zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands - zone2LevelBracket[361] = {47, 57}; // Felwood - zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater - zone2LevelBracket[618] = {54, 61}; // Winterspring - zone2LevelBracket[1377] = {54, 63}; // Silithus - - // The Burning Crusade - Zones - zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula - zone2LevelBracket[3518] = {64, 70}; // Nagrand - zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest - zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley - zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh - zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains - zone2LevelBracket[3523] = {67, 73}; // Netherstorm - zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas - - // Wrath of the Lich King - Zones - zone2LevelBracket[65] = {71, 77}; // Dragonblight - zone2LevelBracket[66] = {74, 80}; // Zul'Drak - zone2LevelBracket[67] = {77, 80}; // Storm Peaks - zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier - zone2LevelBracket[394] = {72, 78}; // Grizzly Hills - zone2LevelBracket[495] = {68, 74}; // Howling Fjord - zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest - zone2LevelBracket[3537] = {68, 75}; // Borean Tundra - zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin - zone2LevelBracket[4197] = {79, 80}; // Wintergrasp - - // Override with values from config - for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) - { - zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; - } -} - -void RandomPlayerbotMgr::PrepareTeleportCache() -{ - uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); - - LOG_INFO("playerbots", "Preparing random teleport caches for {} levels...", maxLevel); - - QueryResult results = WorldDatabase.Query( - "SELECT " - "g.map, " - "position_x, " - "position_y, " - "position_z, " - "t.minlevel, " - "t.maxlevel " - "FROM " - "(SELECT " - "map, " - "MIN( c.guid ) guid " - "FROM " - "creature c " - "INNER JOIN creature_template t ON c.id1 = t.entry " - "WHERE " - "t.npcflag = 0 " - "AND t.lootid != 0 " - "AND t.maxlevel - t.minlevel < 3 " - "AND map IN ({}) " - "AND t.entry not in (32820, 24196, 30627, 30617) " - "AND c.spawntimesecs < 1000 " - "AND t.faction not in (11, 71, 79, 85, 188, 1575) " - "AND (t.unit_flags & 256) = 0 " - "AND (t.unit_flags & 4096) = 0 " - "AND t.rank = 0 " - // "AND (t.flags_extra & 32768) = 0 " - "GROUP BY " - "map, " - "ROUND(position_x / 50), " - "ROUND(position_y / 50), " - "ROUND(position_z / 50) " - "HAVING " - "count(*) >= 2) " - "AS g " - "INNER JOIN creature c ON g.guid = c.guid " - "INNER JOIN creature_template t on c.id1 = t.entry " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - uint32 collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - uint32 min_level = fields[4].Get(); - uint32 max_level = fields[5].Get(); - uint32 level = (min_level + max_level + 1) / 2; - WorldLocation loc(mapId, x, y, z, 0); - collected_locs++; - for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; - l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) - { - if (l < 1 || l > maxLevel) - { - continue; - } - locsPerLevelCache[(uint8)l].push_back(loc); - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} locations for level collected.", collected_locs); - - if (sPlayerbotAIConfig.enableNewRpgStrategy) - { - PrepareZone2LevelBracket(); - LOG_INFO("playerbots", "Preparing innkeepers / flightmasters locations for level..."); - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.faction, " - "t.entry, " - "t.npcflag, " - "c.guid " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 73728 " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 faction = fields[5].Get(); - uint32 tEntry = fields[6].Get(); - uint32 tNpcflag = fields[7].Get(); - uint32 guid = fields[8].Get(); - - if (tEntry == 3838 || tEntry == 29480) - continue; - - const FactionTemplateEntry* entry = sFactionTemplateStore.LookupEntry(faction); - - WorldLocation loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); - collected_locs++; - Map* map = sMapMgr->FindMap(loc.GetMapId(), 0); - if (!map) - continue; - bool forHorde = !(entry->hostileMask & 4); - bool forAlliance = !(entry->hostileMask & 2); - if (tNpcflag & UNIT_NPC_FLAG_FLIGHTMASTER) - { - WorldPosition pos(mapId, x, y, z, orient); - if (forHorde) - FlightMasterCache::Instance().AddHordeFlightMaster(guid, pos); - - if (forAlliance) - FlightMasterCache::Instance().AddAllianceFlightMaster(guid, pos); - } - const AreaTableEntry* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); - uint32 zoneId = area->zone ? area->zone : area->ID; - if (zone2LevelBracket.find(zoneId) == zone2LevelBracket.end()) - continue; - LevelBracket bracket = zone2LevelBracket[zoneId]; - for (int i = bracket.low; i <= bracket.high; i++) - { - if (forHorde) - hordeStarterPerLevelCache[i].push_back(loc); - if (forAlliance) - allianceStarterPerLevelCache[i].push_back(loc); - } - - } while (results->NextRow()); - } - - // add all initial position - for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) - { - for (uint32 j = 1; j < MAX_CLASSES; j++) - { - PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); - - if (!info) - continue; - - WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); - - for (int32 l = 1; l <= 5; l++) - { - if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) - allianceStarterPerLevelCache[(uint8)l].push_back(pos); - else - hordeStarterPerLevelCache[(uint8)l].push_back(pos); - } - break; - } - } - LOG_INFO("playerbots", ">> {} innkeepers locations for level collected.", collected_locs); - } - - results = WorldDatabase.Query( - "SELECT " - "map, " - "position_x, " - "position_y, " - "position_z, " - "orientation, " - "t.minlevel, " - "t.entry " - "FROM " - "creature c " - "INNER JOIN creature_template t on c.id1 = t.entry " - "WHERE " - "t.npcflag & 131072 " - "AND t.npcflag != 135298 " - "AND t.minlevel != 55 " - "AND t.minlevel != 65 " - "AND t.faction not in (35, 474, 69, 57) " - "AND t.entry not in (30606, 30608, 29282) " - "AND map IN ({}) " - "ORDER BY " - "t.minlevel;", - sPlayerbotAIConfig.randomBotMapsAsString.c_str()); - collected_locs = 0; - if (results) - { - do - { - Field* fields = results->Fetch(); - uint16 mapId = fields[0].Get(); - float x = fields[1].Get(); - float y = fields[2].Get(); - float z = fields[3].Get(); - float orient = fields[4].Get(); - uint32 level = fields[5].Get(); - uint32 entry = fields[6].Get(); - BankerLocation bLoc; - bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); - bLoc.entry = entry; - collected_locs++; - for (int32 l = 1; l <= maxLevel; l++) - { - // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) - if (l <=60 && level > 45) - { - continue; - } - // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) - if ((l >=61 && l <=70) && (level < 60 || level > 70)) - { - continue; - } - // Bots 71+ go to Dalaran bankers (all have minlevel 75) - if ((l >=71) && level != 75) - { - continue; - } - bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); - bankerEntryToLocation[bLoc.entry] = bLoc.loc; - } - } while (results->NextRow()); - } - LOG_INFO("playerbots", ">> {} banker locations for level collected.", collected_locs); -} - void RandomPlayerbotMgr::PrepareAddclassCache() { // Using accounts marked as type 2 (AddClass) @@ -2125,11 +1757,6 @@ void RandomPlayerbotMgr::Init() if (sPlayerbotAIConfig.addClassCommand) sRandomPlayerbotMgr.PrepareAddclassCache(); - if (sPlayerbotAIConfig.enabled) - { - sRandomPlayerbotMgr.PrepareTeleportCache(); - } - if (sPlayerbotAIConfig.randomBotJoinBG) sRandomPlayerbotMgr.LoadBattleMastersCache(); @@ -2141,103 +1768,17 @@ void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; - if (level >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + std::vector locs = sTravelMgr.GetCityLocations(bot); + if (!locs.empty()) { - std::vector fallbackLocs; - for (auto& bLoc : bankerLocsPerLevelCache[level]) - fallbackLocs.push_back(bLoc.loc); - - if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Collect valid cities based on bot faction. - std::unordered_set validBankerCities; - for (auto& loc : bankerLocsPerLevelCache[level]) - { - auto cityIt = bankerToCity.find(loc.entry); - if (cityIt == bankerToCity.end()) continue; - - CityId cityId = cityIt->second.first; - FactionId cityFactionId = cityIt->second.second; - - if ((IsAlliance(bot->getRace()) && cityFactionId == FactionId::ALLIANCE) || - (!IsAlliance(bot->getRace()) && cityFactionId == FactionId::HORDE) || - (cityFactionId == FactionId::NEUTRAL)) - { - validBankerCities.insert(cityId); - } - } - - // Fallback if no valid cities - if (validBankerCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Apply weights to valid cities - std::vector weightedCities; - for (CityId city : validBankerCities) - { - int weight = 0; - switch (city) - { - case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; - case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; - case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; - case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; - case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; - case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; - case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; - case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; - case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; - case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; - default: weight = 0; break; - } - if (weight <= 0) continue; - - for (int i = 0; i < weight; ++i) - { - weightedCities.push_back(city); - } - } - - // Fallback if no valid cities - if (weightedCities.empty()) - { - RandomTeleport(bot, fallbackLocs, true); - return; - } - - // Pick a weighted city randomly, then a random banker in that city - // then teleport to that banker - CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; - auto const& bankers = cityToBankers.at(selectedCity); - uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; - auto locIt = bankerEntryToLocation.find(selectedBankerEntry); - if (locIt != bankerEntryToLocation.end()) - { - std::vector teleportTarget = { locIt->second }; - RandomTeleport(bot, teleportTarget, true); - return; - } - - // Fallback if something went wrong - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, true); + return; } - else + locs = sTravelMgr.GetTeleportLocations(bot); + if (!locs.empty()) { - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs, false); + return; } } @@ -2246,17 +1787,11 @@ void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot) if (bot->InBattleground()) return; - uint32 level = bot->GetLevel(); - uint8 race = bot->getRace(); - std::vector* locs = nullptr; - if (sPlayerbotAIConfig.enableNewRpgStrategy) - locs = IsAlliance(race) ? &allianceStarterPerLevelCache[level] : &hordeStarterPerLevelCache[level]; - else - locs = &locsPerLevelCache[level]; + std::vector locs = sTravelMgr.GetTeleportLocations(bot); LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(), - bot->GetLevel(), locs->size()); + bot->GetLevel(), locs.size()); - RandomTeleport(bot, *locs); + RandomTeleport(bot, locs); } void RandomPlayerbotMgr::RandomTeleport(Player* bot) diff --git a/src/Bot/RandomPlayerbotMgr.h b/src/Bot/RandomPlayerbotMgr.h index 94c0a0151..db74f2cbe 100644 --- a/src/Bot/RandomPlayerbotMgr.h +++ b/src/Bot/RandomPlayerbotMgr.h @@ -164,25 +164,8 @@ public: static uint8 GetTeamClassIdx(bool isAlliance, uint8 claz) { return isAlliance * 20 + claz; } void PrepareAddclassCache(); - void PrepareZone2LevelBracket(); - void PrepareTeleportCache(); void Init(); std::map> addclassCache; - std::map> locsPerLevelCache; - std::map> allianceStarterPerLevelCache; - std::map> hordeStarterPerLevelCache; - - struct LevelBracket { - uint32 low; - uint32 high; - bool InsideBracket(uint32 val) { return val >= low && val <= high; } - }; - std::map zone2LevelBracket; - struct BankerLocation { - WorldLocation loc; - uint32 entry; - }; - std::map> bankerLocsPerLevelCache; // Account type management void AssignAccountTypes(); diff --git a/src/Db/FlightMasterCache.cpp b/src/Db/FlightMasterCache.cpp deleted file mode 100644 index effe24993..000000000 --- a/src/Db/FlightMasterCache.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "FlightMasterCache.h" - -void FlightMasterCache::AddHordeFlightMaster(uint32 entry, WorldPosition pos) -{ - hordeFlightMasterCache[entry] = pos; -} - -void FlightMasterCache::AddAllianceFlightMaster(uint32 entry, WorldPosition pos) -{ - allianceFlightMasterCache[entry] = pos; -} - -Creature* FlightMasterCache::GetNearestFlightMaster(Player* bot) -{ - std::map& flightMasterCache = - (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; - - Creature* nearestFlightMaster = nullptr; - float nearestDistance = std::numeric_limits::max(); - - for (auto const& [entry, pos] : flightMasterCache) - { - if (pos.GetMapId() == bot->GetMapId()) - { - float distance = bot->GetExactDist2dSq(pos); - if (distance < nearestDistance) - { - Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); - if (flightMaster) - { - nearestDistance = distance; - nearestFlightMaster = flightMaster; - } - } - } - } - - return nearestFlightMaster; -} diff --git a/src/Db/FlightMasterCache.h b/src/Db/FlightMasterCache.h deleted file mode 100644 index 7f8b95310..000000000 --- a/src/Db/FlightMasterCache.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef _PLAYERBOT_FLIGHTMASTER_H -#define _PLAYERBOT_FLIGHTMASTER_H - -#include "Creature.h" -#include "Player.h" -#include "TravelMgr.h" - -class FlightMasterCache -{ -public: - static FlightMasterCache& Instance() - { - static FlightMasterCache instance; - - return instance; - } - - Creature* GetNearestFlightMaster(Player* bot); - void AddHordeFlightMaster(uint32 entry, WorldPosition pos); - void AddAllianceFlightMaster(uint32 entry, WorldPosition pos); - -private: - FlightMasterCache() = default; - ~FlightMasterCache() = default; - - FlightMasterCache(const FlightMasterCache&) = delete; - FlightMasterCache& operator=(const FlightMasterCache&) = delete; - - FlightMasterCache(FlightMasterCache&&) = delete; - FlightMasterCache& operator=(FlightMasterCache&&) = delete; - - std::map allianceFlightMasterCache; - std::map hordeFlightMasterCache; -}; - -#endif diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 703cca0cc..4ddba6d46 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -8,6 +8,10 @@ #include #include +#include "Creature.h" +#include "Log.h" +#include "ObjectAccessor.h" +#include "TravelNode.h" #include "Talentspec.h" #include "ChatHelper.h" #include "MMapFactory.h" @@ -22,6 +26,71 @@ #include "Corpse.h" #include "CellImpl.h" +// Navigation data + +enum class CityId : uint8 +{ + STORMWIND, + IRONFORGE, + DARNASSUS, + EXODAR, + ORGRIMMAR, + UNDERCITY, + THUNDER_BLUFF, + SILVERMOON_CITY, + SHATTRATH_CITY, + DALARAN +}; + +static const std::unordered_map> bankerToCity = { + {2455, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2456, {CityId::STORMWIND, TEAM_ALLIANCE}}, {2457, {CityId::STORMWIND, TEAM_ALLIANCE}}, + {2460, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {2461, {CityId::IRONFORGE, TEAM_ALLIANCE}}, {5099, {CityId::IRONFORGE, TEAM_ALLIANCE}}, + {4155, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4208, {CityId::DARNASSUS, TEAM_ALLIANCE}}, {4209, {CityId::DARNASSUS, TEAM_ALLIANCE}}, + {17773, {CityId::EXODAR, TEAM_ALLIANCE}}, {18350, {CityId::EXODAR, TEAM_ALLIANCE}}, {16710, {CityId::EXODAR, TEAM_ALLIANCE}}, + {3320, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3309, {CityId::ORGRIMMAR, TEAM_HORDE}}, {3318, {CityId::ORGRIMMAR, TEAM_HORDE}}, + {4549, {CityId::UNDERCITY, TEAM_HORDE}}, {2459, {CityId::UNDERCITY, TEAM_HORDE}}, {2458, {CityId::UNDERCITY, TEAM_HORDE}}, {4550, {CityId::UNDERCITY, TEAM_HORDE}}, + {2996, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8356, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, {8357, {CityId::THUNDER_BLUFF, TEAM_HORDE}}, + {17631, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17632, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {17633, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {16615, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16616, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, {16617, {CityId::SILVERMOON_CITY, TEAM_HORDE}}, + {19246, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19338, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {19034, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, {19318, {CityId::SHATTRATH_CITY, TEAM_NEUTRAL}}, + {30604, {CityId::DALARAN, TEAM_NEUTRAL}}, {30605, {CityId::DALARAN, TEAM_NEUTRAL}}, {30607, {CityId::DALARAN, TEAM_NEUTRAL}}, + {28675, {CityId::DALARAN, TEAM_NEUTRAL}}, {28676, {CityId::DALARAN, TEAM_NEUTRAL}}, {28677, {CityId::DALARAN, TEAM_NEUTRAL}} +}; + +static const std::unordered_map> cityToBankers = { + {CityId::STORMWIND, {2455, 2456, 2457}}, + {CityId::IRONFORGE, {2460, 2461, 5099}}, + {CityId::DARNASSUS, {4155, 4208, 4209}}, + {CityId::EXODAR, {17773, 18350, 16710}}, + {CityId::ORGRIMMAR, {3320, 3309, 3318}}, + {CityId::UNDERCITY, {4549, 2459, 2458, 4550}}, + {CityId::THUNDER_BLUFF, {2996, 8356, 8357}}, + {CityId::SILVERMOON_CITY, {17631, 17632, 17633, 16615, 16616, 16617}}, + {CityId::SHATTRATH_CITY, {19246, 19338, 19034, 19318}}, + {CityId::DALARAN, {30604, 30605, 30607, 28675, 28676, 28677, 29530}} +}; + +static int GetCityWeight(CityId city) +{ + int weight = 0; + switch (city) + { + case CityId::STORMWIND: weight = sPlayerbotAIConfig.weightTeleToStormwind; break; + case CityId::IRONFORGE: weight = sPlayerbotAIConfig.weightTeleToIronforge; break; + case CityId::DARNASSUS: weight = sPlayerbotAIConfig.weightTeleToDarnassus; break; + case CityId::EXODAR: weight = sPlayerbotAIConfig.weightTeleToExodar; break; + case CityId::ORGRIMMAR: weight = sPlayerbotAIConfig.weightTeleToOrgrimmar; break; + case CityId::UNDERCITY: weight = sPlayerbotAIConfig.weightTeleToUndercity; break; + case CityId::THUNDER_BLUFF: weight = sPlayerbotAIConfig.weightTeleToThunderBluff; break; + case CityId::SILVERMOON_CITY: weight = sPlayerbotAIConfig.weightTeleToSilvermoonCity; break; + case CityId::SHATTRATH_CITY: weight = sPlayerbotAIConfig.weightTeleToShattrathCity; break; + case CityId::DALARAN: weight = sPlayerbotAIConfig.weightTeleToDalaran; break; + default: weight = 0; break; + } + return weight; +} + WorldPosition::WorldPosition(std::string const str) { std::vector tokens = split(str, '|'); @@ -4287,3 +4356,434 @@ void TravelMgr::printObj(WorldObject* obj, std::string const type) } } } + +void TravelMgr::Init() +{ + if (sPlayerbotAIConfig.enabled) + { + PrepareZone2LevelBracket(); + PrepareDestinationCache(); + } + sTravelNodeMap.InitTaxiGraph(); + LOG_INFO("playerbots", "Playerbots Taxi graph and destination cache built."); +} + +Creature* TravelMgr::GetNearestFlightMaster(Player* bot) +{ + std::map& flightMasterCache = + (bot->GetTeamId() == TEAM_ALLIANCE) ? allianceFlightMasterCache : hordeFlightMasterCache; + + Creature* nearestFlightMaster = nullptr; + float nearestDistance = std::numeric_limits::max(); + + for (auto const& [entry, pos] : flightMasterCache) + { + if (pos.GetMapId() != bot->GetMapId()) + continue; + + float distance = bot->GetExactDist2dSq(pos); + if (distance > nearestDistance) + continue; + + Creature* flightMaster = ObjectAccessor::GetSpawnedCreatureByDBGUID(bot->GetMapId(), entry); + if (flightMaster) + { + nearestDistance = distance; + nearestFlightMaster = flightMaster; + } + } + + return nearestFlightMaster; +} + +ObjectGuid TravelMgr::GetNearestFlightMasterGuid(Player* bot) +{ + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster) + return ObjectGuid::Empty; + + return nearestFlightMaster->GetGUID(); +} + +std::vector> TravelMgr::GetOptimalFlightDestinations(Player* bot) +{ + std::vector> validDestinations; + + Creature* nearestFlightMaster = GetNearestFlightMaster(bot); + if (!nearestFlightMaster || bot->GetDistance(nearestFlightMaster) > 500.0f) + return validDestinations; + + uint32 fromNode = sObjectMgr->GetNearestTaxiNode(nearestFlightMaster->GetPositionX(), nearestFlightMaster->GetPositionY(), + nearestFlightMaster->GetPositionZ(), nearestFlightMaster->GetMapId(), + bot->GetTeamId()); + if (!fromNode) + return validDestinations; + std::vector candidateLocations; + if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100) + candidateLocations = GetCityLocations(bot); + + std::vector hubLocations = GetTravelHubs(bot); + candidateLocations.insert(candidateLocations.end(), hubLocations.begin(), hubLocations.end()); + + for (auto const& loc : candidateLocations) + { + uint32 candidateNode = sObjectMgr->GetNearestTaxiNode(loc.GetPositionX(), loc.GetPositionY(), + loc.GetPositionZ(), loc.GetMapId(), + bot->GetTeamId()); + if (!candidateNode) + continue; + + std::vector path = sTravelNodeMap.FindTaxiPath(fromNode, candidateNode); + if (!path.empty()) + validDestinations.push_back(path); + } + return validDestinations; +} + +const std::vector TravelMgr::GetTeleportLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + uint8 isAlliance = bot->GetTeamId() == TEAM_ALLIANCE; + if (sPlayerbotAIConfig.enableNewRpgStrategy) + return isAlliance ? allianceHubsPerLevelCache[level] : hordeHubsPerLevelCache[level]; + + return locsPerLevelCache[level]; +} + +const std::vector TravelMgr::GetTravelHubs(Player* bot) +{ + std::vector locs = bot->GetTeamId() == TEAM_ALLIANCE + ? allianceHubsPerLevelCache[bot->GetLevel()] + : hordeHubsPerLevelCache[bot->GetLevel()]; + return locs; +} + +std::vector TravelMgr::GetCityLocations(Player* bot) +{ + uint32 level = bot->GetLevel(); + + std::vector fallbackLocations; + for (auto& bLoc : bankerLocsPerLevelCache[level]) + fallbackLocations.push_back(bLoc.loc); + + if (!sPlayerbotAIConfig.enableWeightTeleToCityBankers) + return fallbackLocations; + + TeamId botTeamId = bot->GetTeamId(); + std::unordered_set validBankerCities; + for (auto& loc : bankerLocsPerLevelCache[level]) + { + auto cityIt = bankerToCity.find(loc.entry); + if (cityIt == bankerToCity.end()) + continue; + + TeamId cityTeamId = cityIt->second.second; + + if (cityTeamId == botTeamId || + (cityTeamId == TEAM_NEUTRAL) + ) + validBankerCities.insert(cityIt->second.first); + } + // Fallback if no valid cities + if (validBankerCities.empty()) + return fallbackLocations; + + // Apply weights to valid cities + std::vector weightedCities; + for (CityId city : validBankerCities) + { + int weight = GetCityWeight(city); + if (weight <= 0) + continue; + + for (int i = 0; i < weight; ++i) + weightedCities.push_back(city); + } + + // Fallback if no valid cities + if (weightedCities.empty()) + return fallbackLocations; + + // Pick a weighted city randomly, then a random banker in that city + CityId selectedCity = weightedCities[urand(0, weightedCities.size() - 1)]; + + auto const& bankers = cityToBankers.at(selectedCity); + uint32 selectedBankerEntry = bankers[urand(0, bankers.size() - 1)]; + auto locIt = bankerEntryToLocation.find(selectedBankerEntry); + if (locIt != bankerEntryToLocation.end()) + return { locIt->second }; + // Fallback if something went wrong + return fallbackLocations; +} + +void TravelMgr::PrepareZone2LevelBracket() +{ + // Classic WoW - Low - level zones + zone2LevelBracket[1] = {5, 12}; // Dun Morogh + zone2LevelBracket[12] = {5, 12}; // Elwynn Forest + zone2LevelBracket[14] = {5, 12}; // Durotar + zone2LevelBracket[85] = {5, 12}; // Tirisfal Glades + zone2LevelBracket[141] = {5, 12}; // Teldrassil + zone2LevelBracket[215] = {5, 12}; // Mulgore + zone2LevelBracket[3430] = {5, 12}; // Eversong Woods + zone2LevelBracket[3524] = {5, 12}; // Azuremyst Isle + + // Classic WoW - Mid - level zones + zone2LevelBracket[17] = {10, 25}; // Barrens + zone2LevelBracket[38] = {10, 20}; // Loch Modan + zone2LevelBracket[40] = {10, 21}; // Westfall + zone2LevelBracket[130] = {10, 23}; // Silverpine Forest + zone2LevelBracket[148] = {10, 21}; // Darkshore + zone2LevelBracket[3433] = {10, 22}; // Ghostlands + zone2LevelBracket[3525] = {10, 21}; // Bloodmyst Isle + + // Classic WoW - High - level zones + zone2LevelBracket[10] = {19, 33}; // Deadwind Pass + zone2LevelBracket[11] = {21, 30}; // Wetlands + zone2LevelBracket[44] = {16, 28}; // Redridge Mountains + zone2LevelBracket[267] = {20, 34}; // Hillsbrad Foothills + zone2LevelBracket[331] = {18, 33}; // Ashenvale + zone2LevelBracket[400] = {24, 36}; // Thousand Needles + zone2LevelBracket[406] = {16, 29}; // Stonetalon Mountains + + // Classic WoW - Higher - level zones + zone2LevelBracket[3] = {36, 46}; // Badlands + zone2LevelBracket[8] = {36, 46}; // Swamp of Sorrows + zone2LevelBracket[15] = {35, 46}; // Dustwallow Marsh + zone2LevelBracket[16] = {45, 52}; // Azshara + zone2LevelBracket[33] = {32, 47}; // Stranglethorn Vale + zone2LevelBracket[45] = {30, 42}; // Arathi Highlands + zone2LevelBracket[47] = {42, 51}; // Hinterlands + zone2LevelBracket[51] = {45, 51}; // Searing Gorge + zone2LevelBracket[357] = {40, 52}; // Feralas + zone2LevelBracket[405] = {30, 41}; // Desolace + zone2LevelBracket[440] = {41, 52}; // Tanaris + + // Classic WoW - Top - level zones + zone2LevelBracket[4] = {52, 57}; // Blasted Lands + zone2LevelBracket[28] = {50, 60}; // Western Plaguelands + zone2LevelBracket[46] = {51, 60}; // Burning Steppes + zone2LevelBracket[139] = {54, 62}; // Eastern Plaguelands + zone2LevelBracket[361] = {47, 57}; // Felwood + zone2LevelBracket[490] = {49, 56}; // Un'Goro Crater + zone2LevelBracket[618] = {54, 61}; // Winterspring + zone2LevelBracket[1377] = {54, 63}; // Silithus + + // The Burning Crusade - Zones + zone2LevelBracket[3483] = {58, 66}; // Hellfire Peninsula + zone2LevelBracket[3518] = {64, 70}; // Nagrand + zone2LevelBracket[3519] = {62, 73}; // Terokkar Forest + zone2LevelBracket[3520] = {66, 73}; // Shadowmoon Valley + zone2LevelBracket[3521] = {60, 67}; // Zangarmarsh + zone2LevelBracket[3522] = {64, 73}; // Blade's Edge Mountains + zone2LevelBracket[3523] = {67, 73}; // Netherstorm + zone2LevelBracket[4080] = {68, 73}; // Isle of Quel'Danas + + // Wrath of the Lich King - Zones + zone2LevelBracket[65] = {71, 77}; // Dragonblight + zone2LevelBracket[66] = {74, 80}; // Zul'Drak + zone2LevelBracket[67] = {77, 80}; // Storm Peaks + zone2LevelBracket[210] = {77, 80}; // Icecrown Glacier + zone2LevelBracket[394] = {72, 78}; // Grizzly Hills + zone2LevelBracket[495] = {68, 74}; // Howling Fjord + zone2LevelBracket[2817] = {77, 80}; // Crystalsong Forest + zone2LevelBracket[3537] = {68, 75}; // Borean Tundra + zone2LevelBracket[3711] = {75, 80}; // Sholazar Basin + zone2LevelBracket[4197] = {79, 80}; // Wintergrasp + + // Override with values from config + for (auto const& [zoneId, bracketPair] : sPlayerbotAIConfig.zoneBrackets) + zone2LevelBracket[zoneId] = {bracketPair.first, bracketPair.second}; +} + +void TravelMgr::PrepareDestinationCache() +{ + uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); + uint32 flightMastersCount = 0; + uint32 innkeepersCount = 0; + uint32 bankerCount = 0; + + LOG_INFO("playerbots", "Preparing destination caches for {} levels...", maxLevel); + // Temporary map to group creatures by entry and area + std::map, std::vector> tempLocsCache; + std::map>> tempCreatureCache; + for (auto const& [guid, creatureData] : sObjectMgr->GetAllCreatureData()) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureData.id1); + if (!creatureTemplate) + continue; + + uint16 mapId = creatureData.mapid; + if (std::find(sPlayerbotAIConfig.randomBotMaps.begin(), sPlayerbotAIConfig.randomBotMaps.end(), mapId) + == sPlayerbotAIConfig.randomBotMaps.end()) + continue; + + float x = creatureData.posX; + float y = creatureData.posY; + float z = creatureData.posZ; + float orient = creatureData.orientation; + uint32 templateEntry = creatureData.id1; + + Map* map = sMapMgr->FindMap(mapId, 0); + if (!map) + continue; + + AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(PHASEMASK_NORMAL, x, y, z)); + if (!area) + continue; + + uint32 areaId = area->zone ? area->zone : area->ID; + + // CREATURES + if (creatureTemplate->npcflag == 0 && + creatureTemplate->lootid != 0 && + creatureTemplate->maxlevel - creatureTemplate->minlevel < 3 && + creatureTemplate->Entry != 32820 && creatureTemplate->Entry != 24196 && + creatureTemplate->Entry != 30627 && creatureTemplate->Entry != 30617 && + creatureData.spawntimesecs < 1000 && + creatureTemplate->faction != 11 && creatureTemplate->faction != 71 && + creatureTemplate->faction != 79 && creatureTemplate->faction != 85 && + creatureTemplate->faction != 188 && creatureTemplate->faction != 1575 && + (creatureTemplate->unit_flags & 256) == 0 && + (creatureTemplate->unit_flags & 4096) == 0 && + creatureTemplate->rank == 0) + { + uint32 roundX = (x / 50.0f) * 10.0f; + uint32 roundY = (y / 50.0f) * 10.0f; + uint32 roundZ = (z / 50.0f) * 10.0f; + tempLocsCache[std::make_tuple(mapId, roundX, roundY, roundZ)].push_back(creatureData); + tempCreatureCache[templateEntry][areaId].push_back(WorldLocation(mapId, x, y, z)); + } + // FLIGHT MASTERS + else if ((creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER || + creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) && + creatureTemplate->Entry != 3838 && creatureTemplate->Entry != 29480) + { + FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(creatureTemplate->faction); + bool forHorde = !(factionEntry->hostileMask & 4); + bool forAlliance = !(factionEntry->hostileMask & 2); + + if (creatureTemplate->npcflag & UNIT_NPC_FLAG_FLIGHTMASTER) + { + WorldPosition pos(mapId, x, y, z, orient); + if (forHorde) + hordeFlightMasterCache[guid] = pos; + + if (forAlliance) + allianceFlightMasterCache[guid] = pos; + flightMastersCount++; + } + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_INNKEEPER) + { + if (zone2LevelBracket.find(areaId) == zone2LevelBracket.end()) + continue; + + LevelBracket bracket = zone2LevelBracket[areaId]; + WorldPosition loc(mapId, x + cos(orient) * 5.0f, y + sin(orient) * 5.0f, z + 0.5f, orient + M_PI); + for (int i = bracket.low; i <= bracket.high; i++) + { + if (forHorde) + hordeHubsPerLevelCache[i].push_back(loc); + + if (forAlliance) + allianceHubsPerLevelCache[i].push_back(loc); + innkeepersCount++; + } + } + } + // === BANKERS === + else if (creatureTemplate->npcflag & UNIT_NPC_FLAG_BANKER && + creatureTemplate->npcflag != 135298 && + creatureTemplate->minlevel != 55 && + creatureTemplate->minlevel != 65 && + creatureTemplate->faction != 35 && creatureTemplate->faction != 474 && + creatureTemplate->faction != 69 && creatureTemplate->faction != 57 && + creatureTemplate->Entry != 30606 && creatureTemplate->Entry != 30608 && + creatureTemplate->Entry != 29282) + { + BankerLocation bLoc; + bLoc.loc = WorldLocation(mapId, x + cos(orient) * 6.0f, y + sin(orient) * 6.0f, z + 2.0f, orient + M_PI); + bLoc.entry = templateEntry; + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = 1; l <= maxLevel; l++) + { + // Bots 1-60 go to base game bankers (all have minlevel 30 or 45) + if (l <=60 && level > 45) + continue; + + // Bots 61-70 go to Shattrath bankers (all have minlevel 60 or 70) + if ((l >=61 && l <=70) && (level < 60 || level > 70)) + continue; + + // Bots 71+ go to Dalaran bankers (all have minlevel 75) + if ((l >=71) && level != 75) + continue; + + bankerLocsPerLevelCache[(uint8)l].push_back(bLoc); + bankerEntryToLocation[bLoc.entry] = bLoc.loc; + } + bankerCount++; + } + } + + // Process temporary caches + for (auto const& [gridTuple, creatureDataList] : tempLocsCache) + { + if (creatureDataList.size() > 2) + { + CreatureTemplate const* creatureTemplate = sObjectMgr->GetCreatureTemplate(creatureDataList[0].id1); + uint32 level = (creatureTemplate->minlevel + creatureTemplate->maxlevel + 1) / 2; + for (int32 l = (int32)level - (int32)sPlayerbotAIConfig.randomBotTeleLowerLevel; + l <= (int32)level + (int32)sPlayerbotAIConfig.randomBotTeleHigherLevel; l++) + { + if (l < 1 || l > maxLevel) + continue; + + locsPerLevelCache[(uint8)l].push_back(WorldLocation(std::get<0>(gridTuple))); + } + } + } + for (auto const& [entry, areaMap] : tempCreatureCache) + { + for (auto const& [area, locList] : areaMap) + { + if (locList.size() > 3) + continue; + + float totalX = 0, totalY = 0, totalZ = 0; + for (auto const& loc : locList) + { + totalX += loc.GetPositionX(); + totalY += loc.GetPositionY(); + totalZ += loc.GetPositionZ(); + } + float avgX = totalX / locList.size(); + float avgY = totalY / locList.size(); + float avgZ = totalZ / locList.size(); + creatureSpawnsByTemplate[entry].push_back(WorldLocation(locList[0].GetMapId(), avgX, avgY, avgZ, 0)); + } + } + // Add travel hubs based on player start locations + for (uint32 i = 1; i < MAX_RACES; i++) + { + for (uint32 j = 1; j < MAX_CLASSES; j++) + { + PlayerInfo const* info = sObjectMgr->GetPlayerInfo(i, j); + + if (!info) + continue; + + WorldPosition pos(info->mapId, info->positionX, info->positionY, info->positionZ, info->orientation); + + for (int32 l = 1; l <= 5; l++) + { + if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + allianceHubsPerLevelCache[(uint8)l].push_back(pos); + else + hordeHubsPerLevelCache[(uint8)l].push_back(pos); + } + break; + } + } + LOG_INFO("playerbots", ">> {} flight masters and {} innkeepers and {} banker locations for level collected.", flightMastersCount, innkeepersCount, bankerCount); +} diff --git a/src/Mgr/Travel/TravelMgr.h b/src/Mgr/Travel/TravelMgr.h index 1f5f848cd..f300ae636 100644 --- a/src/Mgr/Travel/TravelMgr.h +++ b/src/Mgr/Travel/TravelMgr.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_TRAVELMGR_H #include +#include #include #include "AiObject.h" @@ -15,6 +16,7 @@ #include "GridDefines.h" #include "PlayerbotAIConfig.h" +class Creature; class GuidPosition; class ObjectGuid; class Quest; @@ -854,6 +856,16 @@ public: void Clear(); void LoadQuestTravelTable(); + // Navigation + void Init(); + Creature* GetNearestFlightMaster(Player* bot); + ObjectGuid GetNearestFlightMasterGuid(Player* bot); + std::vector> GetOptimalFlightDestinations(Player* bot); + const std::vector GetTeleportLocations(Player* bot); + const std::vector GetTravelHubs(Player* bot); + std::vector GetCityLocations(Player* bot); + const std::vector& GetLocsPerLevelCache(uint8 level) { return locsPerLevelCache[level]; } + template void weighted_shuffle(D first, D last, W first_weight, W last_weight, URBG&& g) { @@ -943,6 +955,37 @@ private: TravelMgr(TravelMgr&&) = delete; TravelMgr& operator=(TravelMgr&&) = delete; + + // Navigation initialization + void PrepareZone2LevelBracket(); + void PrepareDestinationCache(); + + // Internal types + struct LevelBracket + { + uint32 low; + uint32 high; + bool InsideBracket(uint32 val) const { return val >= low && val <= high; } + }; + + struct BankerLocation + { + WorldLocation loc; + uint32 entry; + }; + + // Navigation caches + std::map allianceFlightMasterCache; + std::map hordeFlightMasterCache; + std::map> allianceHubsPerLevelCache; + std::map> hordeHubsPerLevelCache; + std::map> bankerLocsPerLevelCache; + std::unordered_map bankerEntryToLocation; + std::map> locsPerLevelCache; + std::unordered_map> creatureSpawnsByTemplate; + std::map zone2LevelBracket; }; +#define sTravelMgr TravelMgr::instance() + #endif diff --git a/src/Mgr/Travel/TravelNode.cpp b/src/Mgr/Travel/TravelNode.cpp index 3e304677f..3b4996e97 100644 --- a/src/Mgr/Travel/TravelNode.cpp +++ b/src/Mgr/Travel/TravelNode.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "BudgetValues.h" #include "PathGenerator.h" @@ -2447,3 +2448,127 @@ WorldPosition TravelNodeMap::getMapOffset(uint32 mapId) return WorldPosition(mapId, 0, 0, 0, 0); } + +// ============================================================ +// TravelNodeMap taxi graph (BFS-based flight path lookup) +// ============================================================ + +void TravelNodeMap::InitTaxiGraph() +{ + BuildTaxiGraph(); + ComputeAllPaths(); +} + +std::vector TravelNodeMap::FindTaxiPath(uint32 fromNode, uint32 toNode) +{ + if (fromNode == toNode) + return {}; + + TaxiNodesEntry const* startNode = sTaxiNodesStore.LookupEntry(fromNode); + TaxiNodesEntry const* endNode = sTaxiNodesStore.LookupEntry(toNode); + + if (!startNode || !endNode || startNode->map_id != endNode->map_id) + return {}; + + auto cacheItr = taxiPathCache.find(fromNode); + if (cacheItr == taxiPathCache.end()) + return {}; + + auto toNodeItr = cacheItr->second.find(toNode); + if (toNodeItr == cacheItr->second.end()) + return {}; + + return toNodeItr->second; +} + +void TravelNodeMap::BuildTaxiGraph() +{ + taxiGraph.clear(); + std::unordered_map> tempGraph; + for (uint32 i = 0; i < sTaxiPathStore.GetNumRows(); ++i) + { + TaxiPathEntry const* path = sTaxiPathStore.LookupEntry(i); + if (!path) + continue; + + if (path->to == 0 || path->to == uint32(-1)) + continue; + + tempGraph[path->from].insert(path->to); + tempGraph[path->to].insert(path->from); + } + for (auto const& [node, neighbors] : tempGraph) + taxiGraph[node] = std::vector(neighbors.begin(), neighbors.end()); +} + +void TravelNodeMap::ComputeAllPaths() +{ + std::set allNodes; + for (auto const& [source, neighbors] : taxiGraph) + allNodes.insert(source); + + for (uint32 source : allNodes) + { + auto parentMap = BFS(source); + + for (uint32 target : allNodes) + { + if (source == target) + continue; + + auto path = BuildPath(source, target, parentMap); + if (!path.empty()) + taxiPathCache[source][target] = path; + } + } +} + +std::unordered_map TravelNodeMap::BFS(uint32 fromNode) +{ + std::queue workQueue; + std::unordered_set visited; + std::unordered_map parentMap; + + workQueue.push(fromNode); + visited.insert(fromNode); + parentMap[fromNode] = 0; + + while (!workQueue.empty()) + { + uint32 current = workQueue.front(); + workQueue.pop(); + + for (uint32 next : taxiGraph.at(current)) + { + if (visited.count(next)) + continue; + + visited.insert(next); + parentMap[next] = current; + workQueue.push(next); + } + } + return parentMap; +} + +std::vector TravelNodeMap::BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap) +{ + if (!parentMap.count(toNode)) + return {}; // unreachable + + std::vector path; + uint32 current = toNode; + while (current != fromNode) + { + path.push_back(current); + auto it = parentMap.find(current); + if (it == parentMap.end() || it->second == 0) + break; + current = it->second; + } + + path.push_back(fromNode); + std::reverse(path.begin(), path.end()); + return path; +} diff --git a/src/Mgr/Travel/TravelNode.h b/src/Mgr/Travel/TravelNode.h index 4dc235721..9e05e2490 100644 --- a/src/Mgr/Travel/TravelNode.h +++ b/src/Mgr/Travel/TravelNode.h @@ -580,6 +580,10 @@ public: void calcMapOffset(); WorldPosition getMapOffset(uint32 mapId); + // Taxi graph (BFS-based path lookup between taxi nodes) + void InitTaxiGraph(); + std::vector FindTaxiPath(uint32 fromNode, uint32 toNode); + std::shared_timed_mutex m_nMapMtx; std::unordered_map> teleportNodes; @@ -593,6 +597,16 @@ private: TravelNodeMap(TravelNodeMap&&) = delete; TravelNodeMap& operator=(TravelNodeMap&&) = delete; + // Taxi graph internals + void BuildTaxiGraph(); + void ComputeAllPaths(); + std::unordered_map BFS(uint32 startNode); + std::vector BuildPath(uint32 fromNode, uint32 toNode, + const std::unordered_map& parentMap); + + std::unordered_map> taxiGraph; + std::map>> taxiPathCache; + std::vector m_nodes; std::vector> mapOffsets; diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index fb0ffad4d..6a8d60129 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -15,6 +15,7 @@ #include "RandomPlayerbotFactory.h" #include "RandomPlayerbotMgr.h" #include "Talentspec.h" +#include "TravelMgr.h" template void LoadList(std::string const value, T& list) @@ -691,6 +692,7 @@ bool PlayerbotAIConfig::Initialize() { PlayerbotDungeonRepository::instance().LoadDungeonSuggestions(); } + sTravelMgr.Init(); excludedHunterPetFamilies.clear(); LoadList>(sConfigMgr->GetOption("AiPlayerbot.ExcludedHunterPetFamilies", ""), excludedHunterPetFamilies); From cba6af27add9722ea954d6084ecf2ae3212206a4 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:40:59 -0500 Subject: [PATCH 08/17] Fix Assassination Rogue Finishers and add Cold Blood (#2215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Pull Request Description **Note for reviewers**: The Rogue files are very confusing, so for background, there is DpsRogueStrategy, which is for all Rogues and represented by the “dps” strategy in game, and there is also AssassinationRogueStrategy, which is for Assassination and Subtlety specs and represented by the “melee” strategy in game. So Combat has only the dps strategy, while Assassination and Subtlety have the dps and melee strategies. - The main focus of this PR is to fix an issue with Assassination Rogues that caused them to use Eviscerate instead of Envenom about 1/3 of the time they should have been using Envenom, which was significantly reducing their DPS. See the bottom of this post for an explanation for why this was happening and why the fix works. Well, LMK if you think it's wrong, but this is how I am understanding things, and my back-of-the-envelope math (also below) supports it. - After this PR, Assassination Rogues will use Eviscerate only if they are unable to use Envenom (don't have the ability learned or no Deadly Poison on the target) or if they don’t have Rank 3 in Master Poisoner. - Additionally, Assassination Rogues previously would use Envenom/Eviscerate at 3 or more combo points. This is suboptimal so I created a new “combo points 4 available” trigger that will fire at 4 or 5 combo points only. They will still use the finisher at 3 combo points if the mob is almost dead (via the existing “target with combo points almost dead” trigger). - I then added Cold Blood, which Rogues previously would not use at all. Now there is a ColdBloodAction(), and Cold Blood is used when a Rogue has at least 4 combo points, right before using Envenom (or Eviscerate). I implemented it as a standard BuffTrigger so they’ll just use the ability off cooldown. - While looking at the combo point triggers, I thought it was confusing that the “combo points available” trigger actually meant 5 combo points (presumably because the default parameter for combo points in ComboPointsAvailableTrigger() is 5). I changed the string to “combo points 5 available” so it’s less confusing going forward. This necessitated some changes in the Druid files too. - Next, I cleaned up DpsRogueStrategy a bit. Not a lot to say, just some duplicative or useless logic was removed. There shouldn’t be any impact on gameplay from the changes. - In the process of making the edits in the Druid files, I noticed that the trigger for Tiger’s Fury in OffhealDruidCatStrategy was “low energy,” which does not exist (there is a “light energy available,” but the EnergyAvailable triggers are for when energy is AT LEAST the designated level, not AT MOST the designated level). So I replaced the trigger with the already-existing “tiger’s fury” trigger, which I think is just a generic BuffTrigger so I don’t actually know why it exists (i.e., Druid will use the spell off cooldown). But this particular change is just a quick fix and not intended to be thoughtful (that would be outside the scope of this PR). ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. There should be no relevant impact on performance. This PR adds one new action triggered by the standard BuffTrigger. Otherwise, these are just fixes to existing logic. ## How to Test the Changes The easiest way to test is to fight a boss that doesn't tend to result in downtime (since downtime can lead to the loss of deadly poison stacks, in which case Eviscerate will (and should be) used by Assassination Rogues). You can use a damage meter such as Skada to track ability use. You should see: - Assassination Rogues don't use Eviscerate at all, or very few times. - Assassination Rogues use Cold Blood. - Offheal Cat Druids use Tiger's Fury. - Otherwise, Rogue and Cat Druid behavior should remain the same. ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [ ] No - [x] Yes (**explain why**) Default behavior for Assassination Rogues was broken, as explained above. - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [ ] No - [x] Yes (**explain below**) I had Claude help me diagnose the initial issue and help me understand the queue system. And I had it implement the changes that were just busywork (like combo point triggers). ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers The reason for why Assassination Rogues were using Eviscerate so frequently is due to the fact that Envenom and Eviscerate were part of the same TriggerNode. When actions are part of the same TriggerNode, they're processed together, and the actions are queued by priority. When the higher-priority action is executed, the lower-priority action is not cleared--it remains in the queue for expireActionTime from the config, which is 5 seconds by default. Then, as soon as the lower-priority action can be executed (without regard for triggers because it is already triggered, just sitting in the queue), it will execute. This pattern of code works fine ingame if (1) you are actually trying to queue actions, like what I did with Cold Blood -> Envenom, (2) there are other guards like IsUseful() and IsPossible() that keep unwanted actions from executing, or (3) the trigger is just constantly firing so the higher-priority action is always evaluated. But TriggerNode isn't really the right way to implement action priority--that's through ActionNode. AssassinationRogueStrategy had Envenom and Eviscerate in the same TriggerNode, and then the corresponding ActionNode had Rupture as a fallback. Now, I changed it so Eviscerate is instead a fallback in the Envenom ActionNode (and Rupture is removed entirely because Assassination Rogues just shouldn't be using it, except maybe on very high-armor targets that are immune to poison, but that is very niche). ~ I did some back-of-the-envelope math to check this pattern. Say we're in a situation where Deadly Poison is up so ideally the Rogue should use Envenom 100% of the time. Through the old system, what would happen when the trigger fired? - Rogue uses Envenom since it's the higher-priority action. - Due to the Ruthlessness talent, Rogue has a 60% chance of having 1 combo point after the finisher, 40% chance of 0 combo points. If it has 1 combo point, it uses Eviscerate immediately. - If it has 0 combo points, it uses Mutilate. Mutilate grants 2 combo points, unless it crits, in which case it grants 3 due to Seal Fate. If Mutilate doesn't crit, the Rogue has 2 combo points, and it uses Eviscerate. If Mutilate does crit, the Rogue has 3 combo points, and it uses Envenom. - So let's assume Mutilate has a 55% crit chance (very reasonable for a Rogue in entry-level raid gear with raid buffs due to Opportunity giving +20% crit chance to Mutilate). Mutilate hits twice, and if either hit crits, Seal Fate Procs. The chance of at least one crit with two hits at a 55% crit chance is ~80%. That means if Ruthlessness doesn't give a combo point, there is an 80% chance that Envenom will be used and a 20% chance that Eviscerate will be used. - Combine the above, and the result of one trigger firing is you get 1 guaranteed Envenom + 0.6 Eviscerates (Ruthlessness proc path) + 0.32 Envenoms (No Ruthlessness proc but Seal Fate proc path) + 0.08 Eviscerates (No Ruthlessness proc and no Seal Fate proc path) = 1.32 Envenoms to each 0.68 Eviscerates, or a 1.94:1 ratio of Envenoms to Eviscerates. That is basically identical to what I saw in practice of roughly a 2:1 ratio of Envenoms to Eviscerates. - I understand the above is simplistic and it assumes that the Rogue gets a combo point within 5 seconds following using Envenom (very likely) and that there are not two opportunities to use Envenom or Eviscerate in the 5-second queue period after using Envenom (it can happen but is uncommon). That's all at the margins and isn't going to impact the math very much. --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Ai/Base/TriggerContext.h | 6 ++-- .../Druid/Strategy/CatDpsDruidStrategy.cpp | 2 +- .../Strategy/OffhealDruidCatStrategy.cpp | 4 +-- src/Ai/Class/Rogue/Action/RogueActions.cpp | 2 +- src/Ai/Class/Rogue/Action/RogueActions.h | 6 ++++ src/Ai/Class/Rogue/RogueAiObjectContext.cpp | 2 ++ .../Strategy/AssassinationRogueStrategy.cpp | 11 ++++---- .../Class/Rogue/Strategy/DpsRogueStrategy.cpp | 28 ++----------------- 8 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index ceb7c001c..d4f8ac3b7 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -103,7 +103,8 @@ public: creators["enemy within melee"] = &TriggerContext::enemy_within_melee; creators["party member to heal out of spell range"] = &TriggerContext::party_member_to_heal_out_of_spell_range; - creators["combo points available"] = &TriggerContext::ComboPointsAvailable; + creators["combo points 5 available"] = &TriggerContext::ComboPoints5Available; + creators["combo points 4 available"] = &TriggerContext::ComboPoints4Available; creators["combo points 3 available"] = &TriggerContext::ComboPoints3Available; creators["target with combo points almost dead"] = &TriggerContext::target_with_combo_points_almost_dead; creators["combo points not full"] = &TriggerContext::ComboPointsNotFull; @@ -338,7 +339,8 @@ private: { return new PartyMemberToHealOutOfSpellRangeTrigger(botAI); } - static Trigger* ComboPointsAvailable(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI); } + static Trigger* ComboPoints5Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 5); } + static Trigger* ComboPoints4Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 4); } static Trigger* ComboPoints3Available(PlayerbotAI* botAI) { return new ComboPointsAvailableTrigger(botAI, 3); } static Trigger* target_with_combo_points_almost_dead(PlayerbotAI* ai) { diff --git a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp index fda1b5f94..b1a4685b1 100644 --- a/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/CatDpsDruidStrategy.cpp @@ -228,7 +228,7 @@ void CatDpsDruidStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rip", ACTION_HIGH + 6) } diff --git a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp index c472ce8d8..fb7893651 100644 --- a/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp +++ b/src/Ai/Class/Druid/Strategy/OffhealDruidCatStrategy.cpp @@ -176,7 +176,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rip", ACTION_HIGH + 6) } @@ -257,7 +257,7 @@ void OffhealDruidCatStrategy::InitTriggers(std::vector& triggers) ); triggers.push_back( new TriggerNode( - "low energy", + "tiger's fury", { NextAction("tiger's fury", ACTION_NORMAL + 1) } diff --git a/src/Ai/Class/Rogue/Action/RogueActions.cpp b/src/Ai/Class/Rogue/Action/RogueActions.cpp index b554a8253..46beaf86c 100644 --- a/src/Ai/Class/Rogue/Action/RogueActions.cpp +++ b/src/Ai/Class/Rogue/Action/RogueActions.cpp @@ -61,7 +61,7 @@ bool CastEnvenomAction::isUseful() bool CastEnvenomAction::isPossible() { // alternate to eviscerate if talents unlearned - return botAI->HasAura(58410, bot) /* Master Poisoner */; + return botAI->HasAura(58410, bot) /* Master Poisoner Rank 3 */; } bool CastTricksOfTheTradeOnMainTankAction::isUseful() diff --git a/src/Ai/Class/Rogue/Action/RogueActions.h b/src/Ai/Class/Rogue/Action/RogueActions.h index dd0ad4735..3ae1f8142 100644 --- a/src/Ai/Class/Rogue/Action/RogueActions.h +++ b/src/Ai/Class/Rogue/Action/RogueActions.h @@ -78,6 +78,12 @@ public: CastFeintAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "feint") {} }; +class CastColdBloodAction : public CastBuffSpellAction +{ +public: + CastColdBloodAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cold blood") {} +}; + class CastDismantleAction : public CastSpellAction { public: diff --git a/src/Ai/Class/Rogue/RogueAiObjectContext.cpp b/src/Ai/Class/Rogue/RogueAiObjectContext.cpp index 8586d93d1..4b4ebab1e 100644 --- a/src/Ai/Class/Rogue/RogueAiObjectContext.cpp +++ b/src/Ai/Class/Rogue/RogueAiObjectContext.cpp @@ -143,6 +143,7 @@ public: creators["use instant poison on off hand"] = &RogueAiObjectContextInternal::use_instant_poison_off_hand; creators["fan of knives"] = &RogueAiObjectContextInternal::fan_of_knives; creators["killing spree"] = &RogueAiObjectContextInternal::killing_spree; + creators["cold blood"] = &RogueAiObjectContextInternal::cold_blood; } private: @@ -184,6 +185,7 @@ private: static Action* use_instant_poison_off_hand(PlayerbotAI* ai) { return new UseInstantPoisonOffHandAction(ai); } static Action* fan_of_knives(PlayerbotAI* ai) { return new FanOfKnivesAction(ai); } static Action* killing_spree(PlayerbotAI* ai) { return new CastKillingSpreeAction(ai); } + static Action* cold_blood(PlayerbotAI* ai) { return new CastColdBloodAction(ai); } }; SharedNamedObjectContextList RogueAiObjectContext::sharedStrategyContexts; diff --git a/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp b/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp index b8563893c..a104fae07 100644 --- a/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp +++ b/src/Ai/Class/Rogue/Strategy/AssassinationRogueStrategy.cpp @@ -29,7 +29,7 @@ private: return new ActionNode( "envenom", /*P*/ {}, - /*A*/ { NextAction("rupture") }, + /*A*/ { NextAction("eviscerate") }, /*C*/ {} ); } @@ -108,10 +108,10 @@ void AssassinationRogueStrategy::InitTriggers(std::vector& trigger triggers.push_back( new TriggerNode( - "combo points 3 available", + "combo points 4 available", { - NextAction("envenom", ACTION_HIGH + 5), - NextAction("eviscerate", ACTION_HIGH + 3) + NextAction("cold blood", ACTION_HIGH + 6), + NextAction("envenom", ACTION_HIGH + 5) } ) ); @@ -120,8 +120,7 @@ void AssassinationRogueStrategy::InitTriggers(std::vector& trigger new TriggerNode( "target with combo points almost dead", { - NextAction("envenom", ACTION_HIGH + 4), - NextAction("eviscerate", ACTION_HIGH + 2) + NextAction("envenom", ACTION_HIGH + 4) } ) ); diff --git a/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp b/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp index 22c6a6f83..06aeda57c 100644 --- a/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp +++ b/src/Ai/Class/Rogue/Strategy/DpsRogueStrategy.cpp @@ -12,36 +12,14 @@ class DpsRogueStrategyActionNodeFactory : public NamedObjectFactory public: DpsRogueStrategyActionNodeFactory() { - creators["mutilate"] = &mutilate; creators["sinister strike"] = &sinister_strike; creators["kick"] = &kick; creators["kidney shot"] = &kidney_shot; creators["backstab"] = &backstab; - creators["melee"] = &melee; creators["rupture"] = &rupture; } private: - static ActionNode* melee([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "melee", - /*P*/ {}, - /*A*/ { - NextAction("mutilate") }, - /*C*/ {} - ); - } - static ActionNode* mutilate([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "mutilate", - /*P*/ {}, - /*A*/ { - NextAction("sinister strike") }, - /*C*/ {} - ); - } static ActionNode* sinister_strike([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( @@ -77,7 +55,7 @@ private: "backstab", /*P*/ {}, /*A*/ { - NextAction("mutilate") }, + NextAction("sinister strike") }, /*C*/ {} ); } @@ -140,7 +118,7 @@ void DpsRogueStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("rupture", ACTION_HIGH + 1), NextAction("eviscerate", ACTION_HIGH) @@ -335,7 +313,7 @@ void StealthedRogueStrategy::InitTriggers(std::vector& triggers) { triggers.push_back( new TriggerNode( - "combo points available", + "combo points 5 available", { NextAction("eviscerate", ACTION_HIGH) } From c6a07ad012cb7dd45468d7ce9cb6abfc94115409 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 20 Mar 2026 20:41:22 +0100 Subject: [PATCH 09/17] Every Man for Himself racial support (#2198) # Pull Request Added Every Man for Himself racial support Partially resolves: https://github.com/mod-playerbots/mod-playerbots/issues/2002 --- ## How to Test the Changes - when human bot is in combat apply aura via command `.aura 20066` - bot should use "Every Man for Himself" to remove aura ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) Human bots now using "Every Man for Himself" by default where in combat If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [x] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Copilot CLI to review changes --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Test result: obraz --- src/Ai/Base/ActionContext.h | 2 ++ src/Ai/Base/Actions/GenericSpellActions.cpp | 24 +++++++++++++++++++++ src/Ai/Base/Actions/GenericSpellActions.h | 10 +++++++++ src/Ai/Base/Strategy/RacialsStrategy.cpp | 3 +++ src/Ai/Base/Trigger/GenericTriggers.cpp | 9 ++++++++ src/Ai/Base/Trigger/GenericTriggers.h | 8 +++++++ src/Ai/Base/TriggerContext.h | 2 ++ 7 files changed, 58 insertions(+) diff --git a/src/Ai/Base/ActionContext.h b/src/Ai/Base/ActionContext.h index 6431ee87c..55081b443 100644 --- a/src/Ai/Base/ActionContext.h +++ b/src/Ai/Base/ActionContext.h @@ -163,6 +163,7 @@ public: creators["war stomp"] = &ActionContext::war_stomp; creators["blood fury"] = &ActionContext::blood_fury; creators["berserking"] = &ActionContext::berserking; + creators["every man for himself"] = &ActionContext::every_man_for_himself; creators["use trinket"] = &ActionContext::use_trinket; creators["auto talents"] = &ActionContext::auto_talents; creators["auto share quest"] = &ActionContext::auto_share_quest; @@ -357,6 +358,7 @@ private: static Action* war_stomp(PlayerbotAI* botAI) { return new CastWarStompAction(botAI); } static Action* blood_fury(PlayerbotAI* botAI) { return new CastBloodFuryAction(botAI); } static Action* berserking(PlayerbotAI* botAI) { return new CastBerserkingAction(botAI); } + static Action* every_man_for_himself(PlayerbotAI* botAI) { return new CastEveryManForHimselfAction(botAI); } static Action* use_trinket(PlayerbotAI* botAI) { return new UseTrinketAction(botAI); } static Action* auto_talents(PlayerbotAI* botAI) { return new AutoSetTalentsAction(botAI); } static Action* auto_share_quest(PlayerbotAI* ai) { return new AutoShareQuestAction(ai); } diff --git a/src/Ai/Base/Actions/GenericSpellActions.cpp b/src/Ai/Base/Actions/GenericSpellActions.cpp index 148bc6d3c..392d18500 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.cpp +++ b/src/Ai/Base/Actions/GenericSpellActions.cpp @@ -311,6 +311,30 @@ bool CastVehicleSpellAction::Execute(Event /*event*/) return botAI->CastVehicleSpell(spellId, GetTarget()); } +bool CastEveryManForHimselfAction::isPossible() +{ + uint32 spellId = AI_VALUE2(uint32, "spell id", spell); + if (!spellId) + return false; + + if (!bot->HasSpell(spellId)) + return false; + + if (bot->HasSpellCooldown(spellId)) + return false; + + return true; +} + +bool CastEveryManForHimselfAction::isUseful() +{ + return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM); +} + bool UseTrinketAction::Execute(Event /*event*/) { Item* trinket1 = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_TRINKET1); diff --git a/src/Ai/Base/Actions/GenericSpellActions.h b/src/Ai/Base/Actions/GenericSpellActions.h index 9aa83f62d..fdc0dcdcf 100644 --- a/src/Ai/Base/Actions/GenericSpellActions.h +++ b/src/Ai/Base/Actions/GenericSpellActions.h @@ -284,6 +284,16 @@ public: CastBerserkingAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "berserking") {} }; +class CastEveryManForHimselfAction : public CastSpellAction +{ +public: + CastEveryManForHimselfAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "every man for himself") {} + + std::string const GetTargetName() override { return "self target"; } + bool isPossible() override; + bool isUseful() override; +}; + class UseTrinketAction : public Action { public: diff --git a/src/Ai/Base/Strategy/RacialsStrategy.cpp b/src/Ai/Base/Strategy/RacialsStrategy.cpp index dc6d5bc48..753302c35 100644 --- a/src/Ai/Base/Strategy/RacialsStrategy.cpp +++ b/src/Ai/Base/Strategy/RacialsStrategy.cpp @@ -34,6 +34,9 @@ void RacialsStrategy::InitTriggers(std::vector& triggers) NextAction("berserking", ACTION_NORMAL + 5), NextAction("use trinket", ACTION_NORMAL + 4) })); + triggers.push_back(new TriggerNode( + "loss of control", { NextAction("every man for himself", ACTION_EMERGENCY + 1) })); + } RacialsStrategy::RacialsStrategy(PlayerbotAI* botAI) : Strategy(botAI) diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 209766be7..8cee6bef0 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -464,6 +464,15 @@ bool AttackerCountTrigger::IsActive() { return AI_VALUE(uint8, "attacker count") bool HasAuraTrigger::IsActive() { return botAI->HasAura(getName(), GetTarget(), false, false, -1, true); } +bool LossOfControlTrigger::IsActive() +{ + return bot->HasAuraType(SPELL_AURA_MOD_STUN) || + bot->HasAuraType(SPELL_AURA_MOD_FEAR) || + bot->HasAuraType(SPELL_AURA_MOD_ROOT) || + bot->HasAuraType(SPELL_AURA_MOD_CONFUSE) || + bot->HasAuraType(SPELL_AURA_MOD_CHARM); +} + bool HasAuraStackTrigger::IsActive() { Aura* aura = botAI->GetAura(getName(), GetTarget(), false, true, stack); diff --git a/src/Ai/Base/Trigger/GenericTriggers.h b/src/Ai/Base/Trigger/GenericTriggers.h index 7b884fbf0..68fc4b61f 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.h +++ b/src/Ai/Base/Trigger/GenericTriggers.h @@ -746,6 +746,14 @@ public: bool IsActive() override; }; +class LossOfControlTrigger : public Trigger +{ +public: + LossOfControlTrigger(PlayerbotAI* botAI) : Trigger(botAI, "loss of control", 1) {} + + bool IsActive() override; +}; + class IsSwimmingTrigger : public Trigger { public: diff --git a/src/Ai/Base/TriggerContext.h b/src/Ai/Base/TriggerContext.h index d4f8ac3b7..ca662f3d4 100644 --- a/src/Ai/Base/TriggerContext.h +++ b/src/Ai/Base/TriggerContext.h @@ -59,6 +59,7 @@ public: creators["party member almost full health"] = &TriggerContext::PartyMemberAlmostFullHealth; creators["generic boost"] = &TriggerContext::generic_boost; + creators["loss of control"] = &TriggerContext::loss_of_control; creators["protect party member"] = &TriggerContext::protect_party_member; @@ -363,6 +364,7 @@ private: return new PartyMemberAlmostFullHealthTrigger(botAI); } static Trigger* generic_boost(PlayerbotAI* botAI) { return new GenericBoostTrigger(botAI); } + static Trigger* loss_of_control(PlayerbotAI* botAI) { return new LossOfControlTrigger(botAI); } static Trigger* PartyMemberCriticalHealth(PlayerbotAI* botAI) { return new PartyMemberCriticalHealthTrigger(botAI); From d5762b7e0fcbb81c1afd27c8df1584c14373aa39 Mon Sep 17 00:00:00 2001 From: Crow Date: Fri, 20 Mar 2026 14:41:47 -0500 Subject: [PATCH 10/17] Remove Vertical Speed Limit from Knockback Packet (#2223) ## Pull Request Description This PR removes the break from SMSG_MOVE_KNOCK_BACK for knockbacks with vertical speed of >35.0f. This break is the reason for many vertical knockbacks having no effect on bots, including Shade of Aran's Flame Wreath, High Astromancer Solarian's Wrath of the Astromancer, and Archimonde's Air Burst. There is a comment that indicates that the limit was originally added due to bots getting stuck from high-speed vertical knockbacks. I have not observed this at all and have been playing with this break removed for several months. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. I honestly cannot say if there is impact on processing cost because I have no understanding of packets. I would be surprised if there are any performance issues since knockback packets are ordinarily getting sent all the time, it's just a small number of moves that get skipped due to this break. ## How to Test the Changes 1. .go creature name High Astromancer Solarian 2. Start combat and wait until a bot gets hit with Wrath of the Astromancer 3. Wait for the aura to expire and watch the bot fly to Mars and fall back down ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [ ] No, not at all - [x] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) I do not know for sure, but as noted above, I would be surprised if there was any notable performance impact. - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [ ] Performance impact is understood, tested, and acceptable. <- I can't say for sure, but I've not had any issues. I would appreciate getting thoughts from somebody knowledgeable about packet use, however. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com> Co-authored-by: bash Co-authored-by: Revision Co-authored-by: kadeshar --- src/Bot/PlayerbotAI.cpp | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Bot/PlayerbotAI.cpp b/src/Bot/PlayerbotAI.cpp index 800247c6f..36631cead 100644 --- a/src/Bot/PlayerbotAI.cpp +++ b/src/Bot/PlayerbotAI.cpp @@ -1250,17 +1250,10 @@ void PlayerbotAI::HandleBotOutgoingPacket(WorldPacket const& packet) p >> guid.ReadAsPacked() >> counter >> vcos >> vsin >> horizontalSpeed >> verticalSpeed; if (horizontalSpeed <= 0.1f) - { horizontalSpeed = 0.11f; - } verticalSpeed = -verticalSpeed; - // high vertical may result in stuck as bot can not handle gravity - if (verticalSpeed > 35.0f) - break; - // stop casting - InterruptSpell(); - // stop movement + InterruptSpell(); bot->StopMoving(); bot->GetMotionMaster()->Clear(); From f160420d70414a4d580610dbb0667c5879096034 Mon Sep 17 00:00:00 2001 From: oskov Date: Fri, 20 Mar 2026 21:42:07 +0200 Subject: [PATCH 11/17] Fix/talent tree ordered map (#2222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2050 InitTalents builds a map of talentRow → [TalentEntry*] and iterates it to teach talents row by row. WoW's talent system requires each row to be filled before unlocking the next, so iteration must happen in ascending row order. Commit b474dc4 ("Performance optim") changed the container from std::map to std::unordered_map, which has no guaranteed key ordering. As a result, bots would frequently attempt to learn talents in a row whose prerequisites hadn't been met yet, silently skipping them. I belive it's the reason of #2050 issue. The fix is a one-character type change: restoring std::map, which guarantees ascending key (row) order. How to Test the Changes 1. Make fresh installation 2. Create new character 3. Observe talents tree of fresh rnd bots Was AI assistance used while working on this change? - [X] Yes — GitHub Copilot CLI was used to identify the root cause (unordered_map introduced in b474dc4 breaking talent row ordering), stage the one-line fix, and draft this PR description. The code change was reviewed and fully understood before submission. Root cause commit: b474dc44bb6323430a84fc17c1ec046f9919a101 ("Performance optim") — changed std::map to std::unordered_map in InitTalents, breaking the row-ordering guarantee that WoW's talent prerequisite system depends on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Bot/Factory/PlayerbotFactory.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bot/Factory/PlayerbotFactory.cpp b/src/Bot/Factory/PlayerbotFactory.cpp index c0e6d80db..506ad9154 100644 --- a/src/Bot/Factory/PlayerbotFactory.cpp +++ b/src/Bot/Factory/PlayerbotFactory.cpp @@ -762,7 +762,7 @@ void PlayerbotFactory::InitPetTalents() // pet_family->petTalentType); return; } - std::unordered_map> spells; + std::map> spells; bool diveTypePet = (1LL << ci->family) & diveMask; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) @@ -2653,7 +2653,7 @@ void PlayerbotFactory::InitSpecialSpells() void PlayerbotFactory::InitTalents(uint32 specNo) { uint32 classMask = bot->getClassMask(); - std::unordered_map> spells; + std::map> spells; for (uint32 i = 0; i < sTalentStore.GetNumRows(); ++i) { TalentEntry const* talentInfo = sTalentStore.LookupEntry(i); From 98395a1090fb1ec6df6b976a1dd7ff4cc78de5ca Mon Sep 17 00:00:00 2001 From: kadeshar Date: Fri, 20 Mar 2026 20:42:22 +0100 Subject: [PATCH 12/17] Added cancellation druid form actions (#2194) # Pull Request Added new (for now manual) actions to cancel druid forms. Resolve: https://github.com/mod-playerbots/mod-playerbots/issues/1788 --- ## Feature Evaluation Please answer the following: - order bot enter some form like `do travel form` - order bot cancel form like `do cancel travel form` --- ## How to Test the Changes - order bot enter some form like `do travel form` - order bot cancel form like `do cancel travel form` ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- --- src/Ai/Base/ChatTriggerContext.h | 14 ++++ .../Strategy/ChatCommandHandlerStrategy.cpp | 7 ++ .../Druid/Action/DruidShapeshiftActions.cpp | 6 +- .../Druid/Action/DruidShapeshiftActions.h | 68 ++++++++++++++++++- src/Ai/Class/Druid/DruidAiObjectContext.cpp | 12 ++++ 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/src/Ai/Base/ChatTriggerContext.h b/src/Ai/Base/ChatTriggerContext.h index 54ae236b4..b305b19ea 100644 --- a/src/Ai/Base/ChatTriggerContext.h +++ b/src/Ai/Base/ChatTriggerContext.h @@ -104,6 +104,13 @@ public: creators["target"] = &ChatTriggerContext::target; creators["formation"] = &ChatTriggerContext::formation; creators["stance"] = &ChatTriggerContext::stance; + creators["cancel tree form"] = &ChatTriggerContext::cancel_tree_form; + creators["cancel travel form"] = &ChatTriggerContext::cancel_travel_form; + creators["cancel bear form"] = &ChatTriggerContext::cancel_bear_form; + creators["cancel dire bear form"] = &ChatTriggerContext::cancel_dire_bear_form; + creators["cancel cat form"] = &ChatTriggerContext::cancel_cat_form; + creators["cancel moonkin form"] = &ChatTriggerContext::cancel_moonkin_form; + creators["cancel aquatic form"] = &ChatTriggerContext::cancel_aquatic_form; creators["sendmail"] = &ChatTriggerContext::sendmail; creators["mail"] = &ChatTriggerContext::mail; creators["outfit"] = &ChatTriggerContext::outfit; @@ -159,6 +166,13 @@ private: static Trigger* sendmail(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "sendmail"); } static Trigger* formation(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "formation"); } static Trigger* stance(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "stance"); } + static Trigger* cancel_tree_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel tree form"); } + static Trigger* cancel_travel_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel travel form"); } + static Trigger* cancel_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel bear form"); } + static Trigger* cancel_dire_bear_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel dire bear form"); } + static Trigger* cancel_cat_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel cat form"); } + static Trigger* cancel_moonkin_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel moonkin form"); } + static Trigger* cancel_aquatic_form(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "cancel aquatic form"); } static Trigger* attackers(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "attackers"); } static Trigger* target(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "target"); } static Trigger* max_dps(PlayerbotAI* botAI) { return new ChatCommandTrigger(botAI, "max dps"); } diff --git a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp index 7bc1d5395..64334b799 100644 --- a/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp +++ b/src/Ai/Base/Strategy/ChatCommandHandlerStrategy.cpp @@ -160,6 +160,13 @@ ChatCommandHandlerStrategy::ChatCommandHandlerStrategy(PlayerbotAI* botAI) : Pas supported.push_back("save mana"); supported.push_back("formation"); supported.push_back("stance"); + supported.push_back("cancel tree form"); + supported.push_back("cancel travel form"); + supported.push_back("cancel bear form"); + supported.push_back("cancel dire bear form"); + supported.push_back("cancel cat form"); + supported.push_back("cancel moonkin form"); + supported.push_back("cancel aquatic form"); supported.push_back("sendmail"); supported.push_back("mail"); supported.push_back("outfit"); diff --git a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp index a87f22a3b..f30d4ec03 100644 --- a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp +++ b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.cpp @@ -44,13 +44,13 @@ bool CastCasterFormAction::isUseful() AI_VALUE2(uint8, "mana", "self target") > sPlayerbotAIConfig.mediumHealth; } -bool CastCancelTreeFormAction::Execute(Event /*event*/) +bool CastCancelDruidAction::Execute(Event /*event*/) { - botAI->RemoveAura("tree of life"); + botAI->RemoveAura(auraName); return true; } -bool CastCancelTreeFormAction::isUseful() { return botAI->HasAura(33891, bot); } +bool CastCancelDruidAction::isUseful() { return botAI->HasAura(auraId, bot); } bool CastTreeFormAction::isUseful() { diff --git a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h index 9d75f2682..d485171d2 100644 --- a/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h +++ b/src/Ai/Class/Druid/Action/DruidShapeshiftActions.h @@ -71,14 +71,78 @@ public: bool isPossible() override { return true; } }; -class CastCancelTreeFormAction : public CastBuffSpellAction +class CastCancelDruidAction : public CastBuffSpellAction { public: - CastCancelTreeFormAction(PlayerbotAI* botAI) : CastBuffSpellAction(botAI, "cancel tree form") {} + CastCancelDruidAction(PlayerbotAI* botAI, std::string const& actionName, std::string const& auraName, uint32 auraId) + : CastBuffSpellAction(botAI, actionName), auraName(auraName), auraId(auraId) + { + } bool Execute(Event event) override; bool isUseful() override; bool isPossible() override { return true; } + +private: + std::string auraName; + uint32 auraId; +}; + +class CastCancelTreeFormAction : public CastCancelDruidAction +{ +public: + CastCancelTreeFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel tree form", "tree of life", 33891) + { + } +}; + +class CastCancelTravelFormAction : public CastCancelDruidAction +{ +public: + CastCancelTravelFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel travel form", "travel form", 783) + { + } +}; + +class CastCancelBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelBearFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel bear form", "bear form", 5487) {} +}; + +class CastCancelDireBearFormAction : public CastCancelDruidAction +{ +public: + CastCancelDireBearFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel dire bear form", "dire bear form", 9634) + { + } +}; + +class CastCancelCatFormAction : public CastCancelDruidAction +{ +public: + CastCancelCatFormAction(PlayerbotAI* botAI) : CastCancelDruidAction(botAI, "cancel cat form", "cat form", 768) {} +}; + +class CastCancelMoonkinFormAction : public CastCancelDruidAction +{ +public: + CastCancelMoonkinFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel moonkin form", "moonkin form", 24858) + { + } +}; + +class CastCancelAquaticFormAction : public CastCancelDruidAction +{ +public: + CastCancelAquaticFormAction(PlayerbotAI* botAI) + : CastCancelDruidAction(botAI, "cancel aquatic form", "aquatic form", 1066) + { + } }; #endif diff --git a/src/Ai/Class/Druid/DruidAiObjectContext.cpp b/src/Ai/Class/Druid/DruidAiObjectContext.cpp index 29d9d4fdc..cc12f009f 100644 --- a/src/Ai/Class/Druid/DruidAiObjectContext.cpp +++ b/src/Ai/Class/Druid/DruidAiObjectContext.cpp @@ -170,6 +170,12 @@ public: creators["aquatic form"] = &DruidAiObjectContextInternal::aquatic_form; creators["caster form"] = &DruidAiObjectContextInternal::caster_form; creators["cancel tree form"] = &DruidAiObjectContextInternal::cancel_tree_form; + creators["cancel travel form"] = &DruidAiObjectContextInternal::cancel_travel_form; + creators["cancel bear form"] = &DruidAiObjectContextInternal::cancel_bear_form; + creators["cancel dire bear form"] = &DruidAiObjectContextInternal::cancel_dire_bear_form; + creators["cancel cat form"] = &DruidAiObjectContextInternal::cancel_cat_form; + creators["cancel moonkin form"] = &DruidAiObjectContextInternal::cancel_moonkin_form; + creators["cancel aquatic form"] = &DruidAiObjectContextInternal::cancel_aquatic_form; creators["mangle (bear)"] = &DruidAiObjectContextInternal::mangle_bear; creators["maul"] = &DruidAiObjectContextInternal::maul; creators["bash"] = &DruidAiObjectContextInternal::bash; @@ -258,6 +264,12 @@ private: static Action* aquatic_form(PlayerbotAI* botAI) { return new CastAquaticFormAction(botAI); } static Action* caster_form(PlayerbotAI* botAI) { return new CastCasterFormAction(botAI); } static Action* cancel_tree_form(PlayerbotAI* botAI) { return new CastCancelTreeFormAction(botAI); } + static Action* cancel_travel_form(PlayerbotAI* botAI) { return new CastCancelTravelFormAction(botAI); } + static Action* cancel_bear_form(PlayerbotAI* botAI) { return new CastCancelBearFormAction(botAI); } + static Action* cancel_dire_bear_form(PlayerbotAI* botAI) { return new CastCancelDireBearFormAction(botAI); } + static Action* cancel_cat_form(PlayerbotAI* botAI) { return new CastCancelCatFormAction(botAI); } + static Action* cancel_moonkin_form(PlayerbotAI* botAI) { return new CastCancelMoonkinFormAction(botAI); } + static Action* cancel_aquatic_form(PlayerbotAI* botAI) { return new CastCancelAquaticFormAction(botAI); } static Action* mangle_bear(PlayerbotAI* botAI) { return new CastMangleBearAction(botAI); } static Action* maul(PlayerbotAI* botAI) { return new CastMaulAction(botAI); } static Action* bash(PlayerbotAI* botAI) { return new CastBashAction(botAI); } From 2b273f6a2c8ffea04fc41d3bb58788af1a67d1d9 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:23:07 -0700 Subject: [PATCH 13/17] Fix merge error in test staging (#2226) Fix merge error we missed due to core sync issues. ## Pull Request Description ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - [x] No, not at all - [ ] Minimal impact (**explain below**) - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - [x] No - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - [x] No - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - [x] No - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - [x] No - [ ] Yes (**explain below**) ## Final Checklist - [x] Stability is not compromised. - [x] Performance impact is understood, tested, and acceptable. - [x] Added logic complexity is justified and explained. - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Mgr/Travel/TravelMgr.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mgr/Travel/TravelMgr.cpp b/src/Mgr/Travel/TravelMgr.cpp index 4ddba6d46..d6942deab 100644 --- a/src/Mgr/Travel/TravelMgr.cpp +++ b/src/Mgr/Travel/TravelMgr.cpp @@ -4764,7 +4764,7 @@ void TravelMgr::PrepareDestinationCache() } } // Add travel hubs based on player start locations - for (uint32 i = 1; i < MAX_RACES; i++) + for (uint32 i = 1; i < sRaceMgr->GetMaxRaces(); i++) { for (uint32 j = 1; j < MAX_CLASSES; j++) { @@ -4777,7 +4777,7 @@ void TravelMgr::PrepareDestinationCache() for (int32 l = 1; l <= 5; l++) { - if ((1 << (i - 1)) & RACEMASK_ALLIANCE) + if ((1 << (i - 1)) & sRaceMgr->GetAllianceRaceMask()) allianceHubsPerLevelCache[(uint8)l].push_back(pos); else hordeHubsPerLevelCache[(uint8)l].push_back(pos); From 32af1b95dea5b1be2840c8fd94c226239347bbe9 Mon Sep 17 00:00:00 2001 From: dillyns <49765217+dillyns@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:19:22 -0400 Subject: [PATCH 14/17] Paladin Seal of wisdom fallback fix for Ret/Prot Paladins (#2147) # Pull Request This PR removes the fallback for Seal of Wisdom action from generic Paladin strategy and moves it to Holy Paladin only. This is necessary because a paladin who does not have Seal of Wisdom yet who goes low mana and triggers the seal of wisdom action will end up falling back to Seal of Righteousness, even though a better seal may be available, such as Seal of Command. The fallback is added to Holy Paladin so that low level holy paladins still use Righteousness until they get Wisdom --- ## Feature Evaluation Please answer the following: - Describe the **minimum logic** required to achieve the intended behavior? Ret paladin without Seal of Wisdom shouldn't change seals on low mana. - Describe the **cheapest implementation** that produces an acceptable result? Cheapest implementation is to remove seal of wisdom fallback for non-holy paladins. - Describe the **runtime cost** when this logic executes across many bots? No difference in cost compared to existing logic. --- ## How to Test the Changes Use a ret paladin bot who has Seal of Command but who does not have Seal of Wisdom. A paladin under level 38 will do. Order them to attack something, like a test dummy, until they eventually run low on mana. Before this change: The paladin will switch to Seal of Righteousness when they get low mana. After this change: The paladin leaves Seal of Command on when they get low mana. ## Complexity & Impact Does this change add new decision branches? - - [x] No - - [ ] Yes (**explain below**) Does this change increase per-bot or per-tick processing? - - [x] No - - [ ] Yes (**describe and justify impact**) Could this logic scale poorly under load? - - [x] No - - [ ] Yes (**explain why**) --- ## Defaults & Configuration Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) If this introduces more advanced or AI-heavy logic: - - [x] Lightweight mode remains the default - - [ ] More complex behavior is optional and thereby configurable --- ## AI Assistance Was AI assistance (e.g. ChatGPT or similar tools) used while working on this change? - - [x] No - - [ ] Yes (**explain below**) If yes, please specify: - AI tool or model used (e.g. ChatGPT, GPT-4, Claude, etc.) - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation) - Which parts of the change were influenced or generated - Whether the result was manually reviewed and adapted AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. Any AI-influenced changes must be verified against existing CORE and PB logic. We expect contributors to be honest about what they do and do not understand. --- ## Final Checklist - - [x] Stability is not compromised - - [x] Performance impact is understood, tested, and acceptable - - [x] Added logic complexity is justified and explained - - [x] Documentation updated if needed --- ## Notes for Reviewers Anything that significantly improves realism at the cost of stability or performance should be carefully discussed before merging. --- .../Paladin/Strategy/DpsPaladinStrategy.cpp | 33 ----------------- .../GenericPaladinStrategyActionNodeFactory.h | 36 +++++++++++++------ .../Paladin/Strategy/HealPaladinStrategy.cpp | 2 +- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp index 185fb72d7..fc6ae92a0 100644 --- a/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/DpsPaladinStrategy.cpp @@ -15,9 +15,6 @@ public: { creators["sanctity aura"] = &sanctity_aura; creators["retribution aura"] = &retribution_aura; - creators["seal of corruption"] = &seal_of_corruption; - creators["seal of vengeance"] = &seal_of_vengeance; - creators["seal of command"] = &seal_of_command; creators["blessing of might"] = &blessing_of_might; creators["crusader strike"] = &crusader_strike; creators["repentance"] = &repentance; @@ -27,36 +24,6 @@ public: } private: - static ActionNode* seal_of_corruption([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of corruption", - /*P*/ {}, - /*A*/ { NextAction("seal of vengeance") }, - /*C*/ {} - ); - } - - static ActionNode* seal_of_vengeance([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of vengeance", - /*P*/ {}, - /*A*/ { NextAction("seal of command") }, - /*C*/ {} - ); - } - - static ActionNode* seal_of_command([[maybe_unused]] PlayerbotAI* botAI) - { - return new ActionNode( - "seal of command", - /*P*/ {}, - /*A*/ { NextAction("seal of righteousness") }, - /*C*/ {} - ); - } - static ActionNode* blessing_of_might([[maybe_unused]] PlayerbotAI* botAI) { return new ActionNode( diff --git a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h index f1d0d342e..1ac76dbd2 100644 --- a/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h +++ b/src/Ai/Class/Paladin/Strategy/GenericPaladinStrategyActionNodeFactory.h @@ -22,6 +22,9 @@ public: creators["cleanse magic"] = &cleanse_magic; creators["cleanse poison on party"] = &cleanse_poison_on_party; creators["cleanse disease on party"] = &cleanse_disease_on_party; + creators["seal of corruption"] = &seal_of_corruption; + creators["seal of vengeance"] = &seal_of_vengeance; + creators["seal of command"] = &seal_of_command; creators["seal of wisdom"] = &seal_of_wisdom; creators["seal of justice"] = &seal_of_justice; creators["hand of reckoning"] = &hand_of_reckoning; @@ -41,7 +44,6 @@ public: creators["blessing of wisdom on party"] = &blessing_of_wisdom_on_party; creators["blessing of sanctuary on party"] = &blessing_of_sanctuary_on_party; creators["blessing of sanctuary"] = &blessing_of_sanctuary; - creators["seal of command"] = &seal_of_command; creators["taunt spell"] = &hand_of_reckoning; creators["righteous defense"] = &righteous_defense; creators["avenger's shield"] = &avengers_shield; @@ -155,18 +157,39 @@ private: /*A*/ { NextAction("purify disease on party") }, /*C*/ {}); } + static ActionNode* seal_of_corruption(PlayerbotAI* /* ai */) + { + return new ActionNode("seal of corruption", + /*P*/ {}, + /*A*/ { NextAction("seal of vengeance") }, + /*C*/ {}); + } + static ActionNode* seal_of_vengeance(PlayerbotAI* /* ai */) + { + return new ActionNode("seal of vengeance", + /*P*/ {}, + /*A*/ { NextAction("seal of command") }, + /*C*/ {}); + } + static ActionNode* seal_of_command(PlayerbotAI* /* ai */) + { + return new ActionNode("seal of command", + /*P*/ {}, + /*A*/ { NextAction("seal of righteousness") }, + /*C*/ {}); + } static ActionNode* seal_of_wisdom(PlayerbotAI* /* ai */) { return new ActionNode ("seal of wisdom", /*P*/ {}, - /*A*/ { NextAction("seal of righteousness") }, + /*A*/ { NextAction("seal of corruption") }, /*C*/ {}); } static ActionNode* seal_of_justice(PlayerbotAI* /* ai */) { return new ActionNode("seal of justice", /*P*/ {}, - /*A*/ { NextAction("seal of righteousness") }, + /*A*/ { NextAction("seal of corruption") }, /*C*/ {}); } static ActionNode* hand_of_reckoning(PlayerbotAI* /* ai */) @@ -246,13 +269,6 @@ private: /*A*/ {}, /*C*/ {}); } - static ActionNode* seal_of_command(PlayerbotAI* /* ai */) - { - return new ActionNode("seal of command", - /*P*/ {}, - /*A*/ { NextAction("seal of righteousness") }, - /*C*/ {}); - } }; #endif diff --git a/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.cpp b/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.cpp index 5eb1a3acc..fa7a08621 100644 --- a/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.cpp +++ b/src/Ai/Class/Paladin/Strategy/HealPaladinStrategy.cpp @@ -30,7 +30,7 @@ void HealPaladinStrategy::InitTriggers(std::vector& triggers) new TriggerNode( "seal", { - NextAction("seal of wisdom", ACTION_HIGH) + NextAction("seal of wisdom", ACTION_HIGH), } ) ); From 9f875a7c81124baca6392d83240c637e17b6140e Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:41:07 -0700 Subject: [PATCH 15/17] CoreUpdate - ThreatMgr (#2228) ## Pull Request Description Modification to threat system required for current core update PR. ## Feature Evaluation - Describe the **minimum logic** required to achieve the intended behavior. - Describe the **processing cost** when this logic executes across many bots. ## How to Test the Changes ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [ ] No - - [x] Yes (**explain below**) Claude. Module search for changes made. It also identified a section of dead code in EnemyPlayerValue due to incorrect ref that was fixed. ## Final Checklist - - [X] Stability is not compromised. - - [X] Performance impact is understood, tested, and acceptable. - - [X] Added logic complexity is justified and explained. - - [X] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Actions/MovementActions.cpp | 4 +- src/Ai/Base/Actions/TellTargetAction.cpp | 17 +++--- src/Ai/Base/Trigger/GenericTriggers.cpp | 4 +- src/Ai/Base/Value/AttackersValue.cpp | 47 ++++++---------- src/Ai/Base/Value/CcTargetValue.cpp | 2 +- src/Ai/Base/Value/CurrentCcTargetValue.cpp | 2 +- src/Ai/Base/Value/DpsTargetValue.cpp | 62 ++++++++-------------- src/Ai/Base/Value/EnemyPlayerValue.cpp | 34 ++++-------- src/Ai/Base/Value/LeastHpTargetValue.cpp | 2 +- src/Ai/Base/Value/TankTargetValue.cpp | 33 ++++-------- src/Ai/Base/Value/TargetValue.cpp | 22 ++++---- src/Ai/Base/Value/TargetValue.h | 6 +-- src/Ai/Base/Value/ThreatValues.cpp | 2 +- 13 files changed, 92 insertions(+), 145 deletions(-) diff --git a/src/Ai/Base/Actions/MovementActions.cpp b/src/Ai/Base/Actions/MovementActions.cpp index e1b8f6363..1dbca0312 100644 --- a/src/Ai/Base/Actions/MovementActions.cpp +++ b/src/Ai/Base/Actions/MovementActions.cpp @@ -1387,8 +1387,8 @@ bool MovementAction::Flee(Unit* target) } } - HostileReference* ref = target->GetThreatMgr().getCurrentVictim(); - if (ref && ref->getTarget() == bot) // bot is target - try to flee to tank or master + Unit* currentVictim = target->GetThreatMgr().GetCurrentVictim(); + if (currentVictim && currentVictim == bot) // bot is target - try to flee to tank or master { if (Group* group = bot->GetGroup()) { diff --git a/src/Ai/Base/Actions/TellTargetAction.cpp b/src/Ai/Base/Actions/TellTargetAction.cpp index 422d5e542..aca05407d 100644 --- a/src/Ai/Base/Actions/TellTargetAction.cpp +++ b/src/Ai/Base/Actions/TellTargetAction.cpp @@ -6,7 +6,8 @@ #include "TellTargetAction.h" #include "Event.h" -#include "ThreatMgr.h" +#include "CombatManager.h" +#include "ThreatManager.h" #include "AiObjectContext.h" #include "PlayerbotAI.h" @@ -42,21 +43,21 @@ bool TellAttackersAction::Execute(Event /*event*/) botAI->TellMaster("--- Threat ---"); - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - if (!ref) + auto const& threatenedByMe = bot->GetThreatMgr().GetThreatenedByMeList(); + if (threatenedByMe.empty()) return true; - while (ref) + for (auto const& [guid, ref] : threatenedByMe) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* unit = threatMgr->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + float threat = ref->GetThreat(); std::ostringstream out; out << unit->GetName() << " (" << threat << ")"; botAI->TellMaster(out); - - ref = ref->next(); } return true; diff --git a/src/Ai/Base/Trigger/GenericTriggers.cpp b/src/Ai/Base/Trigger/GenericTriggers.cpp index 8cee6bef0..735e1df7e 100644 --- a/src/Ai/Base/Trigger/GenericTriggers.cpp +++ b/src/Ai/Base/Trigger/GenericTriggers.cpp @@ -16,7 +16,7 @@ #include "PositionValue.h" #include "SharedDefines.h" #include "TemporarySummon.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" #include "Timer.h" #include "PlayerbotAI.h" #include "Player.h" @@ -217,7 +217,7 @@ bool LowTankThreatTrigger::IsActive() if (!current_target) return false; - ThreatMgr& mgr = current_target->GetThreatMgr(); + ThreatManager& mgr = current_target->GetThreatMgr(); float threat = mgr.GetThreat(bot); float tankThreat = mgr.GetThreat(mt); return tankThreat == 0.0f || threat > tankThreat * 0.5f; diff --git a/src/Ai/Base/Value/AttackersValue.cpp b/src/Ai/Base/Value/AttackersValue.cpp index dbde7ab8d..65a8edeb3 100644 --- a/src/Ai/Base/Value/AttackersValue.cpp +++ b/src/Ai/Base/Value/AttackersValue.cpp @@ -92,21 +92,15 @@ void AttackersValue::AddAttackersOf(Player* player, std::unordered_set& t if (!player || !player->IsInWorld() || player->IsBeingTeleported()) return; - HostileRefMgr& refManager = player->getHostileRefMgr(); - HostileReference* ref = refManager.getFirst(); - if (!ref) - return; - - while (ref) + for (auto const& [guid, ref] : player->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatMgr = ref->GetSource(); - Unit* attacker = threatMgr->GetOwner(); + Unit* attacker = ref->GetOwner(); + if (!attacker) + continue; if (player->IsValidAttackTarget(attacker) && player->GetDistance2d(attacker) < sPlayerbotAIConfig.sightDistance) targets.insert(attacker); - - ref = ref->next(); } } @@ -131,7 +125,6 @@ bool AttackersValue::hasRealThreat(Unit* attacker) return attacker && attacker->IsInWorld() && attacker->IsAlive() && !attacker->IsPolymorphed() && // !attacker->isInRoots() && !attacker->IsFriendlyTo(bot); - (attacker->GetThreatMgr().getCurrentVictim() || dynamic_cast(attacker)); } bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range*/) @@ -241,9 +234,6 @@ bool AttackersValue::IsPossibleTarget(Unit* attacker, Player* bot, float /*range bool AttackersValue::IsValidTarget(Unit* attacker, Player* bot) { return IsPossibleTarget(attacker, bot) && bot->IsWithinLOSInMap(attacker); - // (attacker->GetThreatMgr().getCurrentVictim() || attacker->GetGuidValue(UNIT_FIELD_TARGET) || - // attacker->GetGUID().IsPlayer() || attacker->GetGUID() == - // GET_PLAYERBOT_AI(bot)->GetAiObjectContext()->GetValue("pull target")->Get()); } bool PossibleAddsValue::Calculate() @@ -255,27 +245,24 @@ bool PossibleAddsValue::Calculate() { if (find(attackers.begin(), attackers.end(), guid) != attackers.end()) continue; + Unit* add = botAI->GetUnit(guid); + if (!add || !add->IsInWorld() || add->IsDuringRemoveFromWorld()) + continue; - if (Unit* add = botAI->GetUnit(guid)) + if (!add->GetTarget() && !add->GetThreatMgr().GetLastVictim() && add->IsHostileTo(bot)) { - if (!add->IsInWorld() || add->IsDuringRemoveFromWorld()) - continue; - - if (!add->GetTarget() && !add->GetThreatMgr().getCurrentVictim() && add->IsHostileTo(bot)) + for (ObjectGuid const attackerGUID : attackers) { - for (ObjectGuid const attackerGUID : attackers) - { - Unit* attacker = botAI->GetUnit(attackerGUID); - if (!attacker) - continue; + Unit* attacker = botAI->GetUnit(attackerGUID); + if (!attacker) + continue; - float dist = ServerFacade::instance().GetDistance2d(attacker, add); - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) - continue; + float dist = ServerFacade::instance().GetDistance2d(attacker, add); + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aoeRadius * 1.5f)) + continue; - if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) - return true; - } + if (ServerFacade::instance().IsDistanceLessOrEqualThan(dist, sPlayerbotAIConfig.aggroDistance)) + return true; } } } diff --git a/src/Ai/Base/Value/CcTargetValue.cpp b/src/Ai/Base/Value/CcTargetValue.cpp index 0559656f7..a8de7a10e 100644 --- a/src/Ai/Base/Value/CcTargetValue.cpp +++ b/src/Ai/Base/Value/CcTargetValue.cpp @@ -20,7 +20,7 @@ public: } public: - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override { Player* bot = botAI->GetBot(); if (!botAI->CanCastSpell(spell, creature)) diff --git a/src/Ai/Base/Value/CurrentCcTargetValue.cpp b/src/Ai/Base/Value/CurrentCcTargetValue.cpp index 97f35c1b5..b095c09d7 100644 --- a/src/Ai/Base/Value/CurrentCcTargetValue.cpp +++ b/src/Ai/Base/Value/CurrentCcTargetValue.cpp @@ -13,7 +13,7 @@ public: { } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (botAI->HasAura(spell, attacker)) result = attacker; diff --git a/src/Ai/Base/Value/DpsTargetValue.cpp b/src/Ai/Base/Value/DpsTargetValue.cpp index 28f308471..010e47191 100644 --- a/src/Ai/Base/Value/DpsTargetValue.cpp +++ b/src/Ai/Base/Value/DpsTargetValue.cpp @@ -13,16 +13,14 @@ class FindMaxThreatGapTargetStrategy : public FindTargetStrategy public: FindMaxThreatGapTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), minThreat(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -32,7 +30,7 @@ public: if (!result || CalcThreatGap(attacker, threatMgr) > CalcThreatGap(result, &result->GetThreatMgr())) result = attacker; } - float CalcThreatGap(Unit* attacker, ThreatMgr* threatMgr) + float CalcThreatGap(Unit* attacker, ThreatManager* threatMgr) { Unit* victim = attacker->GetVictim(); return threatMgr->GetThreat(victim) - threatMgr->GetThreat(attacker); @@ -52,7 +50,7 @@ public: result = nullptr; } - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -61,13 +59,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -90,24 +86,19 @@ public: int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + int32_t level = new_level; if (level % 10 == 2 || level % 10 == 0) - { return new_time < old_time; - } // dont switch targets when all of them with low health Unit* currentTarget = botAI->GetAiObjectContext()->GetValue("current target")->Get(); if (currentTarget == new_unit) - { return true; - } + if (currentTarget == old_unit) - { return false; - } + return new_time > old_time; } int32_t GetIntervalLevel(Unit* unit) @@ -119,13 +110,11 @@ public: attackRange += 5.0f; int level = dis < attackRange ? 10 : 0; if (time >= 5 && time <= 30) - { return level + 2; - } + if (time > 30) - { return level; - } + return level + 1; } @@ -143,7 +132,7 @@ public: { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -152,13 +141,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -186,9 +173,8 @@ public: // attack enemy in range and with lowest health int level = new_level; if (level == 10) - { return new_time < old_time; - } + // all targets are far away, choose the closest one return botAI->GetBot()->GetDistance(new_unit) < botAI->GetBot()->GetDistance(old_unit); } @@ -216,7 +202,7 @@ public: { } - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -225,13 +211,11 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (foundHighPriority) - { return; - } + if (IsHighPriority(attacker)) { result = attacker; @@ -254,9 +238,8 @@ public: int new_level = GetIntervalLevel(new_unit); int old_level = GetIntervalLevel(old_unit); if (new_level != old_level) - { return new_level > old_level; - } + // attack enemy in range and with lowest health int level = new_level; Player* bot = botAI->GetBot(); @@ -264,9 +247,8 @@ public: { Unit* combo_unit = bot->GetComboTarget(); if (new_unit == combo_unit) - { return true; - } + return new_time < old_time; } // all targets are far away, choose the closest one @@ -319,7 +301,7 @@ class FindMaxHpTargetStrategy : public FindTargetStrategy public: FindMaxHpTargetStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI), maxHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr*) override + void CheckAttacker(Unit* attacker, ThreatManager*) override { if (Group* group = botAI->GetBot()->GetGroup()) { diff --git a/src/Ai/Base/Value/EnemyPlayerValue.cpp b/src/Ai/Base/Value/EnemyPlayerValue.cpp index c2f6e056a..41206a4eb 100644 --- a/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -5,6 +5,7 @@ #include "EnemyPlayerValue.h" +#include "CombatManager.h" #include "Playerbots.h" #include "ServerFacade.h" #include "Vehicle.h" @@ -51,34 +52,21 @@ Unit* EnemyPlayerValue::Calculate() controllingVehicle = true; } - // 1. Check units we are currently in combat with. + // 1. Check units we are currently in PvP combat with. std::vector targets; Unit* pVictim = bot->GetVictim(); - HostileReference* pReference = bot->getHostileRefMgr().getFirst(); - while (pReference) + for (auto const& [guid, combatRef] : bot->GetCombatManager().GetPvPCombatRefs()) { - ThreatMgr* threatMgr = pReference->GetSource(); - if (Unit* pTarget = threatMgr->GetOwner()) - { - if (pTarget != pVictim && pTarget->IsPlayer() && pTarget->CanSeeOrDetect(bot) && - bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) - { - if (bot->GetTeamId() == TEAM_HORDE) - { - if (pTarget->HasAura(23333)) - return pTarget; - } - else - { - if (pTarget->HasAura(23335)) - return pTarget; - } + Unit* pTarget = combatRef->GetOther(bot); + if (!pTarget || pTarget == pVictim || !pTarget->IsPlayer() || !pTarget->CanSeeOrDetect(bot) || + !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) + continue; - targets.push_back(pTarget); - } - } + if ((bot->GetTeamId() == TEAM_HORDE && Target->HasAura(23333)) || + (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335))) + return pTarget; - pReference = pReference->next(); + targets.push_back(pTarget); } if (!targets.empty()) diff --git a/src/Ai/Base/Value/LeastHpTargetValue.cpp b/src/Ai/Base/Value/LeastHpTargetValue.cpp index 2992952b3..c185628fa 100644 --- a/src/Ai/Base/Value/LeastHpTargetValue.cpp +++ b/src/Ai/Base/Value/LeastHpTargetValue.cpp @@ -13,7 +13,7 @@ class FindLeastHpTargetStrategy : public FindNonCcTargetStrategy public: FindLeastHpTargetStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minHealth(0) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (IsCcTarget(attacker)) return; diff --git a/src/Ai/Base/Value/TankTargetValue.cpp b/src/Ai/Base/Value/TankTargetValue.cpp index 90c759a7d..80def1cf9 100644 --- a/src/Ai/Base/Value/TankTargetValue.cpp +++ b/src/Ai/Base/Value/TankTargetValue.cpp @@ -15,12 +15,11 @@ class FindTargetForTankStrategy : public FindNonCcTargetStrategy public: FindTargetForTankStrategy(PlayerbotAI* botAI) : FindNonCcTargetStrategy(botAI), minThreat(0) {} - void CheckAttacker(Unit* creature, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* creature, ThreatManager* threatMgr) override { if (!creature || !creature->IsAlive()) - { return; - } + Player* bot = botAI->GetBot(); float threat = threatMgr->GetThreat(bot); if (!result) @@ -29,14 +28,10 @@ public: result = creature; } // neglect if victim is main tank, or no victim (for untauntable target) - if (threatMgr->getCurrentVictim()) + if (Unit* victim = threatMgr->GetCurrentVictim()) { - // float max_threat = threatMgr->GetThreat(threatMgr->getCurrentVictim()->getTarget()); - Unit* victim = threatMgr->getCurrentVictim()->getTarget(); - if (victim && victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) - { + if (victim->ToPlayer() && botAI->IsMainTank(victim->ToPlayer())) return; - } } if (minThreat >= threat) { @@ -54,7 +49,7 @@ class FindTankTargetSmartStrategy : public FindTargetStrategy public: FindTankTargetSmartStrategy(PlayerbotAI* botAI) : FindTargetStrategy(botAI) {} - void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) override + void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) override { if (Group* group = botAI->GetBot()->GetGroup()) { @@ -63,13 +58,10 @@ public: return; } if (!attacker->IsAlive()) - { return; - } + if (!result || IsBetter(attacker, result)) - { result = attacker; - } } bool IsBetter(Unit* new_unit, Unit* old_unit) { @@ -80,6 +72,7 @@ public: { if (old_unit == currentTarget) return false; + if (new_unit == currentTarget) return true; } @@ -89,26 +82,22 @@ public: float old_dis = bot->GetDistance(old_unit); // hasAggro? -> withinMelee? -> threat if (GetIntervalLevel(new_unit) != GetIntervalLevel(old_unit)) - { return GetIntervalLevel(new_unit) > GetIntervalLevel(old_unit); - } + int32_t interval = GetIntervalLevel(new_unit); if (interval == 2) - { return new_dis < old_dis; - } + return new_threat < old_threat; } int32_t GetIntervalLevel(Unit* unit) { if (!botAI->HasAggro(unit)) - { return 2; - } + if (botAI->GetBot()->IsWithinMeleeRange(unit)) - { return 1; - } + return 0; } }; diff --git a/src/Ai/Base/Value/TargetValue.cpp b/src/Ai/Base/Value/TargetValue.cpp index 21621545e..19578daf4 100644 --- a/src/Ai/Base/Value/TargetValue.cpp +++ b/src/Ai/Base/Value/TargetValue.cpp @@ -5,12 +5,13 @@ #include "TargetValue.h" +#include "CombatManager.h" #include "LastMovementValue.h" #include "ObjectGuid.h" #include "Playerbots.h" #include "RtiTargetValue.h" #include "ScriptedCreature.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" Unit* FindTargetStrategy::GetResult() { return result; } @@ -23,8 +24,8 @@ Unit* TargetValue::FindTarget(FindTargetStrategy* strategy) if (!unit) continue; - ThreatMgr& ThreatMgr = unit->GetThreatMgr(); - strategy->CheckAttacker(unit, &ThreatMgr); + ThreatManager& threatMgr = unit->GetThreatMgr(); + strategy->CheckAttacker(unit, &threatMgr); } return strategy->GetResult(); @@ -144,24 +145,23 @@ Unit* FindTargetValue::Calculate() { return nullptr; } - HostileReference* ref = bot->getHostileRefMgr().getFirst(); - while (ref) + for (auto const& [guid, ref] : bot->GetThreatMgr().GetThreatenedByMeList()) { - ThreatMgr* threatManager = ref->GetSource(); - Unit* unit = threatManager->GetOwner(); + Unit* unit = ref->GetOwner(); + if (!unit) + continue; + std::wstring wnamepart; Utf8toWStr(unit->GetName(), wnamepart); wstrToLower(wnamepart); if (!qualifier.empty() && qualifier.length() == wnamepart.length() && Utf8FitTo(qualifier, wnamepart)) - { return unit; - } - ref = ref->next(); } + return nullptr; } -void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatMgr* threatManager) +void FindBossTargetStrategy::CheckAttacker(Unit* attacker, ThreatManager* threatManager) { UnitAI* unitAI = attacker->GetAI(); BossAI* bossAI = dynamic_cast(unitAI); diff --git a/src/Ai/Base/Value/TargetValue.h b/src/Ai/Base/Value/TargetValue.h index fcd7a5889..e9e6cdea4 100644 --- a/src/Ai/Base/Value/TargetValue.h +++ b/src/Ai/Base/Value/TargetValue.h @@ -11,7 +11,7 @@ #include "Value.h" class PlayerbotAI; -class ThreatMgr; +class ThreatManager; class Unit; class FindTargetStrategy @@ -20,7 +20,7 @@ public: FindTargetStrategy(PlayerbotAI* botAI) : result(nullptr), botAI(botAI) {} Unit* GetResult(); - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatMgr) = 0; + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatMgr) = 0; void GetPlayerCount(Unit* creature, uint32* tankCount, uint32* dpsCount); bool IsHighPriority(Unit* attacker); @@ -129,7 +129,7 @@ class FindBossTargetStrategy : public FindTargetStrategy { public: FindBossTargetStrategy(PlayerbotAI* ai) : FindTargetStrategy(ai) {} - virtual void CheckAttacker(Unit* attacker, ThreatMgr* threatManager); + virtual void CheckAttacker(Unit* attacker, ThreatManager* threatManager); }; class BossTargetValue : public TargetValue, public Qualified diff --git a/src/Ai/Base/Value/ThreatValues.cpp b/src/Ai/Base/Value/ThreatValues.cpp index d95b00142..afd253193 100644 --- a/src/Ai/Base/Value/ThreatValues.cpp +++ b/src/Ai/Base/Value/ThreatValues.cpp @@ -6,7 +6,7 @@ #include "ThreatValues.h" #include "Playerbots.h" -#include "ThreatMgr.h" +#include "ThreatManager.h" uint8 ThreatValue::Calculate() { From d0d1171e067997402dc266544ef473011ee68669 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Sun, 22 Mar 2026 15:22:03 +0100 Subject: [PATCH 16/17] Fixed typo (#2230) ## Pull Request Description Fixed typo ## How to Test the Changes - compile test-staging with 20260320-ac-merge ## Impact Assessment - Does this change increase per-bot/per-tick processing or risk scaling poorly with thousands of bots? - - [x] No, not at all - - [ ] Minimal impact (**explain below**) - - [ ] Moderate impact (**explain below**) - Does this change modify default bot behavior? - - [x] No - - [ ] Yes (**explain why**) - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) ## Messages to Translate Does this change add bot messages to translate? - - [x] No - - [ ] Yes (**list messages in the table**) | Message key | Default message | | --------------- | ------------------ | | | | | | | ## AI Assistance Was AI assistance used while working on this change? - - [x] No - - [ ] Yes (**explain below**) ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers --- src/Ai/Base/Value/EnemyPlayerValue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ai/Base/Value/EnemyPlayerValue.cpp b/src/Ai/Base/Value/EnemyPlayerValue.cpp index 41206a4eb..3732e83cb 100644 --- a/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -62,7 +62,7 @@ Unit* EnemyPlayerValue::Calculate() !bot->IsWithinDist(pTarget, VISIBILITY_DISTANCE_NORMAL)) continue; - if ((bot->GetTeamId() == TEAM_HORDE && Target->HasAura(23333)) || + if ((bot->GetTeamId() == TEAM_HORDE && pTarget->HasAura(23333)) || (bot->GetTeamId() == TEAM_ALLIANCE && pTarget->HasAura(23335))) return pTarget; From f00fe15ff120b373edce0cb5d5a7d71d9963cfb5 Mon Sep 17 00:00:00 2001 From: kadeshar Date: Mon, 23 Mar 2026 06:31:24 +0100 Subject: [PATCH 17/17] PR template checkboxes displaying fix (#2232) Maintenance PR --- PULL_REQUEST_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 825717f83..91484cb2e 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -67,7 +67,7 @@ Bot messages have to be translatable, but you don't need to do the translations the message is in a translatable format, and list in the table the message_key and the default English message. Search for GetBotTextOrDefault in the codebase for examples. --> -Does this change add bot messages to translate? +- Does this change add bot messages to translate? - - [ ] No - - [ ] Yes (**list messages in the table**) @@ -81,7 +81,7 @@ Does this change add bot messages to translate? AI assistance is allowed, but all submitted code must be fully understood, reviewed, and owned by the contributor. We expect contributors to be honest about what they do and do not understand. --> -Was AI assistance used while working on this change? +- Was AI assistance used while working on this change? - - [ ] No - - [ ] Yes (**explain below**)