mirror of
https://github.com/liyunfan1223/mod-playerbots.git
synced 2026-06-20 23:49:25 +02:00
<!-- Thank you for contributing to mod-playerbots, please make sure that you... 1. Submit your PR to the test-staging branch, not master. 2. Read the guidelines below before submitting. 3. Don't delete parts of this template. DESIGN PHILOSOPHY: We prioritize STABILITY, PERFORMANCE, AND PREDICTABILITY over behavioral realism. Every action and decision executes PER BOT AND PER TRIGGER. Small increases in logic complexity scale poorly across thousands of bots and negatively affect all. We prioritize a stable system over a smarter one. Bots don't need to behave perfectly; believable behavior is the goal, not human simulation. Default behavior must be cheap in processing; expensive behavior must be opt-in. Before submitting, make sure your changes aligns with these principles. --> ## Pull Request Description <!-- Describe what this change does and why it is needed --> Hello playerbots community! I have been working diligently whilst on vacation to help get pvp gear up and running for pvp specs. Throughout this process, I have looked at our current autogear system, tested it through and through, and made some changes to make gearing more appropriate per spec. _I am going to have my description of the changes in italics_, **and the AI description overview will be bolded.** Let's begin! **This PR makes some improvements to the bot autogear system across item scoring, spec tracking(pvp specs and gear), and stat weights. Changes are split between those that are always active and those controlled by new config options.** **Mandatory Changes:** **PvP Spec Detection (IsSpecPvp) A new method RandomPlayerbotMgr::IsSpecPvp(botGuid, cls) checks the bot's stored specNo against the spec name string defined in config. If the name contains "pvp", the bot is treated as a PvP spec throughout the entire gear pipeline. This is the single source of truth used by both InitEquipment() and ItemUsageValue. In the future this detection can be expanded to drive bot behavior decisions — such as prioritizing dueling players in the world, joining Wintergrasp, or preferring BG and Arena queues over PvE content.** _This is scalable, so if someone were to create their own pvp spec in the config, it would still be tracked if the name contains "pvp". I like the idea of pvp specced random bots having an identifier for pvp events._ **PvP Weights Applied During Loot Evaluation ItemUsageValue::QueryItemUsageForEquip() now calls IsSpecPvp() before scoring a looted item. If the bot is on a PvP spec, it passes SetPvpSpec(true) to the StatsWeightCalculator, ensuring looted items are evaluated with PvP stat priorities (including resilience weighting) rather than PvE weights. Previously, a PvP-specced bot would score loot identically to a PvE bot.** _So, during autogear and upgrade equips, pvp specced bots will now heavily prioritize resilience. On the flip side, pve bots really don't want resilience gear, so a negative weight modifier (penalty for resilience items) has been applied to pve autogearing and upgrade equips. This is important, because you can switch a bot from a pve spec to a pvp spec, and it will automatically consider resilience items in it's inventory as upgrades, and equip them. Same for when you switch a bot from a pvp spec back to a pve spec - the resilience penalty will encourage the bot to switch back to the best available pve gear._ **Resilience Weighting After all per-spec weights are generated in GenerateBasicWeights(), a global resilience modifier is applied unconditionally:** **PvP specs: +7.0 resilience weight — strongly prioritizes resilience gear Non-PvP specs: −3.0 resilience weight — actively discourages resilience gear Resilience is additionally excluded entirely from trinket slot scoring via SetExcludeResilience(true), preventing the PvP resilience bonus from inflating the scores of non-CC trinkets.** _I tried several different numbers here - as high as 10 and as low as 3 for resilience. I ended up with 7 so nearly all specs will slot resilience in every slot EXCEPT for trinkets. I stopped weighing resilience on trinkets because they ended up being garbage trinkets for the most part - other endgame pve trinkets were way more impactful. In my testing, the only class/specs that wont use 100% resilience gears are the tanks, since defense rating/parry/block/dodge weights are so high._ **CC-Break Trinket Cache At server startup, PlayerbotFactory::BuildCcBreakTrinketCache() queries the world database for all trinkets (InventoryType=12, Quality≥2) whose spell IDs include spell 42292 — the CC-break / PvP trinket effect shared by items like Medallion of the Alliance/Horde. Results are sorted by item level descending and cached in a static vector, ready for fast lookup during gearing.** _This creates a cache of cc trinkets on startup, for this:_ **CC-Break Trinket Force-Equip During InitEquipment(), PvP-specced bots at level 50 or higher (level minimum for autogear to apply trinkets) run a pre-selection pass over ccBreakTrinketCache to find the best CC-break trinket they meet the level requirement and quality limit for. Human and Undead bots are excluded from this — they have racial abilities (Every Man for Himself, Will of the Forsaken) that share the PvP trinket cooldown, making a dedicated trinket redundant.** **If a suitable trinket is found, it is stored as pvpTrinket1 and force-equipped into TRINKET1 before the main gear loop runs. If an item already occupies the slot, it is moved to bags first. The second-chance pass also skips TRINKET1 when pvpTrinket1 is set, so the CC trinket is never overwritten.** _This is the catch-all forced pvp trinket for trinket slot 1. In my testing, I really found out how few cc trinkets there are - most of them are epic, and blue ones start showing up super late in the game. An heirloom patch would really help the lower levels, being able to equip a pvp trinket at level 10 or something. Keep in mind, that if your bot isn't getting a pvp trinket with autogear, make sure they aren't human or undead, and check your config for what quality items are allowed with autogear. NOTE - PVP TRINKET STRATEGIES ARE NOT CURRENTLY CODED, SAME WITH CC RACIALS. They will not break out of stun/cc currently. This is for future updates if/when I make a trinketstrategy._ **Enhancement Shaman Dual Wield Fix Classes like Rogues, Frost DKs, and Fury Warriors have their dual wield capability established through class initialization code in the core. Enhancement Shamans acquire Dual Wield only through a specific talent (spell 30798, learned around level 40), and the bot factory had no code to detect and apply this. The result was that Enhancement Shaman bots would sometimes have their offhand weapon unequipped — despite having the talent. After talents are applied in both InitTalentsTree() and InitTalentsBySpecNo(), the code now checks for spell 30798 and explicitly grants SKILL_DUAL_WIELD and SetCanDualWield(true) when present.** _When testing the weapon speed preferences, I noticed that randombot enhancement shamans were unequipping their offhand randomly. They would just walk around with a single 1-hand weapon. This is because they were not considered in the system as dual wielding, so when initequipment or autoequipupgrades was ran, it would unequip the offhand through a function, despite having the dual wield talent. Looking at the code, the other classes already have this flag (warriors, rogues, dks, hunters) because they didn't acquire it through talents._ **CalculateItem() Slot Awareness StatsWeightCalculator::CalculateItem() now accepts an optional slot parameter (default -1). When provided and the item is a weapon, ApplyWeaponSpeedGovernance() can be called. Both item scoring calls inside InitEquipment() — the candidate scoring loop and the incremental old-item comparison — now pass the current equipment slot.** _This change allows the calculate item function to know what slot it's working with, and that's how it modifies it's decision making for some of the optional features below._ **Holy Paladin Weapon Scoring Fix Prior to this change, Holy Paladin could end up equipping 2H weapons because haste and crit sticks (2H weapons) were outscoring appropriate 1H caster weapons — the item type penalty was not catching them correctly. Holy Paladin is now explicitly added to the dual-wield penalty group (preventing 2H weapons from being viable), excluded from the generic caster 1H penalty (since they use 1H + shield rather than a staff), and given a 0.8x soft preference for 1H weapons.** _In autogear testing, sometimes 2h weps with high crit/haste would win over caster gear - this is especially noticeable at lower levels, with shallower item pools (greens only). You'd hit autogear and the holy paladin would equip a 2h axe with crit :( So this makes it so holy paladins only use 1h weapons. They can use either a shield or an offhand, depending on stat weights._ **PvP Spec Slots Added for All Classes The existing RandomClassSpecProb / RandomClassSpecIndex config entries control what percentage of random bots in the world are assigned each spec. Previously only PvE specs (indices 0–2, or 0–3 for Druids) were defined, giving server operators no way to introduce PvP-specced random bots into the world population. This PR adds PvP spec slots for every class (indices 3–6 depending on class), all defaulting to 0 probability. Server operators can raise these values to spawn PvP-specced random bots — e.g., setting RandomClassSpecProb.1.3 = 20 would make 20% of Warrior bots run Arms PvP. Two additional PvE specs have also been added: Death Knight index 3: Double-aura Blood (a hybrid Blood/Frost PvE tank variant) Mage index 3: Frostfire (a PvE hybrid spec) All existing spec entries have been annotated with comments identifying each one (e.g., # arms pve, # holy pve) for readability.** _This change was actually added at the start - I realized that there was no way for pvp-specced randombots to spawn naturally, so I added optional probabilities to the config. They are currently set at 0% by default, but giving the user the option I feel is necessary. Also, it would have been impossible for me to test the init on randombots with pvp gear otherwise. Also, I noticed that the frostfire mage and the dual-aura dk didn't have an option, so I added them in as well, as well as names above each option for quality of life._ **Stat Weight Corrections The following per-spec stat weights were adjusted to better reflect actual WotLK priorities. Entries marked NEW did not previously exist; unmarked rows show old → new values.** <img width="795" height="268" alt="arms warrior" src="https://github.com/user-attachments/assets/cb0deb00-a985-432d-81a1-133fc953088b" /> _Arms warriors would prefer leather/ap gear about half of the time - the combined weights of both would often beat strength gear, especially at lower levels, or where the item pool was shallow. Also, they continued to spawn with spell power gear and defense gear occasionally, especially on gear with resilience (resilience, spell power, crit, haste, stam items)._ <img width="796" height="301" alt="fury warrior" src="https://github.com/user-attachments/assets/715ff3a3-3d20-4e0e-a953-7ed6fd9386db" /> _Fury warriors had the same issues as arms warrior, but really can't afford to lose a strength item - beserker stance increases strength by 20%. Also had to reduce haste here because haste really isn't nearly as important as strength, crit, arp. Haste items would win often over strength/crit/arp gear._ <img width="796" height="300" alt="prot tanks" src="https://github.com/user-attachments/assets/624de09a-2506-4aee-95aa-c49cbc5b85d3" /> _So, prot paladins and prot warriors currently are weighed identically fyi. Look at that whopping 2.0 agility - twice as important as strength? I noticed that my prot paladins/warriors were equipping agility/haste/crit items instead of defense gear on their neck, rings, trinkets, and back. This adjustment pretty much ensures that defense gear takes those slots if it's available. Removed the crit/haste weightings, because realistically if a tank wants more damage, it will just get strength. Lastly added the spell power penalty because prot paladins would spawn in fully holy gear if they were pvp specced (resilience is weighted so high, resilience/spellpower/stam/haste gear would often win). This aims to prevent that._ <img width="802" height="421" alt="dps dks" src="https://github.com/user-attachments/assets/371d1344-2382-4460-b3a7-f38b33025b73" /> _Same issue with plate dps as the warriors had. Spell power gear would occasionally spawn on crit/hit items in pve, and a ton of spell power resilience gear would spawn. There is no scenario where a DK wants spell power, this isn't patch 3.0.1..._ <img width="796" height="447" alt="blood dk" src="https://github.com/user-attachments/assets/c84a2bbf-7daa-4805-acf3-cd3bf815eda4" /> _Similar issues to prot paladin/warrior. I was really tired of seeing block rating/value gear as a result of getting gear with defense/stam. This results in a lot more defense rating/expertise/hit/dodge/parry gear, and basically makes shield stats nearly non-existent (unless the upgrade is good enough, it could still win)_ <img width="794" height="226" alt="ret paladin" src="https://github.com/user-attachments/assets/b09f40ed-b25f-4945-940c-2ce92f81c7c4" /> _Prior to this PR, the positive spellpower and int weights were enough for ret paladins to spawn with spellpower/int/haste/crit gear. This is unlikely now. And agility/ap was reduced to favor more strength gear._ <img width="795" height="306" alt="Enhancement Shaman" src="https://github.com/user-attachments/assets/1e5231fd-36ea-4b7e-a546-cf0075d17bd4" /> _While spell power is a decent stat on enhancment shamans, it was appearing on too much gear, especially on items that were haste/crit/spell power. And for elemental shamans, they were getting agi/haste/crit gear, so this aims to get rid of those items entirely without reducing haste/crit._ <img width="800" height="119" alt="shaman pally" src="https://github.com/user-attachments/assets/6ea3300a-effd-4a03-8f6f-4ae13c5383a5" /> _Holy paladins and resto shamans are scored the same, but this prevents attack power/haste/crit gear, since haste and crit are weighted high._ <img width="1025" height="385" alt="mage" src="https://github.com/user-attachments/assets/03191dfd-dc09-477d-8424-8fd56f3e0d71" /> _Prevents mages from equipping/autogearing items with attack power, some attack power/crit/haste/hit items were winning with shallow item pools._ <img width="1022" height="502" alt="hunter rogue" src="https://github.com/user-attachments/assets/06fa67c9-7709-4ee8-a0e2-34de18594018" /> _Prevents hunters and rogues from getting spell power leather gear with hit/crit. Crit is very heavy for hunters so this was decently common._ **Optional Changes (Config-Controlled)** **AiPlayerbot.PreferClassArmorType (default: 0) Applies a 3x score multiplier to armor matching the bot's class-appropriate type (plate/mail/leather/cloth). A significantly better off-type item can still win — this is a soft preference, not a hard filter.** _Are you tired of your fury warrior being a leather daddy? Are you tired of your holy paladin running around in cloth lingerie? This will fix that. For mail classes (hunters/shamans) and plate classes, this only kicks in after level 40. But it really helps adhere to the highest armor class available. This would be the perfect solution to the quarterly question "Why is my paladin wearing leather?". This definitely should remain optional, as quite a few BIS lists would disagree with it. Leather at certain stages is great for hunters/shamans/warriors/dks._ **AiPlayerbot.AutogearAllowsQuestRewards (default: 0) Builds a cache of equippable armor and weapon quest rewards at startup. Bots can then equip these items during autogear, using the quest's minimum level as the effective required level gate.** _So, I noticed that autogear didn't allow items without a level requirement (quest rewards), because it didn't know how to handle that when giving out gear. It would previously just flat out reject all quest rewards, as they wouldn't be a part of the item pool. This option enables quest rewards to be considered in the item pool, and the level correlates to the lowest level you could get the quest. I have tested this for about 3 hours across all specs and using blue/green gear, it seems like a really nice bonus. Keep in mind that I do 0 quests on my way to 80, so players like me could still benefit from those items. I think this should remain optional._ **AiPlayerbot.EquipAllSlotsAtAnyLevel (default: 0) Bypasses the low-level slot restrictions in InitEquipment(): Trinkets normally locked until level 50 Head/Neck until level 30 Rings until level 20 All other non-weapon slots until level 5** _Autogear currently has level floors for slots - they will not ever give items below the above thresholds. This config option bypasses that. I have not tested this as much as I should have, so as people test this, they could let us know of items that should be blacklisted._ **AiPlayerbot.WeaponSpeedGovernance (default: 0) When enabled, ApplyWeaponSpeedGovernance() applies a 3x score multiplier to weapons matching the spec's ideal attack speed profile. Applies to mainhand, offhand, and ranged slots only. Per-spec preferences: Arms Warrior: Slow 2H (>=3400ms) in mainhand; poleaxes and axes preferred (Axe Specialization) Ret Paladin / Blood & Unholy DK: Slow 2H (>=3400ms) in mainhand Prot Warrior & Paladin: Slow 1H (>=2600ms) in mainhand Fury Warrior dual wield: Slow 1H (>=2600ms) in both hands Fury Warrior titan's grip: Slow 2H (>=3400ms) in both hands Frost DK: Slow 1H (>=2600ms) in both hands; 2H excluded Enhancement Shaman (dual wield): Slow 1H (>=2600ms) in both hands; synchronized MH/OH speeds for flurry procs Enhancement Shaman (pre-dual wield): Slow 2H (>=3400ms) in mainhand Combat Rogue: Slow MH (>=2600ms) + Fast OH (<=1500ms) Assassination / Subtlety Rogue: Slow dagger MH (>=1700ms) + Fast dagger OH (<=1500ms) Hunter: Slow ranged (>=2600ms); melee is a stat stick, speed ignored Feral Druid: No preference (forms normalize attack speed)** _Besides pvp gearing for pvp specs, I feel like this is one of the nicest additions. It was really frustrating to see an enhancement shaman put windfury on a 1.5 dagger. Without this, weights for melee dps are calculated on dps alone, not weapon speed. You'll see 2h specs use fast 2h weapons (3.0), rogues use 2 fast weapons or slow weapons, frost dks occasionally using 2h weapons while having dual wield talents. I tested this for about 6 hours across all mentioned specs at levels 20, 30, 40, 50, 60, 65, 70, 75, and 80, with 3 quality types (greens, blues, purples). I would actually consider making this mandatory, simply because of the impact I saw in the dps charts. Super happy and proud of this._ <img width="1021" height="470" alt="files changes" src="https://github.com/user-attachments/assets/f55d955c-8760-4adf-b4d9-84797da2dc65" /> ## Feature Evaluation <!-- If your PR is very minimal (comment typo, wrong ID reference, etc), and it is very obvious it will not have any impact on performance, you may skip these question. If necessary, a maintainer may ask you for them later. --> <!-- Please answer the following: --> - Describe the **minimum logic** required to achieve the intended behavior. _Two caches are built upon startup - the pvp trinket cache and the quest reward cache. From there, this directly modifies the stat weight calculations involving initequipement (autogear) and autoequipupgrades, as both go off of stat weight calculations. I tried to implement these changes with as little custom functions and coding as possible, and relied as much as I could on the pre-existing framework._ - Describe the **processing cost** when this logic executes across many bots. _Fortunately most of the gates are boolean so it shouldn't impact performance much at all. I ran these changes on my local server with stock 500 bots, noticed no pmon difference from the main branch. Did a 24h stress test on my server yesterday, stats looked consistent with the stress test I did prior to making any changes on 3-31-26._ _It helps that it uses pre-existing functions such as initequipment and autoequipupgrades, and it really just modifies them with slightly more logic. That being said, autogear didn't lag my server at all, nor did the bots equipping upgrades._ ## How to Test the Changes <!-- - Step-by-step instructions to test the change. - Any required setup (e.g. multiple players, number of bots, specific configuration). - Expected behavior and how to verify it. --> _So, with the basic stock playerbots config (do not forget to copy the new config!), the only thing that should change is the pvp gear appearing on pvp specs, and the classes preferring more appropriate stats across the board. You can load into the game, level a bot to 20, autogear, and notice the difference. Same at level 40, 60, 75, or whatever. You could add in the optional config settings to further streamline the gear you want. I currently run with all 4 enabled, 2 of which increase the item pool, and 2 of which help guide them to more appropriate gear (armor/weps)._ ## Impact Assessment <!-- As a generic test, before and after measure of pmon (playerbot pmon tick) can help you here. --> - 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**) _The code is only used on startup (cache generation) and when autogear/autoequipupgrades is called. Not all the time, and not per tick. I noticed no performance impact after these changes._ - Does this change modify default bot behavior? - - [ ] No - - [x] Yes (**explain why**) _It modifies the decision making as far as equipment goes, but as far as priority/strategies, this does not affect that._ - Does this change add new decision branches or increase maintenance complexity? - - [x] No - - [ ] Yes (**explain below**) _Not to my knowledge, but I'll rely on testers and the community to let me know if it does._ ## AI Assistance <!-- 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? - - [ ] No - - [x] Yes (**explain below**) <!-- If yes, please specify: - Purpose of usage (e.g. brainstorming, refactoring, documentation, code generation). - Which parts of the change were influenced or generated, and whether it was thoroughly reviewed. --> _AI was used in the research of the initequipment system, stat weights, and cache building. As far as generating the code, using AI was 2 steps forward, 1 step back. I used Claude Code with Sonnet 4.6 (high) and had gemini/copilot review the work. **AI did generate a large portion of the code being used.** I have personally reviewed every line, and a lot was removed out of being obsolete/new system that copied an old one/too many comments. I don't think anything else can be trimmed, though. I also used AI in the PR description, and made my own comments in italics below each entry. I hate explaining/writing._ <!-- TRANSLATIONS: Anything new that the bots say in chat must be in a translatable format. This is done using GetBotTextOrDefault, which you can search for in the codebase to find examples. Your code needs to have English as the default fallback, while the full translations need to be in an SQL update file. The languages in the file are the nine language options supported by AzerothCore: English, Korean, French, German, Chinese, Taiwanese, Spanish, Spanish Mexico, and Russian. See data/sql/playerbots/updates/2025_12_27_ai_playerbot_fishing_text.sql as an example of a translation SQL update, whose content are called within the codebase at src/strategy/actions/FishingAction.cpp --> ## Final Checklist - - [x] Stability is not compromised. - - [x] Performance impact is understood, tested, and acceptable. - - [x] Added logic complexity is justified and explained. - - [x] Any new bot dialogue lines are translated. - - [x] Documentation updated if needed (Conf comments, WiKi commands). ## Notes for Reviewers <!-- Anything else that's helpful to review or test your pull request. --> _I would like atleast 5-10 people to review this over the next 1-6 months. The big problem I used to have with my PRs was I was acting like they were a sprint, when it's really a marathon - good changes take time, and I was too quick to bust out new content. The old PRs I made introduced just as many new bugs as they did features. I learned my lesson, and have tested this extensively (code was pretty much complete on 4-10-26, been testing alone for the last 11 days) and it's ready for the test realm for others to try out. I think it's going to be a good step forward when it comes to gear decision making for bots as a whole. PvPers have come and gone too much from this project due to the lack of options, and this helps captivate that audience. Please reach out to me on discord at Zhur#4391, I am happy to hear results/suggestions there as well as here._ --------- Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
3127 lines
104 KiB
C++
3127 lines
104 KiB
C++
/*
|
|
* Copyright (C) 2016+ AzerothCore <www.azerothcore.org>, released under GNU AGPL v3 license, you may redistribute it
|
|
* and/or modify it under version 3 of the License, or (at your option), any later version.
|
|
*/
|
|
|
|
#include "RandomPlayerbotMgr.h"
|
|
|
|
#include <WorldSessionMgr.h>
|
|
|
|
#include <algorithm>
|
|
#include <boost/thread/thread.hpp>
|
|
#include <cstdlib>
|
|
#include <ctime>
|
|
#include <iomanip>
|
|
#include <random>
|
|
|
|
#include "AiFactory.h"
|
|
#include "Battleground.h"
|
|
#include "BattlegroundMgr.h"
|
|
#include "ChannelMgr.h"
|
|
#include "DBCStores.h"
|
|
#include "DBCStructure.h"
|
|
#include "DatabaseEnv.h"
|
|
#include "Define.h"
|
|
#include "FleeManager.h"
|
|
#include "GridNotifiers.h"
|
|
#include "LFGMgr.h"
|
|
#include "MapMgr.h"
|
|
#include "NewRpgInfo.h"
|
|
#include "NewRpgStrategy.h"
|
|
#include "ObjectGuid.h"
|
|
#include "PerfMonitor.h"
|
|
#include "Player.h"
|
|
#include "PlayerbotAI.h"
|
|
#include "PlayerbotAIConfig.h"
|
|
#include "PlayerbotFactory.h"
|
|
#include "Playerbots.h"
|
|
#include "Position.h"
|
|
#include "RaceMgr.h"
|
|
#include "Random.h"
|
|
#include "RandomPlayerbotFactory.h"
|
|
#include "ServerFacade.h"
|
|
#include "SharedDefines.h"
|
|
#include "TravelMgr.h"
|
|
#include "Unit.h"
|
|
#include "World.h"
|
|
#include "Cell.h"
|
|
#include "GridNotifiers.h"
|
|
#include "CellImpl.h"
|
|
#include "GridNotifiersImpl.h"
|
|
|
|
struct GuidClassRaceInfo
|
|
{
|
|
ObjectGuid::LowType guid;
|
|
uint32 rClass;
|
|
uint32 rRace;
|
|
};
|
|
|
|
void PrintStatsThread() { sRandomPlayerbotMgr.PrintStats(); }
|
|
|
|
void activatePrintStatsThread()
|
|
{
|
|
boost::thread t(PrintStatsThread);
|
|
t.detach();
|
|
}
|
|
|
|
void CheckBgQueueThread() { sRandomPlayerbotMgr.CheckBgQueue(); }
|
|
|
|
void activateCheckBgQueueThread()
|
|
{
|
|
boost::thread t(CheckBgQueueThread);
|
|
t.detach();
|
|
}
|
|
|
|
void CheckLfgQueueThread() { sRandomPlayerbotMgr.CheckLfgQueue(); }
|
|
|
|
void activateCheckLfgQueueThread()
|
|
{
|
|
boost::thread t(CheckLfgQueueThread);
|
|
t.detach();
|
|
}
|
|
|
|
void CheckPlayersThread() { sRandomPlayerbotMgr.CheckPlayers(); }
|
|
|
|
void activateCheckPlayersThread()
|
|
{
|
|
boost::thread t(CheckPlayersThread);
|
|
t.detach();
|
|
}
|
|
|
|
class botPIDImpl
|
|
{
|
|
public:
|
|
botPIDImpl(double dt, double max, double min, double Kp, double Ki, double Kd);
|
|
~botPIDImpl();
|
|
double calculate(double setpoint, double pv);
|
|
void adjust(double Kp, double Ki, double Kd)
|
|
{
|
|
_Kp = Kp;
|
|
_Ki = Ki;
|
|
_Kd = Kd;
|
|
}
|
|
void reset() { _integral = 0; }
|
|
|
|
private:
|
|
double _dt;
|
|
double _max;
|
|
double _min;
|
|
double _Kp;
|
|
double _Ki;
|
|
double _Kd;
|
|
double _pre_error;
|
|
double _integral;
|
|
};
|
|
|
|
botPID::botPID(double dt, double max, double min, double Kp, double Ki, double Kd)
|
|
{
|
|
pimpl = new botPIDImpl(dt, max, min, Kp, Ki, Kd);
|
|
}
|
|
void botPID::adjust(double Kp, double Ki, double Kd) { pimpl->adjust(Kp, Ki, Kd); }
|
|
void botPID::reset() { pimpl->reset(); }
|
|
double botPID::calculate(double setpoint, double pv) { return pimpl->calculate(setpoint, pv); }
|
|
botPID::~botPID() { delete pimpl; }
|
|
|
|
/**
|
|
* Implementation
|
|
*/
|
|
botPIDImpl::botPIDImpl(double dt, double max, double min, double Kp, double Ki, double Kd)
|
|
: _dt(dt), _max(max), _min(min), _Kp(Kp), _Ki(Ki), _Kd(Kd), _pre_error(0), _integral(0)
|
|
{
|
|
}
|
|
|
|
double botPIDImpl::calculate(double setpoint, double pv)
|
|
{
|
|
// Calculate error
|
|
double error = setpoint - pv;
|
|
|
|
// Proportional term
|
|
double Pout = _Kp * error;
|
|
|
|
// Integral term
|
|
_integral += error * _dt;
|
|
double Iout = _Ki * _integral;
|
|
|
|
// Derivative term
|
|
double derivative = (error - _pre_error) / _dt;
|
|
double Dout = _Kd * derivative;
|
|
|
|
// Calculate total output
|
|
double output = Pout + Iout + Dout;
|
|
|
|
// Restrict to max/min
|
|
if (output > _max)
|
|
{
|
|
output = _max;
|
|
_integral -= error * _dt; // Stop integral buildup at max
|
|
}
|
|
else if (output < _min)
|
|
{
|
|
output = _min;
|
|
_integral -= error * _dt; // Stop integral buildup at min
|
|
}
|
|
|
|
// Save error to previous error
|
|
_pre_error = error;
|
|
|
|
return output;
|
|
}
|
|
|
|
botPIDImpl::~botPIDImpl() {}
|
|
|
|
uint32 RandomPlayerbotMgr::GetMaxAllowedBotCount() { return GetEventValue(0, "bot_count"); }
|
|
|
|
void RandomPlayerbotMgr::LogPlayerLocation()
|
|
{
|
|
activeBots = 0;
|
|
|
|
try
|
|
{
|
|
sPlayerbotAIConfig.openLog("player_location.csv", "w");
|
|
|
|
if (sPlayerbotAIConfig.randomBotAutologin)
|
|
{
|
|
for (auto i : GetAllBots())
|
|
{
|
|
Player* bot = i.second;
|
|
if (!bot)
|
|
continue;
|
|
|
|
std::ostringstream out;
|
|
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
|
|
out << "RND"
|
|
<< ",";
|
|
out << bot->GetName() << ",";
|
|
out << std::fixed << std::setprecision(2);
|
|
WorldPosition(bot).printWKT(out);
|
|
out << bot->GetOrientation() << ",";
|
|
out << std::to_string(bot->getRace()) << ",";
|
|
out << std::to_string(bot->getClass()) << ",";
|
|
out << bot->GetMapId() << ",";
|
|
out << bot->GetLevel() << ",";
|
|
out << bot->GetHealth() << ",";
|
|
out << bot->GetPowerPct(bot->getPowerType()) << ",";
|
|
out << bot->GetMoney() << ",";
|
|
|
|
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot))
|
|
{
|
|
out << std::to_string(uint8(botAI->GetGrouperType())) << ",";
|
|
out << std::to_string(uint8(botAI->GetGuilderType())) << ",";
|
|
out << (botAI->AllowActivity(ALL_ACTIVITY) ? "active" : "inactive") << ",";
|
|
out << (botAI->IsActive() ? "active" : "delay") << ",";
|
|
out << botAI->HandleRemoteCommand("state") << ",";
|
|
|
|
if (botAI->AllowActivity(ALL_ACTIVITY))
|
|
activeBots++;
|
|
}
|
|
else
|
|
{
|
|
out << 0 << "," << 0 << ",err,err,err,";
|
|
}
|
|
|
|
out << (bot->IsInCombat() ? "combat" : "safe") << ",";
|
|
out << (bot->isDead() ? (bot->GetCorpse() ? "ghost" : "dead") : "alive");
|
|
|
|
sPlayerbotAIConfig.log("player_location.csv", out.str().c_str());
|
|
}
|
|
|
|
for (auto i : GetPlayers())
|
|
{
|
|
Player* bot = i;
|
|
if (!bot)
|
|
continue;
|
|
|
|
std::ostringstream out;
|
|
out << sPlayerbotAIConfig.GetTimestampStr() << "+00,";
|
|
out << "PLR"
|
|
<< ",";
|
|
out << bot->GetName() << ",";
|
|
out << std::fixed << std::setprecision(2);
|
|
WorldPosition(bot).printWKT(out);
|
|
out << bot->GetOrientation() << ",";
|
|
out << std::to_string(bot->getRace()) << ",";
|
|
out << std::to_string(bot->getClass()) << ",";
|
|
out << bot->GetMapId() << ",";
|
|
out << bot->GetLevel() << ",";
|
|
out << bot->GetHealth() << ",";
|
|
out << bot->GetPowerPct(bot->getPowerType()) << ",";
|
|
out << bot->GetMoney() << ",";
|
|
|
|
if (PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot))
|
|
{
|
|
out << std::to_string(uint8(botAI->GetGrouperType())) << ",";
|
|
out << std::to_string(uint8(botAI->GetGuilderType())) << ",";
|
|
out << (botAI->AllowActivity(ALL_ACTIVITY) ? "active" : "inactive") << ",";
|
|
out << (botAI->IsActive() ? "active" : "delay") << ",";
|
|
out << botAI->HandleRemoteCommand("state") << ",";
|
|
|
|
if (botAI->AllowActivity(ALL_ACTIVITY))
|
|
activeBots++;
|
|
}
|
|
else
|
|
{
|
|
out << 0 << "," << 0 << ",player,player,player,";
|
|
}
|
|
|
|
out << (bot->IsInCombat() ? "combat" : "safe") << ",";
|
|
out << (bot->isDead() ? (bot->GetCorpse() ? "ghost" : "dead") : "alive");
|
|
|
|
sPlayerbotAIConfig.log("player_location.csv", out.str().c_str());
|
|
}
|
|
}
|
|
}
|
|
catch (...)
|
|
{
|
|
return;
|
|
// This is to prevent some thread-unsafeness. Crashes would happen if bots get added or removed.
|
|
// We really don't care here. Just skip a log. Making this thread-safe is not worth the effort.
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::UpdateAIInternal(uint32 /*elapsed*/, bool /*minimal*/)
|
|
{
|
|
if (totalPmo)
|
|
totalPmo->finish();
|
|
|
|
totalPmo = sPerfMonitor.start(PERF_MON_TOTAL, "RandomPlayerbotMgr::FullTick");
|
|
|
|
if (!sPlayerbotAIConfig.randomBotAutologin || !sPlayerbotAIConfig.enabled)
|
|
return;
|
|
|
|
/*if (sPlayerbotAIConfig.enablePrototypePerformanceDiff)
|
|
{
|
|
LOG_INFO("playerbots", "---------------------------------------");
|
|
LOG_INFO("playerbots",
|
|
"PROTOTYPE: Playerbot performance enhancements are active. Issues and instability may occur.");
|
|
LOG_INFO("playerbots", "---------------------------------------");
|
|
ScaleBotActivity();
|
|
}*/
|
|
|
|
uint32 maxAllowedBotCount = GetEventValue(0, "bot_count");
|
|
if (!maxAllowedBotCount || (maxAllowedBotCount < sPlayerbotAIConfig.minRandomBots ||
|
|
maxAllowedBotCount > sPlayerbotAIConfig.maxRandomBots))
|
|
{
|
|
maxAllowedBotCount = urand(sPlayerbotAIConfig.minRandomBots, sPlayerbotAIConfig.maxRandomBots);
|
|
SetEventValue(0, "bot_count", maxAllowedBotCount,
|
|
urand(sPlayerbotAIConfig.randomBotCountChangeMinInterval,
|
|
sPlayerbotAIConfig.randomBotCountChangeMaxInterval));
|
|
}
|
|
|
|
GetBots();
|
|
std::list<uint32> availableBots = currentBots;
|
|
uint32 availableBotCount = availableBots.size();
|
|
uint32 onlineBotCount = playerBots.size();
|
|
|
|
uint32 onlineBotFocus = 75;
|
|
if (onlineBotCount < (uint32)(sPlayerbotAIConfig.minRandomBots * 90 / 100))
|
|
onlineBotFocus = 25;
|
|
|
|
// only keep updating till initializing time has completed,
|
|
// which prevents unneeded expensive GameTime calls.
|
|
if (_isBotInitializing)
|
|
{
|
|
_isBotInitializing = GameTime::GetUptime().count() < sPlayerbotAIConfig.maxRandomBots * (0.11 + 0.4);
|
|
}
|
|
|
|
uint32 updateIntervalTurboBoost = _isBotInitializing ? 1 : sPlayerbotAIConfig.randomBotUpdateInterval;
|
|
SetNextCheckDelay(updateIntervalTurboBoost * (onlineBotFocus + 25) * 10);
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(
|
|
PERF_MON_TOTAL,
|
|
onlineBotCount < maxAllowedBotCount ? "RandomPlayerbotMgr::Login" : "RandomPlayerbotMgr::UpdateAIInternal");
|
|
|
|
bool realPlayerIsLogged = false;
|
|
if (sPlayerbotAIConfig.disabledWithoutRealPlayer)
|
|
{
|
|
if (sWorldSessionMgr->GetActiveAndQueuedSessionCount() > 0)
|
|
{
|
|
RealPlayerLastTimeSeen = time(nullptr);
|
|
realPlayerIsLogged = true;
|
|
|
|
if (DelayLoginBotsTimer == 0)
|
|
{
|
|
DelayLoginBotsTimer = time(nullptr) + sPlayerbotAIConfig.disabledWithoutRealPlayerLoginDelay;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (DelayLoginBotsTimer)
|
|
{
|
|
DelayLoginBotsTimer = 0;
|
|
}
|
|
|
|
if (RealPlayerLastTimeSeen != 0 && onlineBotCount > 0 &&
|
|
time(nullptr) > RealPlayerLastTimeSeen + sPlayerbotAIConfig.disabledWithoutRealPlayerLogoutDelay)
|
|
{
|
|
LogoutAllBots();
|
|
LOG_INFO("playerbots", "Logout all bots due no real player session.");
|
|
}
|
|
}
|
|
|
|
if (availableBotCount < maxAllowedBotCount &&
|
|
(sPlayerbotAIConfig.disabledWithoutRealPlayer == false ||
|
|
(realPlayerIsLogged && DelayLoginBotsTimer != 0 && time(nullptr) >= DelayLoginBotsTimer)))
|
|
{
|
|
AddRandomBots();
|
|
}
|
|
}
|
|
else if (availableBotCount < maxAllowedBotCount)
|
|
{
|
|
AddRandomBots();
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.syncLevelWithPlayers && !players.empty())
|
|
{
|
|
if (time(nullptr) > (PlayersCheckTimer + 60))
|
|
sRandomPlayerbotMgr.CheckPlayers();
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.randomBotJoinBG /* && !players.empty()*/)
|
|
{
|
|
if (time(nullptr) > (BgCheckTimer + 35))
|
|
sRandomPlayerbotMgr.CheckBgQueue();
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.randomBotJoinLfg /* && !players.empty()*/)
|
|
{
|
|
if (time(nullptr) > (LfgCheckTimer + 30))
|
|
sRandomPlayerbotMgr.CheckLfgQueue();
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.randomBotAutologin && time(nullptr) > (printStatsTimer + 300))
|
|
{
|
|
if (!printStatsTimer)
|
|
{
|
|
printStatsTimer = time(nullptr);
|
|
}
|
|
else
|
|
{
|
|
sRandomPlayerbotMgr.PrintStats();
|
|
// activatePrintStatsThread();
|
|
}
|
|
}
|
|
uint32 updateBots = sPlayerbotAIConfig.randomBotsPerInterval * onlineBotFocus / 100;
|
|
uint32 maxNewBots =
|
|
onlineBotCount < maxAllowedBotCount &&
|
|
(sPlayerbotAIConfig.disabledWithoutRealPlayer == false ||
|
|
(realPlayerIsLogged && DelayLoginBotsTimer != 0 && time(nullptr) >= DelayLoginBotsTimer))
|
|
? maxAllowedBotCount - onlineBotCount
|
|
: 0;
|
|
uint32 loginBots = std::min(sPlayerbotAIConfig.randomBotsPerInterval - updateBots, maxNewBots);
|
|
|
|
if (!availableBots.empty())
|
|
{
|
|
// Update bots
|
|
for (auto bot : availableBots)
|
|
{
|
|
if (!GetPlayerBot(bot))
|
|
continue;
|
|
|
|
if (ProcessBot(bot))
|
|
{
|
|
updateBots--;
|
|
}
|
|
|
|
if (!updateBots)
|
|
break;
|
|
}
|
|
|
|
if (loginBots && botLoading.empty())
|
|
{
|
|
loginBots += updateBots;
|
|
loginBots = std::min(loginBots, maxNewBots);
|
|
|
|
LOG_DEBUG("playerbots", "{} new bots prepared to login", loginBots);
|
|
|
|
// Log in bots
|
|
for (auto bot : availableBots)
|
|
{
|
|
if (GetPlayerBot(bot))
|
|
continue;
|
|
|
|
if (ProcessBot(bot))
|
|
{
|
|
loginBots--;
|
|
}
|
|
|
|
if (!loginBots)
|
|
break;
|
|
}
|
|
|
|
DelayLoginBotsTimer = 0;
|
|
}
|
|
}
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
|
|
if (sPlayerbotAIConfig.hasLog("player_location.csv"))
|
|
{
|
|
LogPlayerLocation();
|
|
}
|
|
}
|
|
|
|
// void RandomPlayerbotMgr::ScaleBotActivity()
|
|
//{
|
|
// float activityPercentage = getActivityPercentage();
|
|
//
|
|
// // if (activityPercentage >= 100.0f || activityPercentage <= 0.0f) pid.reset(); //Stop integer buildup during
|
|
// // max/min activity
|
|
//
|
|
// // % increase/decrease wanted diff , avg diff
|
|
// float activityPercentageMod = pid.calculate(
|
|
// sRandomPlayerbotMgr.GetPlayers().empty() ? sPlayerbotAIConfig.diffEmpty :
|
|
// sPlayerbotAIConfig.diffWithPlayer, sWorldUpdateTime.GetAverageUpdateTime());
|
|
//
|
|
// activityPercentage = activityPercentageMod + 50;
|
|
//
|
|
// // Cap the percentage between 0 and 100.
|
|
// activityPercentage = std::max(0.0f, std::min(100.0f, activityPercentage));
|
|
//
|
|
// setActivityPercentage(activityPercentage);
|
|
// }
|
|
|
|
// Assigns accounts as RNDbot accounts (type 1) based on MaxRandomBots and EnablePeriodicOnlineOffline and its ratio,
|
|
// and assigns accounts as AddClass accounts (type 2) based AddClassAccountPoolSize. Type 1 and 2 assignments are
|
|
// permenant, unless MaxRandomBots or AddClassAccountPoolSize are set to 0. If so, their associated accounts will
|
|
// be unassigned (type 0)
|
|
void RandomPlayerbotMgr::AssignAccountTypes()
|
|
{
|
|
LOG_INFO("playerbots", "Assigning account types for random bot accounts...");
|
|
|
|
// Clear existing filtered lists
|
|
rndBotTypeAccounts.clear();
|
|
addClassTypeAccounts.clear();
|
|
|
|
// First, get ALL randombot accounts from the database
|
|
std::vector<uint32> allRandomBotAccounts;
|
|
QueryResult allAccounts = LoginDatabase.Query(
|
|
"SELECT id FROM account WHERE username LIKE '{}%%' ORDER BY id",
|
|
sPlayerbotAIConfig.randomBotAccountPrefix.c_str());
|
|
|
|
if (allAccounts)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = allAccounts->Fetch();
|
|
uint32 accountId = fields[0].Get<uint32>();
|
|
allRandomBotAccounts.push_back(accountId);
|
|
} while (allAccounts->NextRow());
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Found {} total randombot accounts in database", allRandomBotAccounts.size());
|
|
|
|
// Check existing assignments
|
|
QueryResult existingAssignments = PlayerbotsDatabase.Query("SELECT account_id, account_type FROM playerbots_account_type");
|
|
std::map<uint32, uint8> currentAssignments;
|
|
|
|
if (existingAssignments)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = existingAssignments->Fetch();
|
|
uint32 accountId = fields[0].Get<uint32>();
|
|
uint8 accountType = fields[1].Get<uint8>();
|
|
currentAssignments[accountId] = accountType;
|
|
} while (existingAssignments->NextRow());
|
|
}
|
|
|
|
// Mark ALL randombot accounts as unassigned if not already assigned
|
|
for (uint32 accountId : allRandomBotAccounts)
|
|
{
|
|
if (currentAssignments.find(accountId) == currentAssignments.end())
|
|
{
|
|
PlayerbotsDatabase.Execute("INSERT INTO playerbots_account_type (account_id, account_type) VALUES ({}, 0) ON DUPLICATE KEY UPDATE account_type = account_type", accountId);
|
|
currentAssignments[accountId] = 0;
|
|
}
|
|
}
|
|
|
|
// Calculate needed RNDbot accounts
|
|
uint32 neededRndBotAccounts = 0;
|
|
if (sPlayerbotAIConfig.maxRandomBots > 0)
|
|
{
|
|
int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount();
|
|
int maxBots = sPlayerbotAIConfig.maxRandomBots;
|
|
|
|
// Take periodic online-offline into account
|
|
if (sPlayerbotAIConfig.enablePeriodicOnlineOffline)
|
|
{
|
|
maxBots *= sPlayerbotAIConfig.periodicOnlineOfflineRatio;
|
|
}
|
|
|
|
// Calculate base accounts needed for RNDbots, ensuring round up for maxBots not cleanly divisible by the divisor
|
|
neededRndBotAccounts = (maxBots + divisor - 1) / divisor;
|
|
}
|
|
|
|
// Count existing assigned accounts
|
|
uint32 existingRndBotAccounts = 0;
|
|
uint32 existingAddClassAccounts = 0;
|
|
|
|
for (auto const& [accountId, accountType] : currentAssignments)
|
|
{
|
|
if (accountType == 1) existingRndBotAccounts++;
|
|
else if (accountType == 2) existingAddClassAccounts++;
|
|
}
|
|
|
|
// Assign RNDbot accounts from lowest position if needed
|
|
if (existingRndBotAccounts < neededRndBotAccounts)
|
|
{
|
|
uint32 toAssign = neededRndBotAccounts - existingRndBotAccounts;
|
|
uint32 assigned = 0;
|
|
|
|
for (uint32 i = 0; i < allRandomBotAccounts.size() && assigned < toAssign; i++)
|
|
{
|
|
uint32 accountId = allRandomBotAccounts[i];
|
|
if (currentAssignments[accountId] == 0) // Unassigned
|
|
{
|
|
PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 1, assignment_date = NOW() WHERE account_id = {}", accountId);
|
|
currentAssignments[accountId] = 1;
|
|
assigned++;
|
|
}
|
|
}
|
|
|
|
if (assigned < toAssign)
|
|
{
|
|
LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill RNDbot requirements. Need {} more accounts.", toAssign - assigned);
|
|
}
|
|
}
|
|
|
|
// Assign AddClass accounts from highest position if needed
|
|
uint32 neededAddClassAccounts = sPlayerbotAIConfig.addClassAccountPoolSize;
|
|
|
|
if (existingAddClassAccounts < neededAddClassAccounts)
|
|
{
|
|
uint32 toAssign = neededAddClassAccounts - existingAddClassAccounts;
|
|
uint32 assigned = 0;
|
|
|
|
for (size_t idx = allRandomBotAccounts.size(); idx-- > 0 && assigned < toAssign;)
|
|
{
|
|
uint32 accountId = allRandomBotAccounts[idx];
|
|
if (currentAssignments[accountId] == 0) // Unassigned
|
|
{
|
|
PlayerbotsDatabase.Execute("UPDATE playerbots_account_type SET account_type = 2, assignment_date = NOW() WHERE account_id = {}", accountId);
|
|
currentAssignments[accountId] = 2;
|
|
assigned++;
|
|
}
|
|
}
|
|
|
|
if (assigned < toAssign)
|
|
{
|
|
LOG_ERROR("playerbots", "Not enough unassigned accounts to fulfill AddClass requirements. Need {} more accounts.", toAssign - assigned);
|
|
}
|
|
}
|
|
|
|
// Populate filtered account lists with ALL accounts of each type
|
|
for (auto const& [accountId, accountType] : currentAssignments)
|
|
{
|
|
if (accountType == 1) rndBotTypeAccounts.push_back(accountId);
|
|
else if (accountType == 2) addClassTypeAccounts.push_back(accountId);
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Account type assignment complete: {} RNDbot accounts, {} AddClass accounts, {} unassigned",
|
|
rndBotTypeAccounts.size(), addClassTypeAccounts.size(),
|
|
currentAssignments.size() - rndBotTypeAccounts.size() - addClassTypeAccounts.size());
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsAccountType(uint32 accountId, uint8 accountType)
|
|
{
|
|
QueryResult result = PlayerbotsDatabase.Query("SELECT 1 FROM playerbots_account_type WHERE account_id = {} AND account_type = {}", accountId, accountType);
|
|
return result != nullptr;
|
|
}
|
|
|
|
// Logs-in bots in 4 phases. Phase 1 logs Alliance bots up to how much is expected according to the faction ratio,
|
|
// and Phase 2 logs-in the remainder Horde bots to reach the total maxAllowedBotCount. If maxAllowedBotCount is not
|
|
// reached after Phase 2, the function goes back to log-in Alliance bots and reach maxAllowedBotCount. This is done
|
|
// because not every account is guaranteed 5A/5H bots, so the true ratio might be skewed by few percentages. Finally,
|
|
// Phase 4 is reached if and only if the value of RandomBotAccountCount is lower than it should.
|
|
uint32 RandomPlayerbotMgr::AddRandomBots()
|
|
{
|
|
uint32 maxAllowedBotCount = GetEventValue(0, "bot_count");
|
|
static time_t missingBotsTimer = 0;
|
|
|
|
if (currentBots.size() < maxAllowedBotCount)
|
|
{
|
|
// Calculate how many bots to add
|
|
maxAllowedBotCount -= currentBots.size();
|
|
maxAllowedBotCount = std::min(sPlayerbotAIConfig.randomBotsPerInterval, maxAllowedBotCount);
|
|
|
|
// Single RNG instance for all shuffling
|
|
std::mt19937 rng(std::chrono::steady_clock::now().time_since_epoch().count());
|
|
|
|
// Only need to track the Alliance count, as it's in Phase 1
|
|
uint32 totalRatio = sPlayerbotAIConfig.randomBotAllianceRatio + sPlayerbotAIConfig.randomBotHordeRatio;
|
|
uint32 allowedAllianceCount = maxAllowedBotCount * (sPlayerbotAIConfig.randomBotAllianceRatio) / totalRatio;
|
|
|
|
uint32 remainder = maxAllowedBotCount * (sPlayerbotAIConfig.randomBotAllianceRatio) % totalRatio;
|
|
|
|
// Fix #1082: Randomly add one based on reminder
|
|
if (remainder && urand(1, totalRatio) <= remainder)
|
|
{
|
|
allowedAllianceCount++;
|
|
}
|
|
|
|
// Determine which accounts to use based on EnablePeriodicOnlineOffline
|
|
std::vector<uint32> accountsToUse;
|
|
if (sPlayerbotAIConfig.enablePeriodicOnlineOffline)
|
|
{
|
|
|
|
// Calculate how many accounts can be used
|
|
// With enablePeriodicOnlineOffline, don't use all of rndBotTypeAccounts right away. Fraction results are rounded up
|
|
uint32 accountsToUseCount = (rndBotTypeAccounts.size() + sPlayerbotAIConfig.periodicOnlineOfflineRatio - 1)
|
|
/ sPlayerbotAIConfig.periodicOnlineOfflineRatio;
|
|
|
|
// Randomly select accounts
|
|
std::vector<uint32> shuffledAccounts = rndBotTypeAccounts;
|
|
std::shuffle(shuffledAccounts.begin(), shuffledAccounts.end(), rng);
|
|
|
|
for (uint32 i = 0; i < accountsToUseCount && i < shuffledAccounts.size(); i++)
|
|
{
|
|
accountsToUse.push_back(shuffledAccounts[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
accountsToUse = rndBotTypeAccounts;
|
|
}
|
|
|
|
// Pre-map all characters from selected accounts
|
|
struct CharacterInfo
|
|
{
|
|
uint32 guid;
|
|
uint8 rClass;
|
|
uint8 rRace;
|
|
uint32 accountId;
|
|
};
|
|
std::vector<CharacterInfo> allCharacters;
|
|
|
|
for (uint32 accountId : accountsToUse)
|
|
{
|
|
CharacterDatabasePreparedStatement* stmt =
|
|
CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID);
|
|
stmt->SetData(0, accountId);
|
|
PreparedQueryResult result = CharacterDatabase.Query(stmt);
|
|
if (!result)
|
|
continue;
|
|
|
|
do
|
|
{
|
|
Field* fields = result->Fetch();
|
|
CharacterInfo info;
|
|
info.guid = fields[0].Get<uint32>();
|
|
info.rClass = fields[1].Get<uint8>();
|
|
info.rRace = fields[2].Get<uint8>();
|
|
info.accountId = accountId;
|
|
allCharacters.push_back(info);
|
|
} while (result->NextRow());
|
|
}
|
|
|
|
// Shuffle for class balance
|
|
std::shuffle(allCharacters.begin(), allCharacters.end(), rng);
|
|
|
|
// Separate characters by faction for phased login
|
|
std::vector<CharacterInfo> allianceChars;
|
|
std::vector<CharacterInfo> hordeChars;
|
|
|
|
for (auto const& charInfo : allCharacters)
|
|
{
|
|
if (IsAlliance(charInfo.rRace))
|
|
allianceChars.push_back(charInfo);
|
|
|
|
else
|
|
hordeChars.push_back(charInfo);
|
|
}
|
|
|
|
// Lambda to handle bot login logic
|
|
auto tryLoginBot = [&](const CharacterInfo& charInfo) -> bool
|
|
{
|
|
if (GetEventValue(charInfo.guid, "add") ||
|
|
GetEventValue(charInfo.guid, "logout") ||
|
|
GetPlayerBot(charInfo.guid) ||
|
|
std::find(currentBots.begin(), currentBots.end(), charInfo.guid) != currentBots.end() ||
|
|
(sPlayerbotAIConfig.disableDeathKnightLogin && charInfo.rClass == CLASS_DEATH_KNIGHT))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint32 add_time = sPlayerbotAIConfig.enablePeriodicOnlineOffline
|
|
? urand(sPlayerbotAIConfig.minRandomBotInWorldTime,
|
|
sPlayerbotAIConfig.maxRandomBotInWorldTime)
|
|
: sPlayerbotAIConfig.permanentlyInWorldTime;
|
|
|
|
SetEventValue(charInfo.guid, "add", 1, add_time);
|
|
SetEventValue(charInfo.guid, "logout", 0, 0);
|
|
currentBots.push_back(charInfo.guid);
|
|
|
|
return true;
|
|
};
|
|
|
|
// PHASE 1: Log-in Alliance bots up to allowedAllianceCount
|
|
for (auto const& charInfo : allianceChars)
|
|
{
|
|
if (!allowedAllianceCount)
|
|
break;
|
|
|
|
if (tryLoginBot(charInfo))
|
|
{
|
|
maxAllowedBotCount--;
|
|
allowedAllianceCount--;
|
|
}
|
|
}
|
|
|
|
// PHASE 2: Log-in Horde bots up to maxAllowedBotCount
|
|
for (auto const& charInfo : hordeChars)
|
|
{
|
|
if (!maxAllowedBotCount)
|
|
break;
|
|
|
|
if (tryLoginBot(charInfo))
|
|
maxAllowedBotCount--;
|
|
}
|
|
|
|
// PHASE 3: If maxAllowedBotCount wasn't reached, log-in more Alliance bots
|
|
for (auto const& charInfo : allianceChars)
|
|
{
|
|
if (!maxAllowedBotCount)
|
|
break;
|
|
|
|
if (tryLoginBot(charInfo))
|
|
maxAllowedBotCount--;
|
|
}
|
|
|
|
// PHASE 4: An error is given if maxAllowedBotCount is still not reached
|
|
if (maxAllowedBotCount)
|
|
{
|
|
if (missingBotsTimer == 0)
|
|
missingBotsTimer = time(nullptr);
|
|
|
|
if (time(nullptr) - missingBotsTimer >= 10)
|
|
{
|
|
int divisor = RandomPlayerbotFactory::CalculateAvailableCharsPerAccount();
|
|
uint32 moreAccountsNeeded = (maxAllowedBotCount + divisor - 1) / divisor;
|
|
LOG_ERROR("playerbots",
|
|
"Can't log-in all the requested bots. Try increasing RandomBotAccountCount in your conf file.\n"
|
|
"{} more accounts needed.", moreAccountsNeeded);
|
|
missingBotsTimer = 0; // Reset timer so error is not spammed every tick
|
|
}
|
|
}
|
|
else
|
|
{
|
|
missingBotsTimer = 0; // Reset timer if logins for this interval were successful
|
|
}
|
|
}
|
|
else
|
|
{
|
|
missingBotsTimer = 0; // Reset timer if there's enough bots
|
|
}
|
|
|
|
return currentBots.size();
|
|
}
|
|
|
|
void RandomPlayerbotMgr::LoadBattleMastersCache()
|
|
{
|
|
BattleMastersCache.clear();
|
|
|
|
LOG_INFO("playerbots", "Loading Battlemasters Cache...");
|
|
|
|
QueryResult result = WorldDatabase.Query("SELECT `entry`,`bg_template` FROM `battlemaster_entry`");
|
|
|
|
uint32 count = 0;
|
|
|
|
if (!result)
|
|
{
|
|
return;
|
|
}
|
|
|
|
do
|
|
{
|
|
++count;
|
|
|
|
Field* fields = result->Fetch();
|
|
|
|
uint32 entry = fields[0].Get<uint32>();
|
|
uint32 bgTypeId = fields[1].Get<uint32>();
|
|
|
|
CreatureTemplate const* bmaster = sObjectMgr->GetCreatureTemplate(entry);
|
|
if (!bmaster)
|
|
continue;
|
|
|
|
FactionTemplateEntry const* bmFaction = sFactionTemplateStore.LookupEntry(bmaster->faction);
|
|
uint32 bmFactionId = bmFaction->faction;
|
|
FactionEntry const* bmParentFaction = sFactionStore.LookupEntry(bmFactionId);
|
|
uint32 bmParentTeam = bmParentFaction->team;
|
|
TeamId bmTeam = TEAM_NEUTRAL;
|
|
if (bmParentTeam == 891)
|
|
bmTeam = TEAM_ALLIANCE;
|
|
|
|
if (bmFactionId == 189)
|
|
bmTeam = TEAM_ALLIANCE;
|
|
|
|
if (bmParentTeam == 892)
|
|
bmTeam = TEAM_HORDE;
|
|
|
|
if (bmFactionId == 66)
|
|
bmTeam = TEAM_HORDE;
|
|
|
|
BattleMastersCache[bmTeam][BattlegroundTypeId(bgTypeId)].insert(
|
|
BattleMastersCache[bmTeam][BattlegroundTypeId(bgTypeId)].end(), entry);
|
|
LOG_DEBUG("playerbots", "Cached Battlemaster #{} for BG Type {} ({})", entry, bgTypeId,
|
|
bmTeam == TEAM_ALLIANCE ? "Alliance"
|
|
: bmTeam == TEAM_HORDE ? "Horde"
|
|
: "Neutral");
|
|
|
|
} while (result->NextRow());
|
|
|
|
LOG_INFO("playerbots", ">> Loaded {} battlemaster entries", count);
|
|
}
|
|
|
|
std::vector<uint32> parseBrackets(const std::string& str)
|
|
{
|
|
std::vector<uint32> brackets;
|
|
std::stringstream ss(str);
|
|
std::string item;
|
|
|
|
while (std::getline(ss, item, ','))
|
|
{
|
|
brackets.push_back(static_cast<uint32>(std::stoi(item)));
|
|
}
|
|
|
|
return brackets;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::CheckBgQueue()
|
|
{
|
|
if (!BgCheckTimer)
|
|
{
|
|
BgCheckTimer = time(nullptr);
|
|
return; // Exit immediately after initializing the timer
|
|
}
|
|
|
|
if (time(nullptr) < BgCheckTimer)
|
|
{
|
|
return; // No need to proceed if the current time is less than the timer
|
|
}
|
|
|
|
// Update the timer to the current time
|
|
BgCheckTimer = time(nullptr);
|
|
|
|
LOG_DEBUG("playerbots", "Checking BG Queue...");
|
|
|
|
// Initialize Battleground Data (do not clear here)
|
|
|
|
for (int bracket = BG_BRACKET_ID_FIRST; bracket < MAX_BATTLEGROUND_BRACKETS; ++bracket)
|
|
{
|
|
for (int queueType = BATTLEGROUND_QUEUE_AV; queueType < MAX_BATTLEGROUND_QUEUE_TYPES; ++queueType)
|
|
{
|
|
BattlegroundData[queueType][bracket] = BattlegroundInfo();
|
|
}
|
|
}
|
|
|
|
// Process real players and populate Battleground Data with player/queue count
|
|
// Opens a queue for bots to join
|
|
for (Player* player : players)
|
|
{
|
|
// Skip player if not currently in a queue
|
|
if (!player->InBattlegroundQueue())
|
|
continue;
|
|
|
|
Battleground* bg = player->GetBattleground();
|
|
if (bg && bg->GetStatus() == STATUS_WAIT_LEAVE)
|
|
continue;
|
|
|
|
TeamId teamId = player->GetTeamId();
|
|
|
|
for (uint8 queueType = 0; queueType < PLAYER_MAX_BATTLEGROUND_QUEUES; ++queueType)
|
|
{
|
|
BattlegroundQueueTypeId queueTypeId = player->GetBattlegroundQueueTypeId(queueType);
|
|
if (queueTypeId == BATTLEGROUND_QUEUE_NONE)
|
|
continue;
|
|
|
|
// Check if real player is able to create/join this queue
|
|
BattlegroundTypeId bgTypeId = sBattlegroundMgr->BGTemplateId(queueTypeId);
|
|
uint32 mapId = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId)->GetMapId();
|
|
PvPDifficultyEntry const* pvpDiff = GetBattlegroundBracketByLevel(mapId, player->GetLevel());
|
|
if (!pvpDiff)
|
|
continue;
|
|
|
|
// If player is allowed, populate the BattlegroundData with the appropriate level requirements
|
|
BattlegroundBracketId bracketId = pvpDiff->GetBracketId();
|
|
BattlegroundData[queueTypeId][bracketId].minLevel = pvpDiff->minLevel;
|
|
BattlegroundData[queueTypeId][bracketId].maxLevel = pvpDiff->maxLevel;
|
|
|
|
// Arena logic
|
|
bool isRated = false;
|
|
if (BattlegroundMgr::BGArenaType(queueTypeId))
|
|
{
|
|
BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(queueTypeId);
|
|
GroupQueueInfo ginfo;
|
|
|
|
if (bgQueue.GetPlayerGroupInfoData(player->GetGUID(), &ginfo))
|
|
{
|
|
isRated = ginfo.IsRated;
|
|
}
|
|
|
|
if (bgQueue.IsPlayerInvitedToRatedArena(player->GetGUID()) ||
|
|
(player->InArena() && player->GetBattleground()->isRated()))
|
|
isRated = true;
|
|
|
|
if (isRated)
|
|
BattlegroundData[queueTypeId][bracketId].ratedArenaPlayerCount++;
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].skirmishArenaPlayerCount++;
|
|
}
|
|
// BG Logic
|
|
else
|
|
{
|
|
if (teamId == TEAM_ALLIANCE)
|
|
BattlegroundData[queueTypeId][bracketId].bgAlliancePlayerCount++;
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].bgHordePlayerCount++;
|
|
|
|
// If a player has joined the BG, update the instance count in BattlegroundData (for consistency)
|
|
if (player->InBattleground())
|
|
{
|
|
std::vector<uint32>* instanceIds = nullptr;
|
|
uint32 instanceId = player->GetBattleground()->GetInstanceID();
|
|
|
|
instanceIds = &BattlegroundData[queueTypeId][bracketId].bgInstances;
|
|
if (instanceIds &&
|
|
std::find(instanceIds->begin(), instanceIds->end(), instanceId) == instanceIds->end())
|
|
instanceIds->push_back(instanceId);
|
|
|
|
BattlegroundData[queueTypeId][bracketId].bgInstanceCount = instanceIds->size();
|
|
}
|
|
}
|
|
|
|
if (!player->IsInvitedForBattlegroundInstance() && !player->InBattleground())
|
|
{
|
|
if (BattlegroundMgr::BGArenaType(queueTypeId))
|
|
{
|
|
if (isRated)
|
|
BattlegroundData[queueTypeId][bracketId].activeRatedArenaQueue = 1;
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].activeSkirmishArenaQueue = 1;
|
|
}
|
|
else
|
|
{
|
|
BattlegroundData[queueTypeId][bracketId].activeBgQueue = 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process player bots
|
|
for (auto& [guid, bot] : playerBots)
|
|
{
|
|
if (!bot || !bot->InBattlegroundQueue() || !bot->IsInWorld() || !IsRandomBot(bot))
|
|
continue;
|
|
|
|
Battleground* bg = bot->GetBattleground();
|
|
if (bg && bg->GetStatus() == STATUS_WAIT_LEAVE)
|
|
continue;
|
|
|
|
TeamId teamId = bot->GetTeamId();
|
|
|
|
for (uint8 queueType = 0; queueType < PLAYER_MAX_BATTLEGROUND_QUEUES; ++queueType)
|
|
{
|
|
BattlegroundQueueTypeId queueTypeId = bot->GetBattlegroundQueueTypeId(queueType);
|
|
if (queueTypeId == BATTLEGROUND_QUEUE_NONE)
|
|
continue;
|
|
|
|
BattlegroundTypeId bgTypeId = sBattlegroundMgr->BGTemplateId(queueTypeId);
|
|
uint32 mapId = sBattlegroundMgr->GetBattlegroundTemplate(bgTypeId)->GetMapId();
|
|
PvPDifficultyEntry const* pvpDiff = GetBattlegroundBracketByLevel(mapId, bot->GetLevel());
|
|
if (!pvpDiff)
|
|
continue;
|
|
|
|
BattlegroundBracketId bracketId = pvpDiff->GetBracketId();
|
|
BattlegroundData[queueTypeId][bracketId].minLevel = pvpDiff->minLevel;
|
|
BattlegroundData[queueTypeId][bracketId].maxLevel = pvpDiff->maxLevel;
|
|
|
|
if (BattlegroundMgr::BGArenaType(queueTypeId))
|
|
{
|
|
bool isRated = false;
|
|
BattlegroundQueue& bgQueue = sBattlegroundMgr->GetBattlegroundQueue(queueTypeId);
|
|
GroupQueueInfo ginfo;
|
|
|
|
if (bgQueue.GetPlayerGroupInfoData(guid, &ginfo))
|
|
{
|
|
isRated = ginfo.IsRated;
|
|
}
|
|
|
|
if (bgQueue.IsPlayerInvitedToRatedArena(guid) || (bot->InArena() && bot->GetBattleground()->isRated()))
|
|
isRated = true;
|
|
|
|
if (isRated)
|
|
BattlegroundData[queueTypeId][bracketId].ratedArenaBotCount++;
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].skirmishArenaBotCount++;
|
|
}
|
|
else
|
|
{
|
|
if (teamId == TEAM_ALLIANCE)
|
|
BattlegroundData[queueTypeId][bracketId].bgAllianceBotCount++;
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].bgHordeBotCount++;
|
|
}
|
|
|
|
if (bot->InBattleground())
|
|
{
|
|
std::vector<uint32>* instanceIds = nullptr;
|
|
uint32 instanceId = bot->GetBattleground()->GetInstanceID();
|
|
bool isArena = false;
|
|
bool isRated = false;
|
|
|
|
// Arena logic
|
|
if (bot->InArena())
|
|
{
|
|
isArena = true;
|
|
if (bot->GetBattleground()->isRated())
|
|
{
|
|
isRated = true;
|
|
instanceIds = &BattlegroundData[queueTypeId][bracketId].ratedArenaInstances;
|
|
}
|
|
else
|
|
{
|
|
instanceIds = &BattlegroundData[queueTypeId][bracketId].skirmishArenaInstances;
|
|
}
|
|
}
|
|
// BG Logic
|
|
else
|
|
{
|
|
instanceIds = &BattlegroundData[queueTypeId][bracketId].bgInstances;
|
|
}
|
|
|
|
if (instanceIds &&
|
|
std::find(instanceIds->begin(), instanceIds->end(), instanceId) == instanceIds->end())
|
|
instanceIds->push_back(instanceId);
|
|
|
|
if (isArena)
|
|
{
|
|
if (isRated)
|
|
BattlegroundData[queueTypeId][bracketId].ratedArenaInstanceCount = instanceIds->size();
|
|
else
|
|
BattlegroundData[queueTypeId][bracketId].skirmishArenaInstanceCount = instanceIds->size();
|
|
}
|
|
else
|
|
{
|
|
BattlegroundData[queueTypeId][bracketId].bgInstanceCount = instanceIds->size();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If enabled, wait for all bots to have logged in before queueing for Arena's / BG's
|
|
if (sPlayerbotAIConfig.randomBotAutoJoinBG && playerBots.size() >= GetMaxAllowedBotCount())
|
|
{
|
|
uint32 randomBotAutoJoinArenaBracket = sPlayerbotAIConfig.randomBotAutoJoinArenaBracket;
|
|
uint32 randomBotAutoJoinBGRatedArena2v2Count = sPlayerbotAIConfig.randomBotAutoJoinBGRatedArena2v2Count;
|
|
uint32 randomBotAutoJoinBGRatedArena3v3Count = sPlayerbotAIConfig.randomBotAutoJoinBGRatedArena3v3Count;
|
|
uint32 randomBotAutoJoinBGRatedArena5v5Count = sPlayerbotAIConfig.randomBotAutoJoinBGRatedArena5v5Count;
|
|
|
|
uint32 randomBotAutoJoinBGICCount = sPlayerbotAIConfig.randomBotAutoJoinBGICCount;
|
|
uint32 randomBotAutoJoinBGEYCount = sPlayerbotAIConfig.randomBotAutoJoinBGEYCount;
|
|
uint32 randomBotAutoJoinBGAVCount = sPlayerbotAIConfig.randomBotAutoJoinBGAVCount;
|
|
uint32 randomBotAutoJoinBGABCount = sPlayerbotAIConfig.randomBotAutoJoinBGABCount;
|
|
uint32 randomBotAutoJoinBGWSCount = sPlayerbotAIConfig.randomBotAutoJoinBGWSCount;
|
|
|
|
std::vector<uint32> icBrackets = parseBrackets(sPlayerbotAIConfig.randomBotAutoJoinICBrackets);
|
|
std::vector<uint32> eyBrackets = parseBrackets(sPlayerbotAIConfig.randomBotAutoJoinEYBrackets);
|
|
std::vector<uint32> avBrackets = parseBrackets(sPlayerbotAIConfig.randomBotAutoJoinAVBrackets);
|
|
std::vector<uint32> abBrackets = parseBrackets(sPlayerbotAIConfig.randomBotAutoJoinABBrackets);
|
|
std::vector<uint32> wsBrackets = parseBrackets(sPlayerbotAIConfig.randomBotAutoJoinWSBrackets);
|
|
|
|
// Check both bgInstanceCount / bgInstances.size
|
|
// to help counter against potentional inconsistencies
|
|
auto updateRatedArenaInstanceCount = [&](uint32 queueType, uint32 bracket, uint32 minCount)
|
|
{
|
|
if (BattlegroundData[queueType][bracket].activeRatedArenaQueue == 0 &&
|
|
BattlegroundData[queueType][bracket].ratedArenaInstanceCount < minCount &&
|
|
BattlegroundData[queueType][bracket].ratedArenaInstances.size() < minCount)
|
|
BattlegroundData[queueType][bracket].activeRatedArenaQueue = 1;
|
|
};
|
|
|
|
auto updateBGInstanceCount = [&](uint32 queueType, std::vector<uint32> brackets, uint32 minCount)
|
|
{
|
|
for (uint32 bracket : brackets)
|
|
{
|
|
if (BattlegroundData[queueType][bracket].activeBgQueue == 0 &&
|
|
BattlegroundData[queueType][bracket].bgInstanceCount < minCount &&
|
|
BattlegroundData[queueType][bracket].bgInstances.size() < minCount)
|
|
BattlegroundData[queueType][bracket].activeBgQueue = 1;
|
|
}
|
|
};
|
|
|
|
// Update rated arena instance counts
|
|
updateRatedArenaInstanceCount(BATTLEGROUND_QUEUE_2v2, randomBotAutoJoinArenaBracket,
|
|
randomBotAutoJoinBGRatedArena2v2Count);
|
|
updateRatedArenaInstanceCount(BATTLEGROUND_QUEUE_3v3, randomBotAutoJoinArenaBracket,
|
|
randomBotAutoJoinBGRatedArena3v3Count);
|
|
updateRatedArenaInstanceCount(BATTLEGROUND_QUEUE_5v5, randomBotAutoJoinArenaBracket,
|
|
randomBotAutoJoinBGRatedArena5v5Count);
|
|
|
|
// Update battleground instance counts
|
|
updateBGInstanceCount(BATTLEGROUND_QUEUE_IC, icBrackets, randomBotAutoJoinBGICCount);
|
|
updateBGInstanceCount(BATTLEGROUND_QUEUE_EY, eyBrackets, randomBotAutoJoinBGEYCount);
|
|
updateBGInstanceCount(BATTLEGROUND_QUEUE_AV, avBrackets, randomBotAutoJoinBGAVCount);
|
|
updateBGInstanceCount(BATTLEGROUND_QUEUE_AB, abBrackets, randomBotAutoJoinBGABCount);
|
|
updateBGInstanceCount(BATTLEGROUND_QUEUE_WS, wsBrackets, randomBotAutoJoinBGWSCount);
|
|
}
|
|
|
|
LogBattlegroundInfo();
|
|
}
|
|
|
|
void RandomPlayerbotMgr::LogBattlegroundInfo()
|
|
{
|
|
for (auto const& queueTypePair : BattlegroundData)
|
|
{
|
|
uint8 queueType = queueTypePair.first;
|
|
|
|
BattlegroundQueueTypeId queueTypeId = BattlegroundQueueTypeId(queueType);
|
|
|
|
if (uint8 type = BattlegroundMgr::BGArenaType(queueTypeId))
|
|
{
|
|
for (auto const& bracketIdPair : queueTypePair.second)
|
|
{
|
|
auto& bgInfo = bracketIdPair.second;
|
|
if (bgInfo.minLevel == 0)
|
|
continue;
|
|
LOG_INFO("playerbots",
|
|
"ARENA:{} {}: Player (Skirmish:{}, Rated:{}) Bots (Skirmish:{}, Rated:{}) Total (Skirmish:{} "
|
|
"Rated:{}), Instances (Skirmish:{} Rated:{})",
|
|
type == ARENA_TYPE_2v2 ? "2v2"
|
|
: type == ARENA_TYPE_3v3 ? "3v3"
|
|
: "5v5",
|
|
std::to_string(bgInfo.minLevel) + "-" + std::to_string(bgInfo.maxLevel),
|
|
bgInfo.skirmishArenaPlayerCount, bgInfo.ratedArenaPlayerCount, bgInfo.skirmishArenaBotCount,
|
|
bgInfo.ratedArenaBotCount, bgInfo.skirmishArenaPlayerCount + bgInfo.skirmishArenaBotCount,
|
|
bgInfo.ratedArenaPlayerCount + bgInfo.ratedArenaBotCount, bgInfo.skirmishArenaInstanceCount,
|
|
bgInfo.ratedArenaInstanceCount);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
BattlegroundTypeId bgTypeId = BattlegroundMgr::BGTemplateId(queueTypeId);
|
|
std::string _bgType;
|
|
switch (bgTypeId)
|
|
{
|
|
case BATTLEGROUND_AV:
|
|
_bgType = "AV";
|
|
break;
|
|
case BATTLEGROUND_WS:
|
|
_bgType = "WSG";
|
|
break;
|
|
case BATTLEGROUND_AB:
|
|
_bgType = "AB";
|
|
break;
|
|
case BATTLEGROUND_EY:
|
|
_bgType = "EotS";
|
|
break;
|
|
case BATTLEGROUND_RB:
|
|
_bgType = "Random";
|
|
break;
|
|
case BATTLEGROUND_SA:
|
|
_bgType = "SotA";
|
|
break;
|
|
case BATTLEGROUND_IC:
|
|
_bgType = "IoC";
|
|
break;
|
|
default:
|
|
_bgType = "Other";
|
|
break;
|
|
}
|
|
|
|
for (auto const& bracketIdPair : queueTypePair.second)
|
|
{
|
|
auto& bgInfo = bracketIdPair.second;
|
|
if (bgInfo.minLevel == 0)
|
|
continue;
|
|
|
|
LOG_INFO("playerbots",
|
|
"BG:{} {}: Player ({}:{}) Bot ({}:{}) Total (A:{} H:{}), Instances {}, Active Queue: {}", _bgType,
|
|
std::to_string(bgInfo.minLevel) + "-" + std::to_string(bgInfo.maxLevel),
|
|
bgInfo.bgAlliancePlayerCount, bgInfo.bgHordePlayerCount, bgInfo.bgAllianceBotCount,
|
|
bgInfo.bgHordeBotCount, bgInfo.bgAlliancePlayerCount + bgInfo.bgAllianceBotCount,
|
|
bgInfo.bgHordePlayerCount + bgInfo.bgHordeBotCount, bgInfo.bgInstanceCount, bgInfo.activeBgQueue);
|
|
}
|
|
}
|
|
LOG_DEBUG("playerbots", "BG Queue check finished");
|
|
}
|
|
|
|
void RandomPlayerbotMgr::CheckLfgQueue()
|
|
{
|
|
if (!LfgCheckTimer || time(nullptr) > (LfgCheckTimer + 30))
|
|
LfgCheckTimer = time(nullptr);
|
|
|
|
LOG_DEBUG("playerbots", "Checking LFG Queue...");
|
|
|
|
// Clear LFG list
|
|
LfgDungeons[TEAM_ALLIANCE].clear();
|
|
LfgDungeons[TEAM_HORDE].clear();
|
|
|
|
for (std::vector<Player*>::iterator i = players.begin(); i != players.end(); ++i)
|
|
{
|
|
Player* player = *i;
|
|
if (!player || !player->IsInWorld())
|
|
continue;
|
|
|
|
Group* group = player->GetGroup();
|
|
ObjectGuid guid = group ? group->GetGUID() : player->GetGUID();
|
|
|
|
lfg::LfgState gState = sLFGMgr->GetState(guid);
|
|
if (gState != lfg::LFG_STATE_NONE && gState < lfg::LFG_STATE_DUNGEON)
|
|
{
|
|
lfg::LfgDungeonSet const& dList = sLFGMgr->GetSelectedDungeons(player->GetGUID());
|
|
for (lfg::LfgDungeonSet::const_iterator itr = dList.begin(); itr != dList.end(); ++itr)
|
|
{
|
|
lfg::LFGDungeonData const* dungeon = sLFGMgr->GetLFGDungeon(*itr);
|
|
if (!dungeon)
|
|
continue;
|
|
|
|
LfgDungeons[player->GetTeamId()].push_back(dungeon->id);
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG_DEBUG("playerbots", "LFG Queue check finished");
|
|
}
|
|
|
|
void RandomPlayerbotMgr::CheckPlayers()
|
|
{
|
|
if (!PlayersCheckTimer || time(nullptr) > (PlayersCheckTimer + 60))
|
|
PlayersCheckTimer = time(nullptr);
|
|
|
|
LOG_INFO("playerbots", "Checking Players...");
|
|
|
|
if (!playersLevel)
|
|
playersLevel = sPlayerbotAIConfig.randombotStartingLevel;
|
|
|
|
for (std::vector<Player*>::iterator i = players.begin(); i != players.end(); ++i)
|
|
{
|
|
Player* player = *i;
|
|
|
|
if (player->IsGameMaster())
|
|
continue;
|
|
|
|
// if (player->GetSession()->GetSecurity() > SEC_PLAYER)
|
|
// continue;
|
|
|
|
if (player->GetLevel() > playersLevel)
|
|
playersLevel = player->GetLevel() + 3;
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Max player level is {}, max bot level set to {}", playersLevel - 3, playersLevel);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::ScheduleRandomize(uint32 bot, uint32 time) { SetEventValue(bot, "randomize", 1, time); }
|
|
|
|
void RandomPlayerbotMgr::ScheduleTeleport(uint32 bot, uint32 time)
|
|
{
|
|
if (!time)
|
|
time = 60 + urand(sPlayerbotAIConfig.randomBotUpdateInterval, sPlayerbotAIConfig.randomBotUpdateInterval * 3);
|
|
|
|
SetEventValue(bot, "teleport", 1, time);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::ScheduleChangeStrategy(uint32 bot, uint32 time)
|
|
{
|
|
if (!time)
|
|
time = urand(sPlayerbotAIConfig.minRandomBotChangeStrategyTime,
|
|
sPlayerbotAIConfig.maxRandomBotChangeStrategyTime);
|
|
|
|
SetEventValue(bot, "change_strategy", 1, time);
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::ProcessBot(uint32 bot)
|
|
{
|
|
ObjectGuid botGUID = ObjectGuid::Create<HighGuid::Player>(bot);
|
|
Player* player = GetPlayerBot(botGUID);
|
|
PlayerbotAI* botAI = player ? GET_PLAYERBOT_AI(player) : nullptr;
|
|
|
|
uint32 isValid = GetEventValue(bot, "add");
|
|
if (!isValid)
|
|
{
|
|
if (!player || !player->GetGroup())
|
|
{
|
|
if (player)
|
|
LOG_DEBUG("playerbots", "Bot #{} {}:{} <{}>: log out", bot, IsAlliance(player->getRace()) ? "A" : "H",
|
|
player->GetLevel(), player->GetName().c_str());
|
|
else
|
|
LOG_DEBUG("playerbots", "Bot #{}: log out", bot);
|
|
|
|
SetEventValue(bot, "add", 0, 0);
|
|
currentBots.remove(bot);
|
|
|
|
if (player)
|
|
LogoutPlayerBot(botGUID);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32 randomTime;
|
|
if (!player)
|
|
{
|
|
AddPlayerBot(botGUID, 0);
|
|
randomTime = urand(1, 2);
|
|
|
|
uint32 randomBotUpdateInterval = _isBotInitializing ? 1 : sPlayerbotAIConfig.randomBotUpdateInterval;
|
|
randomTime = urand(std::max(5, static_cast<int>(randomBotUpdateInterval * 0.5)),
|
|
std::max(12, static_cast<int>(randomBotUpdateInterval * 2)));
|
|
SetEventValue(bot, "update", 1, randomTime);
|
|
|
|
// do not randomize or teleport immediately after server start (prevent lagging)
|
|
if (!GetEventValue(bot, "randomize"))
|
|
{
|
|
randomTime = urand(3, std::max(4, static_cast<int>(randomBotUpdateInterval * 0.4)));
|
|
ScheduleRandomize(bot, randomTime);
|
|
}
|
|
if (!GetEventValue(bot, "teleport"))
|
|
{
|
|
randomTime = urand(std::max(7, static_cast<int>(randomBotUpdateInterval * 0.7)),
|
|
std::max(14, static_cast<int>(randomBotUpdateInterval * 1.4)));
|
|
ScheduleTeleport(bot, randomTime);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (!player->IsInWorld())
|
|
return false;
|
|
|
|
if (player->GetGroup() || player->HasUnitState(UNIT_STATE_IN_FLIGHT))
|
|
return false;
|
|
|
|
uint32 update = GetEventValue(bot, "update");
|
|
if (!update)
|
|
{
|
|
if (botAI)
|
|
botAI->GetAiObjectContext()->GetValue<bool>("random bot update")->Set(true);
|
|
|
|
bool update = true;
|
|
if (botAI)
|
|
{
|
|
// botAI->GetAiObjectContext()->GetValue<bool>("random bot update")->Set(true);
|
|
if (!sRandomPlayerbotMgr.IsRandomBot(player))
|
|
update = false;
|
|
|
|
if (player->GetGroup() && botAI->GetGroupLeader())
|
|
{
|
|
PlayerbotAI* groupLeaderBotAI = GET_PLAYERBOT_AI(botAI->GetGroupLeader());
|
|
if (!groupLeaderBotAI || groupLeaderBotAI->IsRealPlayer())
|
|
{
|
|
update = false;
|
|
}
|
|
}
|
|
|
|
// if (botAI->HasPlayerNearby(sPlayerbotAIConfig.grindDistance))
|
|
// update = false;
|
|
}
|
|
|
|
if (update)
|
|
ProcessBot(player);
|
|
|
|
randomTime = urand(sPlayerbotAIConfig.minRandomBotReviveTime, sPlayerbotAIConfig.maxRandomBotReviveTime);
|
|
SetEventValue(bot, "update", 1, randomTime);
|
|
|
|
return true;
|
|
}
|
|
|
|
uint32 logout = GetEventValue(bot, "logout");
|
|
if (player && !logout && !isValid)
|
|
{
|
|
LOG_DEBUG("playerbots", "Bot #{} {}:{} <{}>: log out", bot, IsAlliance(player->getRace()) ? "A" : "H",
|
|
player->GetLevel(), player->GetName().c_str());
|
|
LogoutPlayerBot(botGUID);
|
|
currentBots.remove(bot);
|
|
SetEventValue(bot, "logout", 1,
|
|
urand(sPlayerbotAIConfig.minRandomBotInWorldTime, sPlayerbotAIConfig.maxRandomBotInWorldTime));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::ProcessBot(Player* bot)
|
|
{
|
|
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
return false;
|
|
|
|
if (bot->InBattleground())
|
|
return false;
|
|
|
|
if (bot->InBattlegroundQueue())
|
|
return false;
|
|
|
|
uint32 botId = bot->GetGUID().GetCounter();
|
|
|
|
// if death revive
|
|
if (bot->isDead())
|
|
{
|
|
if (!GetEventValue(botId, "dead"))
|
|
{
|
|
uint32 randomTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotReviveTime, sPlayerbotAIConfig.maxRandomBotReviveTime);
|
|
LOG_DEBUG("playerbots", "Mark bot {} as dead, will be revived in {}s.", bot->GetName().c_str(),
|
|
randomTime);
|
|
SetEventValue(botId, "dead", 1, sPlayerbotAIConfig.maxRandomBotInWorldTime);
|
|
SetEventValue(botId, "revive", 1, randomTime);
|
|
return false;
|
|
}
|
|
|
|
if (!GetEventValue(botId, "revive"))
|
|
{
|
|
Revive(bot);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// leave group if leader is rndbot
|
|
Group* group = bot->GetGroup();
|
|
if (group && !group->isLFGGroup() && IsRandomBot(group->GetLeader()))
|
|
{
|
|
botAI->LeaveOrDisbandGroup();
|
|
LOG_INFO("playerbots", "Bot {} remove from group since leader is random bot.", bot->GetName().c_str());
|
|
}
|
|
|
|
// only randomize and teleport idle bots
|
|
bool idleBot = false;
|
|
if (TravelTarget* target = botAI->GetAiObjectContext()->GetValue<TravelTarget*>("travel target")->Get())
|
|
{
|
|
if (target->getTravelState() == TravelState::TRAVEL_STATE_IDLE)
|
|
{
|
|
idleBot = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
idleBot = true;
|
|
}
|
|
|
|
if (idleBot)
|
|
{
|
|
// randomize
|
|
uint32 randomize = GetEventValue(botId, "randomize");
|
|
if (!randomize)
|
|
{
|
|
// bool randomiser = true;
|
|
// if (player->GetGuildId())
|
|
// {
|
|
// if (Guild* guild = sGuildMgr->GetGuildById(player->GetGuildId()))
|
|
// {
|
|
// if (guild->GetLeaderGUID() == player->GetGUID())
|
|
// {
|
|
// for (std::vector<Player*>::iterator i = players.begin(); i != players.end(); ++i)
|
|
// GuildTaskMgr::instance().Update(*i, player);
|
|
// }
|
|
|
|
// uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guild->GetLeaderGUID());
|
|
// if (!sPlayerbotAIConfig.IsInRandomAccountList(accountId))
|
|
// {
|
|
// uint8 rank = player->GetRank();
|
|
// randomiser = rank < 4 ? false : true;
|
|
// }
|
|
// }
|
|
// }
|
|
// if (randomiser)
|
|
// {
|
|
Randomize(bot);
|
|
LOG_DEBUG("playerbots", "Bot #{} {}:{} <{}>: randomized", botId,
|
|
bot->GetTeamId() == TEAM_ALLIANCE ? "A" : "H", bot->GetLevel(), bot->GetName());
|
|
uint32 randomTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotRandomizeTime, sPlayerbotAIConfig.maxRandomBotRandomizeTime);
|
|
ScheduleRandomize(botId, randomTime);
|
|
return true;
|
|
}
|
|
|
|
// uint32 changeStrategy = GetEventValue(bot, "change_strategy");
|
|
// if (!changeStrategy)
|
|
// {
|
|
// LOG_INFO("playerbots", "Changing strategy for bot #{} <{}>", bot, player->GetName().c_str());
|
|
// ChangeStrategy(player);
|
|
// return true;
|
|
// }
|
|
|
|
uint32 teleport = GetEventValue(botId, "teleport");
|
|
if (!teleport)
|
|
{
|
|
LOG_DEBUG("playerbots", "Bot #{} <{}>: teleport for level and refresh", botId, bot->GetName());
|
|
Refresh(bot);
|
|
RandomTeleportForLevel(bot);
|
|
uint32 time = urand(sPlayerbotAIConfig.minRandomBotTeleportInterval,
|
|
sPlayerbotAIConfig.maxRandomBotTeleportInterval);
|
|
ScheduleTeleport(botId, time);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Revive(Player* player)
|
|
{
|
|
uint32 bot = player->GetGUID().GetCounter();
|
|
|
|
// LOG_INFO("playerbots", "Bot {} revived", player->GetName().c_str());
|
|
SetEventValue(bot, "dead", 0, 0);
|
|
SetEventValue(bot, "revive", 0, 0);
|
|
|
|
Refresh(player);
|
|
RandomTeleportGrindForLevel(player);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomTeleport(Player* bot, std::vector<WorldLocation>& locs, bool hearth)
|
|
{
|
|
// ignore when alrdy teleported or not in the world yet.
|
|
if (bot->IsBeingTeleported() || !bot->IsInWorld())
|
|
return;
|
|
|
|
// no teleport / movement update when rooted.
|
|
if (bot->IsRooted())
|
|
return;
|
|
|
|
// ignore when in queue for battle grounds.
|
|
if (bot->InBattlegroundQueue())
|
|
return;
|
|
|
|
// ignore when in battle grounds or arena.
|
|
if (bot->InBattleground() || bot->InArena())
|
|
return;
|
|
|
|
// ignore when in group (e.g. world, dungeons, raids) and leader is not a player.
|
|
if (bot->GetGroup() && !bot->GetGroup()->IsLeader(bot->GetGUID()))
|
|
return;
|
|
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (botAI)
|
|
{
|
|
// ignore when in when taxi with boat/zeppelin and has players nearby
|
|
if (bot->HasUnitMovementFlag(MOVEMENTFLAG_ONTRANSPORT) && bot->HasUnitState(UNIT_STATE_IGNORE_PATHFINDING) &&
|
|
botAI->HasPlayerNearby())
|
|
return;
|
|
}
|
|
|
|
// if (sPlayerbotAIConfig.randomBotRpgChance < 0)
|
|
// return;
|
|
|
|
if (locs.empty())
|
|
{
|
|
LOG_DEBUG("playerbots", "Cannot teleport bot {} - no locations available", bot->GetName().c_str());
|
|
return;
|
|
}
|
|
|
|
std::vector<WorldPosition> tlocs;
|
|
for (auto& loc : locs)
|
|
tlocs.push_back(WorldPosition(loc));
|
|
// Do not teleport to maps disabled in config
|
|
tlocs.erase(std::remove_if(tlocs.begin(), tlocs.end(),
|
|
[bot](WorldPosition l)
|
|
{
|
|
std::vector<uint32>::iterator i =
|
|
find(sPlayerbotAIConfig.randomBotMaps.begin(),
|
|
sPlayerbotAIConfig.randomBotMaps.end(), l.GetMapId());
|
|
return i == sPlayerbotAIConfig.randomBotMaps.end();
|
|
}),
|
|
tlocs.end());
|
|
if (tlocs.empty())
|
|
{
|
|
LOG_DEBUG("playerbots", "Cannot teleport bot {} - all locations removed by filter", bot->GetName().c_str());
|
|
return;
|
|
}
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "RandomTeleportByLocations");
|
|
|
|
std::shuffle(std::begin(tlocs), std::end(tlocs), RandomEngine::Instance());
|
|
for (uint32 i = 0; i < tlocs.size(); i++)
|
|
{
|
|
WorldLocation loc = tlocs[i];
|
|
|
|
float x = loc.GetPositionX(); // + (attemtps > 0 ? urand(0, sPlayerbotAIConfig.grindDistance) -
|
|
// sPlayerbotAIConfig.grindDistance / 2 : 0);
|
|
float y = loc.GetPositionY(); // + (attemtps > 0 ? urand(0, sPlayerbotAIConfig.grindDistance) -
|
|
// sPlayerbotAIConfig.grindDistance / 2 : 0);
|
|
float z = loc.GetPositionZ();
|
|
|
|
Map* map = sMapMgr->FindMap(loc.GetMapId(), 0);
|
|
if (!map)
|
|
continue;
|
|
|
|
AreaTableEntry const* zone = sAreaTableStore.LookupEntry(map->GetZoneId(bot->GetPhaseMask(), x, y, z));
|
|
if (!zone)
|
|
continue;
|
|
|
|
AreaTableEntry const* area = sAreaTableStore.LookupEntry(map->GetAreaId(bot->GetPhaseMask(), x, y, z));
|
|
if (!area)
|
|
continue;
|
|
|
|
// Do not teleport to enemy zones if level is low
|
|
if (zone->team == 4 && bot->GetTeamId() == TEAM_ALLIANCE)
|
|
continue;
|
|
|
|
if (zone->team == 2 && bot->GetTeamId() == TEAM_HORDE)
|
|
continue;
|
|
|
|
if (map->IsInWater(bot->GetPhaseMask(), x, y, z, bot->GetCollisionHeight()))
|
|
continue;
|
|
|
|
float ground = map->GetHeight(bot->GetPhaseMask(), x, y, z + 0.5f);
|
|
if (ground <= INVALID_HEIGHT)
|
|
continue;
|
|
|
|
z = 0.05f + ground;
|
|
|
|
if (!botAI->StarterLevelDistanceCheck(bot, loc, true))
|
|
continue;
|
|
|
|
const LocaleConstant& locale = sWorld->GetDefaultDbcLocale();
|
|
LOG_DEBUG("playerbots",
|
|
"Random teleporting bot {} (level {}) to Map: {} ({}) Zone: {} ({}) Area: {} ({}) ZoneLevel: {} "
|
|
"AreaLevel: {} {},{},{} ({}/{} "
|
|
"locations)",
|
|
bot->GetName().c_str(), bot->GetLevel(), map->GetId(), map->GetMapName(), zone->ID,
|
|
zone->area_name[locale], area->ID, area->area_name[locale], zone->area_level, area->area_level, x, y,
|
|
z, i + 1, tlocs.size());
|
|
|
|
if (hearth)
|
|
{
|
|
bot->SetHomebind(loc, zone->ID);
|
|
}
|
|
|
|
// Prevent blink to be detected by visible real players
|
|
if (botAI->HasPlayerNearby(150.0f))
|
|
{
|
|
break;
|
|
}
|
|
|
|
bot->GetMotionMaster()->Clear();
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (botAI)
|
|
botAI->Reset(true);
|
|
bot->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
|
bot->TeleportTo(loc.GetMapId(), x, y, z, 0);
|
|
bot->SendMovementFlagUpdate();
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
|
|
return;
|
|
}
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
|
|
// LOG_ERROR("playerbots", "Cannot teleport bot {} - no locations available ({} locations)", bot->GetName().c_str(),
|
|
// tlocs.size());
|
|
}
|
|
|
|
void RandomPlayerbotMgr::PrepareAddclassCache()
|
|
{
|
|
// Using accounts marked as type 2 (AddClass)
|
|
int32 collected = 0;
|
|
|
|
for (uint32 accountId : addClassTypeAccounts)
|
|
{
|
|
for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++)
|
|
{
|
|
if (claz == 10)
|
|
continue;
|
|
|
|
QueryResult results = CharacterDatabase.Query(
|
|
"SELECT guid, race FROM characters "
|
|
"WHERE account = {} AND class = '{}' AND online = 0",
|
|
accountId, claz);
|
|
|
|
if (results)
|
|
{
|
|
do
|
|
{
|
|
Field* fields = results->Fetch();
|
|
ObjectGuid guid = ObjectGuid(HighGuid::Player, fields[0].Get<uint32>());
|
|
uint32 race = fields[1].Get<uint32>();
|
|
bool isAlliance = race == 1 || race == 3 || race == 4 || race == 7 || race == 11;
|
|
addclassCache[GetTeamClassIdx(isAlliance, claz)].insert(guid);
|
|
collected++;
|
|
} while (results->NextRow());
|
|
}
|
|
}
|
|
}
|
|
|
|
LOG_INFO("playerbots", ">> {} characters collected for addclass command from {} AddClass accounts.", collected, addClassTypeAccounts.size());
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Init()
|
|
{
|
|
if (sPlayerbotAIConfig.addClassCommand)
|
|
sRandomPlayerbotMgr.PrepareAddclassCache();
|
|
|
|
if (sPlayerbotAIConfig.randomBotJoinBG)
|
|
sRandomPlayerbotMgr.LoadBattleMastersCache();
|
|
|
|
PlayerbotsDatabase.Execute("DELETE FROM playerbots_random_bots WHERE event = 'add'");
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomTeleportForLevel(Player* bot)
|
|
{
|
|
if (bot->InBattleground())
|
|
return;
|
|
|
|
if (bot->GetLevel() >= 10 && urand(0, 100) < sPlayerbotAIConfig.probTeleToBankers * 100)
|
|
{
|
|
std::vector<WorldLocation> locs = sTravelMgr.GetCityLocations(bot);
|
|
if (!locs.empty())
|
|
{
|
|
RandomTeleport(bot, locs, true);
|
|
return;
|
|
}
|
|
}
|
|
std::vector<WorldLocation> locs = sTravelMgr.GetTeleportLocations(bot);
|
|
if (!locs.empty())
|
|
{
|
|
RandomTeleport(bot, locs, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomTeleportGrindForLevel(Player* bot)
|
|
{
|
|
if (bot->InBattleground())
|
|
return;
|
|
|
|
std::vector<WorldLocation> locs = sTravelMgr.GetTeleportLocations(bot);
|
|
LOG_DEBUG("playerbots", "Random teleporting bot {} for level {} ({} locations available)", bot->GetName().c_str(),
|
|
bot->GetLevel(), locs.size());
|
|
|
|
RandomTeleport(bot, locs);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomTeleport(Player* bot)
|
|
{
|
|
if (bot->InBattleground())
|
|
return;
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "RandomTeleport");
|
|
std::vector<WorldLocation> locs;
|
|
|
|
std::list<Unit*> targets;
|
|
float range = sPlayerbotAIConfig.randomBotTeleportDistance;
|
|
Acore::AnyUnitInObjectRangeCheck u_check(bot, range);
|
|
Acore::UnitListSearcher<Acore::AnyUnitInObjectRangeCheck> searcher(bot, targets, u_check);
|
|
Cell::VisitObjects(bot, searcher, range);
|
|
|
|
if (!targets.empty())
|
|
{
|
|
for (Unit* unit : targets)
|
|
{
|
|
bot->UpdatePosition(*unit);
|
|
FleeManager manager(bot, sPlayerbotAIConfig.sightDistance, 0, true);
|
|
float rx, ry, rz;
|
|
if (manager.CalculateDestination(&rx, &ry, &rz))
|
|
{
|
|
WorldLocation loc(bot->GetMapId(), rx, ry, rz);
|
|
locs.push_back(loc);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RandomTeleportForLevel(bot);
|
|
}
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
|
|
Refresh(bot);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Randomize(Player* bot)
|
|
{
|
|
if (bot->InBattleground())
|
|
return;
|
|
|
|
if (bot->GetLevel() < 3 || (bot->GetLevel() < 56 && bot->getClass() == CLASS_DEATH_KNIGHT))
|
|
{
|
|
RandomizeFirst(bot);
|
|
}
|
|
else if (bot->GetLevel() < sPlayerbotAIConfig.randomBotMaxLevel || !sPlayerbotAIConfig.downgradeMaxLevelBot)
|
|
{
|
|
uint8 level = bot->GetLevel();
|
|
PlayerbotFactory factory(bot, level);
|
|
factory.Randomize(true);
|
|
// IncreaseLevel(bot);
|
|
}
|
|
else
|
|
{
|
|
RandomizeFirst(bot);
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::IncreaseLevel(Player* bot)
|
|
{
|
|
uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
|
|
if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
|
|
maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "IncreaseLevel");
|
|
uint32 lastLevel = GetValue(bot, "level");
|
|
uint8 level = bot->GetLevel() + 1;
|
|
if (level > maxLevel)
|
|
{
|
|
level = maxLevel;
|
|
}
|
|
if (lastLevel != level)
|
|
{
|
|
PlayerbotFactory factory(bot, level);
|
|
factory.Randomize(true);
|
|
}
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomizeFirst(Player* bot)
|
|
{
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
return;
|
|
|
|
uint32 maxLevel = sPlayerbotAIConfig.randomBotMaxLevel;
|
|
if (maxLevel > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL))
|
|
maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
|
|
|
|
// if lvl sync is enabled, max level is limited by online players lvl
|
|
if (sPlayerbotAIConfig.syncLevelWithPlayers)
|
|
maxLevel = std::max(sPlayerbotAIConfig.randomBotMinLevel,
|
|
std::min(playersLevel, sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)));
|
|
|
|
uint32 minLevel = sPlayerbotAIConfig.randomBotMinLevel;
|
|
if (bot->getClass() == CLASS_DEATH_KNIGHT)
|
|
{
|
|
maxLevel = std::max(maxLevel, sWorld->getIntConfig(CONFIG_START_HEROIC_PLAYER_LEVEL));
|
|
minLevel = std::max(minLevel, sWorld->getIntConfig(CONFIG_START_HEROIC_PLAYER_LEVEL));
|
|
}
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "RandomizeFirst");
|
|
|
|
uint32 level;
|
|
|
|
if (sPlayerbotAIConfig.downgradeMaxLevelBot && bot->GetLevel() >= sPlayerbotAIConfig.randomBotMaxLevel)
|
|
{
|
|
if (bot->getClass() == CLASS_DEATH_KNIGHT)
|
|
{
|
|
level = sWorld->getIntConfig(CONFIG_START_HEROIC_PLAYER_LEVEL);
|
|
}
|
|
else
|
|
{
|
|
level = sPlayerbotAIConfig.randomBotMinLevel;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint32 roll = urand(1, 100);
|
|
if (roll <= 100 * sPlayerbotAIConfig.randomBotMaxLevelChance)
|
|
{
|
|
level = maxLevel;
|
|
}
|
|
else if (roll <=
|
|
(100 * (sPlayerbotAIConfig.randomBotMaxLevelChance + sPlayerbotAIConfig.randomBotMinLevelChance)))
|
|
{
|
|
level = minLevel;
|
|
}
|
|
else
|
|
{
|
|
level = urand(minLevel, maxLevel);
|
|
}
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.disableRandomLevels)
|
|
{
|
|
level = bot->getClass() == CLASS_DEATH_KNIGHT ? std::max(sPlayerbotAIConfig.randombotStartingLevel,
|
|
sWorld->getIntConfig(CONFIG_START_HEROIC_PLAYER_LEVEL))
|
|
: sPlayerbotAIConfig.randombotStartingLevel;
|
|
}
|
|
|
|
SetValue(bot, "level", level);
|
|
PlayerbotFactory factory(bot, level);
|
|
factory.Randomize(false);
|
|
|
|
uint32 randomTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotRandomizeTime, sPlayerbotAIConfig.maxRandomBotRandomizeTime);
|
|
uint32 inworldTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotInWorldTime, sPlayerbotAIConfig.maxRandomBotInWorldTime);
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_UPD_RANDOM_BOTS);
|
|
stmt->SetData(0, randomTime);
|
|
stmt->SetData(1, "bot_delete");
|
|
stmt->SetData(2, bot->GetGUID().GetCounter());
|
|
PlayerbotsDatabase.Execute(stmt);
|
|
|
|
stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_UPD_RANDOM_BOTS);
|
|
stmt->SetData(0, inworldTime);
|
|
stmt->SetData(1, "logout");
|
|
stmt->SetData(2, bot->GetGUID().GetCounter());
|
|
PlayerbotsDatabase.Execute(stmt);
|
|
|
|
// teleport to a random inn for bot level
|
|
botAI->Reset(true);
|
|
|
|
if (bot->GetGroup())
|
|
botAI->LeaveOrDisbandGroup();
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
|
|
RandomTeleportForLevel(bot);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomizeMin(Player* bot)
|
|
{
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
return;
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "RandomizeMin");
|
|
uint32 level = sPlayerbotAIConfig.randomBotMinLevel;
|
|
SetValue(bot, "level", level);
|
|
PlayerbotFactory factory(bot, level);
|
|
factory.Randomize(false);
|
|
|
|
uint32 randomTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotRandomizeTime, sPlayerbotAIConfig.maxRandomBotRandomizeTime);
|
|
uint32 inworldTime =
|
|
urand(sPlayerbotAIConfig.minRandomBotInWorldTime, sPlayerbotAIConfig.maxRandomBotInWorldTime);
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_UPD_RANDOM_BOTS);
|
|
stmt->SetData(0, randomTime);
|
|
stmt->SetData(1, "bot_delete");
|
|
stmt->SetData(2, bot->GetGUID().GetCounter());
|
|
PlayerbotsDatabase.Execute(stmt);
|
|
|
|
stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_UPD_RANDOM_BOTS);
|
|
stmt->SetData(0, inworldTime);
|
|
stmt->SetData(1, "logout");
|
|
stmt->SetData(2, bot->GetGUID().GetCounter());
|
|
PlayerbotsDatabase.Execute(stmt);
|
|
|
|
// teleport to a random inn for bot level
|
|
botAI->Reset(true);
|
|
|
|
if (bot->GetGroup())
|
|
botAI->LeaveOrDisbandGroup();
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Clear(Player* bot)
|
|
{
|
|
PlayerbotFactory factory(bot, bot->GetLevel());
|
|
factory.ClearEverything();
|
|
}
|
|
|
|
uint32 RandomPlayerbotMgr::GetZoneLevel(uint16 mapId, float teleX, float teleY, float teleZ)
|
|
{
|
|
uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
|
|
|
|
uint32 level = 0;
|
|
QueryResult results = WorldDatabase.Query(
|
|
"SELECT AVG(t.minlevel) minlevel, AVG(t.maxlevel) maxlevel FROM creature c "
|
|
"INNER JOIN creature_template t ON c.id1 = t.entry WHERE map = {} AND minlevel > 1 AND ABS(position_x - {}) < "
|
|
"{} AND ABS(position_y - {}) < {}",
|
|
mapId, teleX, sPlayerbotAIConfig.randomBotTeleportDistance / 2, teleY,
|
|
sPlayerbotAIConfig.randomBotTeleportDistance / 2);
|
|
|
|
if (results)
|
|
{
|
|
Field* fields = results->Fetch();
|
|
uint8 minLevel = fields[0].Get<uint8>();
|
|
uint8 maxLevel = fields[1].Get<uint8>();
|
|
level = urand(minLevel, maxLevel);
|
|
if (level > maxLevel)
|
|
level = maxLevel;
|
|
}
|
|
else
|
|
{
|
|
level = urand(1, maxLevel);
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Refresh(Player* bot)
|
|
{
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
return;
|
|
|
|
if (bot->isDead())
|
|
{
|
|
bot->ResurrectPlayer(1.0f);
|
|
bot->SpawnCorpseBones();
|
|
botAI->ResetStrategies(false);
|
|
}
|
|
|
|
// if (sPlayerbotAIConfig.disableRandomLevels)
|
|
// return;
|
|
|
|
if (bot->InBattleground())
|
|
return;
|
|
|
|
LOG_DEBUG("playerbots", "Refreshing bot {} <{}>", bot->GetGUID().ToString().c_str(), bot->GetName().c_str());
|
|
|
|
PerfMonitorOperation* pmo = sPerfMonitor.start(PERF_MON_RNDBOT, "Refresh");
|
|
|
|
botAI->Reset();
|
|
|
|
bot->DurabilityRepairAll(false, 1.0f, false);
|
|
bot->SetFullHealth();
|
|
bot->SetPvP(true);
|
|
PlayerbotFactory factory(bot, bot->GetLevel());
|
|
factory.Refresh();
|
|
|
|
if (bot->GetMaxPower(POWER_MANA) > 0)
|
|
bot->SetPower(POWER_MANA, bot->GetMaxPower(POWER_MANA));
|
|
|
|
if (bot->GetMaxPower(POWER_ENERGY) > 0)
|
|
bot->SetPower(POWER_ENERGY, bot->GetMaxPower(POWER_ENERGY));
|
|
|
|
uint32 money = bot->GetMoney();
|
|
bot->SetMoney(money + 500 * sqrt(urand(1, bot->GetLevel() * 5)));
|
|
|
|
if (bot->GetGroup())
|
|
botAI->LeaveOrDisbandGroup();
|
|
|
|
if (pmo)
|
|
pmo->finish();
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsRandomBot(Player* bot)
|
|
{
|
|
if (bot && GET_PLAYERBOT_AI(bot))
|
|
{
|
|
if (GET_PLAYERBOT_AI(bot)->IsRealPlayer())
|
|
return false;
|
|
}
|
|
if (bot)
|
|
{
|
|
return IsRandomBot(bot->GetGUID().GetCounter());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsRandomBot(ObjectGuid::LowType bot)
|
|
{
|
|
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot);
|
|
if (!sPlayerbotAIConfig.IsInRandomAccountList(sCharacterCache->GetCharacterAccountIdByGuid(guid)))
|
|
return false;
|
|
|
|
if (std::find(currentBots.begin(), currentBots.end(), bot) != currentBots.end())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsAddclassBot(Player* bot)
|
|
{
|
|
if (bot && GET_PLAYERBOT_AI(bot))
|
|
{
|
|
if (GET_PLAYERBOT_AI(bot)->IsRealPlayer())
|
|
return false;
|
|
}
|
|
if (bot)
|
|
{
|
|
return IsAddclassBot(bot->GetGUID().GetCounter());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsAddclassBot(ObjectGuid::LowType bot)
|
|
{
|
|
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(bot);
|
|
|
|
// Check the cache with faction considerations
|
|
for (uint8 claz = CLASS_WARRIOR; claz <= CLASS_DRUID; claz++)
|
|
{
|
|
if (claz == 10)
|
|
continue;
|
|
|
|
for (uint8 isAlliance = 0; isAlliance <= 1; isAlliance++)
|
|
{
|
|
if (addclassCache[GetTeamClassIdx(isAlliance, claz)].find(guid) !=
|
|
addclassCache[GetTeamClassIdx(isAlliance, claz)].end())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not in cache, check the account type
|
|
uint32 accountId = sCharacterCache->GetCharacterAccountIdByGuid(guid);
|
|
if (accountId && IsAccountType(accountId, 2)) // Type 2 = AddClass
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::GetBots()
|
|
{
|
|
if (!currentBots.empty())
|
|
return;
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt =
|
|
PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BY_OWNER_AND_EVENT);
|
|
stmt->SetData(0, 0);
|
|
stmt->SetData(1, "add");
|
|
uint32 maxAllowedBotCount = GetEventValue(0, "bot_count");
|
|
if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
|
|
{
|
|
do
|
|
{
|
|
Field* fields = result->Fetch();
|
|
uint32 bot = fields[0].Get<uint32>();
|
|
if (GetEventValue(bot, "add"))
|
|
currentBots.push_back(bot);
|
|
|
|
if (currentBots.size() >= maxAllowedBotCount)
|
|
break;
|
|
} while (result->NextRow());
|
|
}
|
|
}
|
|
|
|
std::vector<uint32> RandomPlayerbotMgr::GetBgBots(uint32 bracket)
|
|
{
|
|
// if (!currentBgBots.empty()) return currentBgBots;
|
|
|
|
std::vector<uint32> BgBots;
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt =
|
|
PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BY_EVENT_AND_VALUE);
|
|
stmt->SetData(0, "bg");
|
|
stmt->SetData(1, bracket);
|
|
if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
|
|
{
|
|
do
|
|
{
|
|
Field* fields = result->Fetch();
|
|
uint32 bot = fields[0].Get<uint32>();
|
|
BgBots.push_back(bot);
|
|
} while (result->NextRow());
|
|
}
|
|
|
|
return BgBots;
|
|
}
|
|
|
|
CachedEvent* RandomPlayerbotMgr::FindEvent(uint32 bot, std::string const& event)
|
|
{
|
|
BotEventCache& cache = eventCache[bot];
|
|
|
|
// Load once
|
|
if (!cache.loaded)
|
|
{
|
|
cache.events.clear();
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt =
|
|
PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_SEL_RANDOM_BOTS_BY_OWNER_AND_BOT);
|
|
stmt->SetData(0, 0);
|
|
stmt->SetData(1, bot);
|
|
|
|
if (PreparedQueryResult result = PlayerbotsDatabase.Query(stmt))
|
|
{
|
|
do
|
|
{
|
|
Field* fields = result->Fetch();
|
|
|
|
CachedEvent e;
|
|
e.value = fields[1].Get<uint32>();
|
|
e.lastChangeTime = fields[2].Get<uint32>();
|
|
e.validIn = fields[3].Get<uint32>();
|
|
e.data = fields[4].Get<std::string>();
|
|
|
|
cache.events.emplace(fields[0].Get<std::string>(), std::move(e));
|
|
} while (result->NextRow());
|
|
}
|
|
|
|
cache.loaded = true;
|
|
}
|
|
|
|
auto it = cache.events.find(event);
|
|
if (it == cache.events.end())
|
|
return nullptr;
|
|
|
|
CachedEvent& e = it->second;
|
|
|
|
// remove expired events
|
|
if (e.validIn && (NowSeconds() - e.lastChangeTime) >= e.validIn && event != "specNo" && event != "specLink")
|
|
{
|
|
cache.events.erase(it);
|
|
return nullptr;
|
|
}
|
|
|
|
return &e;
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::IsSpecPvp(uint32 bot, uint8 cls)
|
|
{
|
|
uint32 stored = GetValue(bot, "specNo");
|
|
if (!stored)
|
|
return false;
|
|
uint32 specIndex = stored - 1;
|
|
std::string const& name = sPlayerbotAIConfig.premadeSpecName[cls][specIndex];
|
|
return !name.empty() && name.find("pvp") != std::string::npos;
|
|
}
|
|
|
|
uint32 RandomPlayerbotMgr::GetEventValue(uint32 bot, std::string const& event)
|
|
{
|
|
if (CachedEvent* e = FindEvent(bot, event))
|
|
return e->value;
|
|
|
|
return 0;
|
|
}
|
|
|
|
std::string RandomPlayerbotMgr::GetEventData(uint32 bot, std::string const& event)
|
|
{
|
|
if (CachedEvent* e = FindEvent(bot, event))
|
|
return e->data;
|
|
|
|
return "";
|
|
}
|
|
|
|
uint32 RandomPlayerbotMgr::SetEventValue(uint32 bot, std::string const& event, uint32 value, uint32 validIn,
|
|
std::string const& data)
|
|
{
|
|
PlayerbotsDatabaseTransaction trans = PlayerbotsDatabase.BeginTransaction();
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt =
|
|
PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_DEL_RANDOM_BOTS_BY_OWNER_AND_EVENT);
|
|
stmt->SetData(0, 0);
|
|
stmt->SetData(1, bot);
|
|
stmt->SetData(2, event.c_str());
|
|
trans->Append(stmt);
|
|
|
|
if (value)
|
|
{
|
|
stmt = PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_INS_RANDOM_BOTS);
|
|
stmt->SetData(0, 0);
|
|
stmt->SetData(1, bot);
|
|
stmt->SetData(2, NowSeconds());
|
|
stmt->SetData(3, validIn);
|
|
stmt->SetData(4, event.c_str());
|
|
stmt->SetData(5, value);
|
|
|
|
if (!data.empty())
|
|
stmt->SetData(6, data.c_str());
|
|
else
|
|
stmt->SetData(6); // NULL
|
|
|
|
trans->Append(stmt);
|
|
}
|
|
|
|
PlayerbotsDatabase.CommitTransaction(trans);
|
|
|
|
// Update in-memory cache
|
|
BotEventCache& cache = eventCache[bot];
|
|
cache.loaded = true;
|
|
|
|
if (!value)
|
|
{
|
|
cache.events.erase(event);
|
|
return 0;
|
|
}
|
|
|
|
CachedEvent& e = cache.events[event]; // create-on-write is OK here
|
|
e.value = value;
|
|
e.lastChangeTime = NowSeconds();
|
|
e.validIn = validIn;
|
|
e.data = data;
|
|
|
|
return value;
|
|
}
|
|
|
|
uint32 RandomPlayerbotMgr::GetValue(uint32 bot, std::string const& type) { return GetEventValue(bot, type); }
|
|
|
|
uint32 RandomPlayerbotMgr::GetValue(Player* bot, std::string const& type)
|
|
{
|
|
return GetValue(bot->GetGUID().GetCounter(), type);
|
|
}
|
|
|
|
std::string RandomPlayerbotMgr::GetData(uint32 bot, std::string const& type) { return GetEventData(bot, type); }
|
|
|
|
void RandomPlayerbotMgr::SetValue(uint32 bot, std::string const& type, uint32 value, std::string const& data)
|
|
{
|
|
SetEventValue(bot, type, value, sPlayerbotAIConfig.maxRandomBotInWorldTime, data);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::SetValue(Player* bot, std::string const& type, uint32 value, std::string const& data)
|
|
{
|
|
SetValue(bot->GetGUID().GetCounter(), type, value, data);
|
|
}
|
|
|
|
bool RandomPlayerbotMgr::HandlePlayerbotConsoleCommand(ChatHandler* handler, char const* args)
|
|
{
|
|
if (!sPlayerbotAIConfig.enabled)
|
|
{
|
|
LOG_ERROR("playerbots", "Playerbots system is currently disabled!");
|
|
return false;
|
|
}
|
|
|
|
if (!args || !*args)
|
|
{
|
|
LOG_ERROR("playerbots", "Usage: rndbot stats/update/reset/init/refresh/add/remove");
|
|
return false;
|
|
}
|
|
|
|
std::string const cmd = args;
|
|
|
|
if (cmd == "reset")
|
|
{
|
|
PlayerbotsDatabase.Execute(PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_DEL_RANDOM_BOTS));
|
|
sRandomPlayerbotMgr.eventCache.clear();
|
|
LOG_INFO("playerbots", "Random bots were reset for all players. Please restart the Server.");
|
|
return true;
|
|
}
|
|
|
|
if (cmd == "stats")
|
|
{
|
|
sRandomPlayerbotMgr.PrintStats();
|
|
// activatePrintStatsThread();
|
|
return true;
|
|
}
|
|
|
|
if (cmd == "reload")
|
|
{
|
|
sPlayerbotAIConfig.Initialize();
|
|
return true;
|
|
}
|
|
|
|
if (cmd == "update")
|
|
{
|
|
sRandomPlayerbotMgr.UpdateAIInternal(0);
|
|
return true;
|
|
}
|
|
|
|
std::map<std::string, ConsoleCommandHandler> handlers;
|
|
// handlers["initmin"] = &RandomPlayerbotMgr::RandomizeMin;
|
|
handlers["init"] = &RandomPlayerbotMgr::RandomizeFirst;
|
|
handlers["clear"] = &RandomPlayerbotMgr::Clear;
|
|
handlers["levelup"] = handlers["level"] = &RandomPlayerbotMgr::IncreaseLevel;
|
|
handlers["refresh"] = &RandomPlayerbotMgr::Refresh;
|
|
handlers["teleport"] = &RandomPlayerbotMgr::RandomTeleportForLevel;
|
|
// handlers["rpg"] = &RandomPlayerbotMgr::RandomTeleportForRpg;
|
|
handlers["revive"] = &RandomPlayerbotMgr::Revive;
|
|
handlers["grind"] = &RandomPlayerbotMgr::RandomTeleport;
|
|
handlers["change_strategy"] = &RandomPlayerbotMgr::ChangeStrategy;
|
|
|
|
for (std::map<std::string, ConsoleCommandHandler>::iterator j = handlers.begin(); j != handlers.end(); ++j)
|
|
{
|
|
std::string const prefix = j->first;
|
|
if (cmd.find(prefix) != 0)
|
|
continue;
|
|
|
|
std::string const name = cmd.size() > prefix.size() + 1 ? cmd.substr(1 + prefix.size()) : "%";
|
|
|
|
std::vector<uint32> botIds;
|
|
for (std::vector<uint32>::iterator i = sPlayerbotAIConfig.randomBotAccounts.begin();
|
|
i != sPlayerbotAIConfig.randomBotAccounts.end(); ++i)
|
|
{
|
|
uint32 account = *i;
|
|
if (QueryResult results = CharacterDatabase.Query(
|
|
"SELECT guid FROM characters WHERE account = {} AND name like '{}'", account, name.c_str()))
|
|
{
|
|
do
|
|
{
|
|
Field* fields = results->Fetch();
|
|
|
|
uint32 botId = fields[0].Get<uint32>();
|
|
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(botId);
|
|
if (!sRandomPlayerbotMgr.IsRandomBot(guid.GetCounter()))
|
|
{
|
|
continue;
|
|
}
|
|
Player* bot = ObjectAccessor::FindPlayer(guid);
|
|
if (!bot)
|
|
continue;
|
|
|
|
botIds.push_back(botId);
|
|
} while (results->NextRow());
|
|
}
|
|
}
|
|
|
|
if (botIds.empty())
|
|
{
|
|
LOG_INFO("playerbots", "Nothing to do");
|
|
return false;
|
|
}
|
|
|
|
uint32 processed = 0;
|
|
for (std::vector<uint32>::iterator i = botIds.begin(); i != botIds.end(); ++i)
|
|
{
|
|
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(*i);
|
|
Player* bot = ObjectAccessor::FindPlayer(guid);
|
|
if (!bot)
|
|
continue;
|
|
|
|
LOG_INFO("playerbots", "[{}/{}] Processing command {} for bot {}", processed++, botIds.size(), cmd.c_str(),
|
|
bot->GetName().c_str());
|
|
|
|
ConsoleCommandHandler handler = j->second;
|
|
(sRandomPlayerbotMgr.*handler)(bot);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// std::vector<std::string> messages = sRandomPlayerbotMgr.HandlePlayerbotCommand(args);
|
|
// for (std::vector<std::string>::iterator i = messages.begin(); i != messages.end(); ++i)
|
|
// {
|
|
// LOG_INFO("playerbots", "{}", i->c_str());
|
|
// }
|
|
return true;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::HandleCommand(uint32 type, std::string const text, Player* fromPlayer, std::string channelName)
|
|
{
|
|
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
|
|
{
|
|
Player* const bot = it->second;
|
|
if (!bot)
|
|
continue;
|
|
|
|
if (!channelName.empty())
|
|
{
|
|
if (ChannelMgr* cMgr = ChannelMgr::forTeam(bot->GetTeamId()))
|
|
{
|
|
Channel* chn = cMgr->GetChannel(channelName, bot);
|
|
if (!chn)
|
|
continue;
|
|
}
|
|
}
|
|
|
|
GET_PLAYERBOT_AI(bot)->HandleCommand(type, text, fromPlayer);
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::OnPlayerLogout(Player* player)
|
|
{
|
|
DisablePlayerBot(player->GetGUID());
|
|
|
|
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
|
|
{
|
|
Player* const bot = it->second;
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (botAI && player == botAI->GetMaster())
|
|
{
|
|
botAI->SetMaster(nullptr);
|
|
if (!bot->InBattleground())
|
|
{
|
|
botAI->ResetStrategies();
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<Player*>::iterator i = std::find(players.begin(), players.end(), player);
|
|
if (i != players.end())
|
|
players.erase(i);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::OnBotLoginInternal(Player* const bot)
|
|
{
|
|
if (_isBotLogging)
|
|
{
|
|
LOG_INFO("playerbots", "{}/{} Bot {} logged in", playerBots.size(),
|
|
sRandomPlayerbotMgr.GetMaxAllowedBotCount(), bot->GetName().c_str());
|
|
|
|
if (playerBots.size() == sRandomPlayerbotMgr.GetMaxAllowedBotCount())
|
|
{
|
|
_isBotLogging = false;
|
|
}
|
|
}
|
|
|
|
// Run guild recovery/assignment at login to handle empty guild tables after restart.
|
|
if (sPlayerbotAIConfig.randomBotGuildCount > 0)
|
|
{
|
|
PlayerbotFactory factory(bot, bot->GetLevel());
|
|
factory.InitGuild();
|
|
}
|
|
|
|
if (sPlayerbotAIConfig.randomBotFixedLevel)
|
|
{
|
|
bot->SetPlayerFlag(PLAYER_FLAGS_NO_XP_GAIN);
|
|
}
|
|
else
|
|
{
|
|
bot->RemovePlayerFlag(PLAYER_FLAGS_NO_XP_GAIN);
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::OnPlayerLogin(Player* player)
|
|
{
|
|
uint32 botsNearby = 0;
|
|
|
|
for (PlayerBotMap::const_iterator it = GetPlayerBotsBegin(); it != GetPlayerBotsEnd(); ++it)
|
|
{
|
|
Player* const bot = it->second;
|
|
if (player == bot /* || GET_PLAYERBOT_AI(player)*/) // TEST
|
|
continue;
|
|
|
|
Cell playerCell(player->GetPositionX(), player->GetPositionY());
|
|
Cell botCell(bot->GetPositionX(), bot->GetPositionY());
|
|
|
|
// if (playerCell == botCell)
|
|
// botsNearby++;
|
|
|
|
Group* group = bot->GetGroup();
|
|
if (!group)
|
|
continue;
|
|
|
|
for (GroupReference* gref = group->GetFirstMember(); gref; gref = gref->next())
|
|
{
|
|
Player* member = gref->GetSource();
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (botAI && member == player && (!botAI->GetMaster() || GET_PLAYERBOT_AI(botAI->GetMaster())))
|
|
{
|
|
if (!bot->InBattleground())
|
|
{
|
|
botAI->SetMaster(player);
|
|
botAI->ResetStrategies();
|
|
botAI->TellMaster("Hello");
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (botsNearby > 100 && false)
|
|
{
|
|
WorldPosition botPos(player);
|
|
|
|
// botPos.GetReachableRandomPointOnGround(player, sPlayerbotAIConfig.reactDistance * 2, true);
|
|
|
|
// player->TeleportTo(botPos);
|
|
// player->Relocate(botPos.coord_x, botPos.coord_y, botPos.coord_z, botPos.orientation);
|
|
|
|
if (!player->GetFactionTemplateEntry())
|
|
{
|
|
botPos.GetReachableRandomPointOnGround(player, sPlayerbotAIConfig.reactDistance * 2, true);
|
|
}
|
|
else
|
|
{
|
|
std::vector<TravelDestination*> dests = TravelMgr::instance().getRpgTravelDestinations(player, true, true, 200000.0f);
|
|
|
|
do
|
|
{
|
|
RpgTravelDestination* dest = (RpgTravelDestination*)dests[urand(0, dests.size() - 1)];
|
|
CreatureTemplate const* cInfo = dest->GetCreatureTemplate();
|
|
if (!cInfo)
|
|
continue;
|
|
|
|
FactionTemplateEntry const* factionEntry = sFactionTemplateStore.LookupEntry(cInfo->faction);
|
|
ReputationRank reaction = Unit::GetFactionReactionTo(player->GetFactionTemplateEntry(), factionEntry);
|
|
|
|
if (reaction > REP_NEUTRAL && dest->nearestPoint(&botPos)->GetMapId() == player->GetMapId())
|
|
{
|
|
botPos = *dest->nearestPoint(&botPos);
|
|
break;
|
|
}
|
|
} while (true);
|
|
}
|
|
|
|
player->RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_TELEPORTED | AURA_INTERRUPT_FLAG_CHANGE_MAP);
|
|
player->TeleportTo(botPos);
|
|
|
|
// player->Relocate(botPos.getX(), botPos.getY(), botPos.getZ(), botPos.getO());
|
|
}
|
|
|
|
if (IsRandomBot(player))
|
|
{
|
|
// ObjectGuid::LowType guid = player->GetGUID().GetCounter(); //not used, conditional could be rewritten for
|
|
// simplicity. line marked for removal.
|
|
}
|
|
else
|
|
{
|
|
players.push_back(player);
|
|
LOG_DEBUG("playerbots", "Including non-random bot player {} into random bot update", player->GetName().c_str());
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::OnPlayerLoginError(uint32 bot)
|
|
{
|
|
SetEventValue(bot, "add", 0, 0);
|
|
currentBots.remove(bot);
|
|
}
|
|
|
|
Player* RandomPlayerbotMgr::GetRandomPlayer()
|
|
{
|
|
if (players.empty())
|
|
return nullptr;
|
|
|
|
uint32 index = urand(0, players.size() - 1);
|
|
return players[index];
|
|
}
|
|
|
|
void RandomPlayerbotMgr::PrintStats()
|
|
{
|
|
printStatsTimer = time(nullptr);
|
|
LOG_INFO("playerbots", "Random Bots Stats: {} online", playerBots.size());
|
|
|
|
std::map<uint8, uint32> alliance, horde;
|
|
for (uint32 i = 0; i < 10; ++i)
|
|
{
|
|
alliance[i] = 0;
|
|
horde[i] = 0;
|
|
}
|
|
|
|
std::map<uint8, uint32> perRace;
|
|
std::map<uint8, uint32> perClass;
|
|
|
|
std::map<uint8, uint32> lvlPerRace;
|
|
std::map<uint8, uint32> lvlPerClass;
|
|
for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race)
|
|
{
|
|
perRace[race] = 0;
|
|
lvlPerRace[race] = 0;
|
|
}
|
|
|
|
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
|
|
{
|
|
perClass[cls] = 0;
|
|
lvlPerClass[cls] = 0;
|
|
}
|
|
|
|
uint32 dps = 0;
|
|
uint32 heal = 0;
|
|
uint32 tank = 0;
|
|
uint32 active = 0;
|
|
/* uint32 update = 0;
|
|
uint32 randomize = 0;
|
|
uint32 teleport = 0;
|
|
uint32 changeStrategy = 0;*/
|
|
uint32 dead = 0;
|
|
uint32 combat = 0;
|
|
// uint32 revive = 0; //not used, line marked for removal.
|
|
uint32 inFlight = 0;
|
|
uint32 moving = 0;
|
|
uint32 mounted = 0;
|
|
uint32 inBg = 0;
|
|
uint32 rest = 0;
|
|
uint32 engine_noncombat = 0;
|
|
uint32 engine_combat = 0;
|
|
uint32 engine_dead = 0;
|
|
std::unordered_map<NewRpgStatus, int> rpgStatusCount;
|
|
// static NewRpgStatistic rpgStasticTotal;
|
|
std::unordered_map<uint32, int> zoneCount;
|
|
uint8 maxBotLevel = 0;
|
|
for (PlayerBotMap::iterator i = playerBots.begin(); i != playerBots.end(); ++i)
|
|
{
|
|
Player* bot = i->second;
|
|
if (IsAlliance(bot->getRace()))
|
|
++alliance[bot->GetLevel()];
|
|
else
|
|
++horde[bot->GetLevel()];
|
|
maxBotLevel = std::max(maxBotLevel, bot->GetLevel());
|
|
|
|
++perRace[bot->getRace()];
|
|
++perClass[bot->getClass()];
|
|
|
|
lvlPerClass[bot->getClass()] += bot->GetLevel();
|
|
lvlPerRace[bot->getRace()] += bot->GetLevel();
|
|
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
{
|
|
LOG_ERROR("playerbots", "Player/Bot {} is registered in sRandomPlayerbotMgr playerBots and has no bot AI!", bot->GetName().c_str());
|
|
continue;
|
|
}
|
|
|
|
if (botAI->AllowActivity())
|
|
++active;
|
|
/* TODO: Review statistics on rpg merge
|
|
if (botAI->GetAiObjectContext()->GetValue<bool>("random bot update")->Get())
|
|
++update;
|
|
|
|
uint32 botId = bot->GetGUID().GetCounter();
|
|
if (!GetEventValue(botId, "randomize"))
|
|
++randomize;
|
|
|
|
if (!GetEventValue(botId, "teleport"))
|
|
++teleport;
|
|
|
|
if (!GetEventValue(botId, "change_strategy"))
|
|
++changeStrategy;
|
|
*/
|
|
if (bot->isDead())
|
|
{
|
|
++dead;
|
|
// if (!GetEventValue(botId, "dead"))
|
|
//++revive;
|
|
}
|
|
if (bot->IsInCombat())
|
|
++combat;
|
|
|
|
if (bot->isMoving())
|
|
++moving;
|
|
|
|
if (bot->IsInFlight())
|
|
++inFlight;
|
|
|
|
if (bot->IsMounted())
|
|
++mounted;
|
|
|
|
if (bot->InBattleground() || bot->InArena())
|
|
++inBg;
|
|
|
|
if (bot->HasFlag(PLAYER_FLAGS, PLAYER_FLAGS_RESTING))
|
|
++rest;
|
|
|
|
if (botAI->GetState() == BOT_STATE_NON_COMBAT)
|
|
++engine_noncombat;
|
|
|
|
else if (botAI->GetState() == BOT_STATE_COMBAT)
|
|
++engine_combat;
|
|
|
|
else
|
|
++engine_dead;
|
|
|
|
if (botAI->IsHeal(bot, true))
|
|
++heal;
|
|
|
|
else if (botAI->IsTank(bot, true))
|
|
++tank;
|
|
|
|
else
|
|
++dps;
|
|
|
|
zoneCount[bot->GetZoneId()]++;
|
|
|
|
if (sPlayerbotAIConfig.enableNewRpgStrategy)
|
|
{
|
|
rpgStatusCount[botAI->rpgInfo.GetStatus()]++;
|
|
rpgStasticTotal += botAI->rpgStatistic;
|
|
botAI->rpgStatistic = NewRpgStatistic();
|
|
}
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Bots level:");
|
|
// uint32 maxLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL);
|
|
uint32_t currentAlliance = 0, currentHorde = 0;
|
|
uint32_t step = std::max(1, static_cast<int>((maxBotLevel + 4) / 8));
|
|
uint32_t from = 1;
|
|
|
|
for (uint8 i = 1; i <= maxBotLevel; ++i)
|
|
{
|
|
currentAlliance += alliance[i];
|
|
currentHorde += horde[i];
|
|
|
|
if (((i + 1) % step == 0) || i == maxBotLevel)
|
|
{
|
|
if (currentAlliance || currentHorde)
|
|
LOG_INFO("playerbots", " {}..{}: {} alliance, {} horde", from, i, currentAlliance, currentHorde);
|
|
currentAlliance = 0;
|
|
currentHorde = 0;
|
|
from = i + 1;
|
|
}
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Bots race:");
|
|
for (uint8 race = RACE_HUMAN; race < sRaceMgr->GetMaxRaces(); ++race)
|
|
{
|
|
if (perRace[race])
|
|
{
|
|
uint32 lvl = lvlPerRace[race] * 10 / perRace[race];
|
|
float flvl = lvl / 10.0f;
|
|
LOG_INFO("playerbots", " {}: {}, avg lvl: {}", ChatHelper::FormatRace(race).c_str(), perRace[race],
|
|
flvl);
|
|
}
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Bots class:");
|
|
for (uint8 cls = CLASS_WARRIOR; cls < MAX_CLASSES; ++cls)
|
|
{
|
|
if (perClass[cls])
|
|
{
|
|
uint32 lvl = lvlPerClass[cls] * 10 / perClass[cls];
|
|
float flvl = lvl / 10.0f;
|
|
LOG_INFO("playerbots", " {}: {}, avg lvl: {}", ChatHelper::FormatClass(cls).c_str(), perClass[cls],
|
|
flvl);
|
|
}
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Bots role:");
|
|
LOG_INFO("playerbots", " tank: {}, heal: {}, dps: {}", tank, heal, dps);
|
|
|
|
LOG_INFO("playerbots", "Bots status:");
|
|
LOG_INFO("playerbots", " Active: {}", active);
|
|
LOG_INFO("playerbots", " Moving: {}", moving);
|
|
|
|
// LOG_INFO("playerbots", "Bots to:");
|
|
// LOG_INFO("playerbots", " update: {}", update);
|
|
// LOG_INFO("playerbots", " randomize: {}", randomize);
|
|
// LOG_INFO("playerbots", " teleport: {}", teleport);
|
|
// LOG_INFO("playerbots", " change_strategy: {}", changeStrategy);
|
|
// LOG_INFO("playerbots", " revive: {}", revive);
|
|
|
|
LOG_INFO("playerbots", " In flight: {}", inFlight);
|
|
LOG_INFO("playerbots", " On mount: {}", mounted);
|
|
LOG_INFO("playerbots", " In combat: {}", combat);
|
|
LOG_INFO("playerbots", " In BG: {}", inBg);
|
|
LOG_INFO("playerbots", " In Rest: {}", rest);
|
|
LOG_INFO("playerbots", " Dead: {}", dead);
|
|
|
|
if (sPlayerbotAIConfig.enableNewRpgStrategy)
|
|
{
|
|
LOG_INFO("playerbots", "Bots rpg status:");
|
|
LOG_INFO("playerbots",
|
|
" Idle: {}, Rest: {}, GoGrind: {}, GoCamp: {}, MoveRandom: {}, MoveNpc: {}, DoQuest: {}, "
|
|
"TravelFlight: {}, OutdoorPvP: {}",
|
|
rpgStatusCount[RPG_IDLE], rpgStatusCount[RPG_REST], rpgStatusCount[RPG_GO_GRIND],
|
|
rpgStatusCount[RPG_GO_CAMP], rpgStatusCount[RPG_WANDER_RANDOM], rpgStatusCount[RPG_WANDER_NPC],
|
|
rpgStatusCount[RPG_DO_QUEST], rpgStatusCount[RPG_TRAVEL_FLIGHT], rpgStatusCount[RPG_OUTDOOR_PVP]);
|
|
|
|
LOG_INFO("playerbots", "Bots total quests:");
|
|
LOG_INFO("playerbots", " Accepted: {}, Rewarded: {}, Dropped: {}", rpgStasticTotal.questAccepted,
|
|
rpgStasticTotal.questRewarded, rpgStasticTotal.questDropped);
|
|
}
|
|
|
|
LOG_INFO("playerbots", "Bots engine:", dead);
|
|
LOG_INFO("playerbots", " Non-combat: {}, Combat: {}, Dead: {}", engine_noncombat, engine_combat, engine_dead);
|
|
}
|
|
|
|
double RandomPlayerbotMgr::GetBuyMultiplier(Player* bot)
|
|
{
|
|
uint32 id = bot->GetGUID().GetCounter();
|
|
uint32 value = GetEventValue(id, "buymultiplier");
|
|
if (!value)
|
|
{
|
|
value = urand(50, 120);
|
|
uint32 validIn = urand(sPlayerbotAIConfig.minRandomBotsPriceChangeInterval,
|
|
sPlayerbotAIConfig.maxRandomBotsPriceChangeInterval);
|
|
SetEventValue(id, "buymultiplier", value, validIn);
|
|
}
|
|
|
|
return (double)value / 100.0;
|
|
}
|
|
|
|
double RandomPlayerbotMgr::GetSellMultiplier(Player* bot)
|
|
{
|
|
uint32 id = bot->GetGUID().GetCounter();
|
|
uint32 value = GetEventValue(id, "sellmultiplier");
|
|
if (!value)
|
|
{
|
|
value = urand(80, 250);
|
|
uint32 validIn = urand(sPlayerbotAIConfig.minRandomBotsPriceChangeInterval,
|
|
sPlayerbotAIConfig.maxRandomBotsPriceChangeInterval);
|
|
SetEventValue(id, "sellmultiplier", value, validIn);
|
|
}
|
|
|
|
return (double)value / 100.0;
|
|
}
|
|
|
|
void RandomPlayerbotMgr::AddTradeDiscount(Player* bot, Player* master, int32 value)
|
|
{
|
|
if (!master)
|
|
return;
|
|
|
|
uint32 discount = GetTradeDiscount(bot, master);
|
|
int32 result = (int32)discount + value;
|
|
discount = (result < 0 ? 0 : result);
|
|
|
|
SetTradeDiscount(bot, master, discount);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::SetTradeDiscount(Player* bot, Player* master, uint32 value)
|
|
{
|
|
if (!master)
|
|
return;
|
|
|
|
uint32 botId = bot->GetGUID().GetCounter();
|
|
uint32 masterId = master->GetGUID().GetCounter();
|
|
|
|
std::ostringstream name;
|
|
name << "trade_discount_" << masterId;
|
|
SetEventValue(botId, name.str(), value, sPlayerbotAIConfig.maxRandomBotInWorldTime);
|
|
}
|
|
|
|
uint32 RandomPlayerbotMgr::GetTradeDiscount(Player* bot, Player* master)
|
|
{
|
|
if (!master)
|
|
return 0;
|
|
|
|
uint32 botId = bot->GetGUID().GetCounter();
|
|
uint32 masterId = master->GetGUID().GetCounter();
|
|
|
|
std::ostringstream name;
|
|
name << "trade_discount_" << masterId;
|
|
return GetEventValue(botId, name.str());
|
|
}
|
|
|
|
std::string const RandomPlayerbotMgr::HandleRemoteCommand(std::string const request)
|
|
{
|
|
std::string::const_iterator pos = std::find(request.begin(), request.end(), ',');
|
|
if (pos == request.end())
|
|
{
|
|
std::ostringstream out;
|
|
out << "invalid request: " << request;
|
|
return out.str();
|
|
}
|
|
|
|
std::string const command = std::string(request.begin(), pos);
|
|
ObjectGuid guid = ObjectGuid::Create<HighGuid::Player>(atoi(std::string(pos + 1, request.end()).c_str()));
|
|
Player* bot = GetPlayerBot(guid);
|
|
if (!bot)
|
|
return "invalid guid";
|
|
|
|
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
|
|
if (!botAI)
|
|
return "invalid guid";
|
|
|
|
return botAI->HandleRemoteCommand(command);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::ChangeStrategy(Player* player)
|
|
{
|
|
uint32 bot = player->GetGUID().GetCounter();
|
|
|
|
if (frand(0.f, 100.f) > sPlayerbotAIConfig.randomBotRpgChance)
|
|
{
|
|
LOG_INFO("playerbots", "Bot #{} <{}>: sent to grind spot", bot, player->GetName().c_str());
|
|
ScheduleTeleport(bot, 30);
|
|
}
|
|
else
|
|
{
|
|
LOG_INFO("playerbots", "Changing strategy for bot #{} <{}> to RPG", bot, player->GetName().c_str());
|
|
LOG_INFO("playerbots", "Bot #{} <{}>: sent to inn", bot, player->GetName().c_str());
|
|
RandomTeleportForLevel(player);
|
|
SetEventValue(bot, "teleport", 1, sPlayerbotAIConfig.maxRandomBotInWorldTime);
|
|
}
|
|
|
|
ScheduleChangeStrategy(bot);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::ChangeStrategyOnce(Player* player)
|
|
{
|
|
uint32 bot = player->GetGUID().GetCounter();
|
|
|
|
if (frand(0.f, 100.f) > sPlayerbotAIConfig.randomBotRpgChance) // select grind / pvp
|
|
{
|
|
LOG_INFO("playerbots", "Bot #{} <{}>: sent to grind spot", bot, player->GetName().c_str());
|
|
RandomTeleportForLevel(player);
|
|
Refresh(player);
|
|
}
|
|
else
|
|
{
|
|
LOG_INFO("playerbots", "Bot #{} <{}>: sent to inn", bot, player->GetName().c_str());
|
|
RandomTeleportForLevel(player);
|
|
}
|
|
}
|
|
|
|
void RandomPlayerbotMgr::RandomTeleportForRpg(Player* bot)
|
|
{
|
|
uint32 race = bot->getRace();
|
|
uint32 level = bot->GetLevel();
|
|
LOG_DEBUG("playerbots", "Random teleporting bot {} for RPG ({} locations available)", bot->GetName().c_str(),
|
|
rpgLocsCacheLevel[race].size());
|
|
RandomTeleport(bot, rpgLocsCacheLevel[race][level], true);
|
|
}
|
|
|
|
void RandomPlayerbotMgr::Remove(Player* bot)
|
|
{
|
|
ObjectGuid owner = bot->GetGUID();
|
|
|
|
PlayerbotsDatabasePreparedStatement* stmt =
|
|
PlayerbotsDatabase.GetPreparedStatement(PLAYERBOTS_DEL_RANDOM_BOTS_BY_OWNER);
|
|
stmt->SetData(0, 0);
|
|
stmt->SetData(1, owner.GetCounter());
|
|
PlayerbotsDatabase.Execute(stmt);
|
|
|
|
uint32 botId = owner.GetCounter();
|
|
eventCache.erase(botId);
|
|
|
|
LogoutPlayerBot(owner);
|
|
}
|
|
|
|
CreatureData const* RandomPlayerbotMgr::GetCreatureDataByEntry(uint32 entry)
|
|
{
|
|
if (entry != 0)
|
|
{
|
|
for (auto const& itr : sObjectMgr->GetAllCreatureData())
|
|
if (itr.second.id1 == entry)
|
|
return &itr.second;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
ObjectGuid RandomPlayerbotMgr::GetBattleMasterGUID(Player* bot, BattlegroundTypeId bgTypeId)
|
|
{
|
|
ObjectGuid battleMasterGUID = ObjectGuid::Empty;
|
|
|
|
TeamId team = bot->GetTeamId();
|
|
std::vector<uint32> Bms;
|
|
|
|
for (auto i = std::begin(BattleMastersCache[team][bgTypeId]); i != std::end(BattleMastersCache[team][bgTypeId]);
|
|
++i)
|
|
{
|
|
Bms.insert(Bms.end(), *i);
|
|
}
|
|
|
|
for (auto i = std::begin(BattleMastersCache[TEAM_NEUTRAL][bgTypeId]);
|
|
i != std::end(BattleMastersCache[TEAM_NEUTRAL][bgTypeId]); ++i)
|
|
{
|
|
Bms.insert(Bms.end(), *i);
|
|
}
|
|
|
|
if (Bms.empty())
|
|
return battleMasterGUID;
|
|
|
|
float dist1 = FLT_MAX;
|
|
|
|
for (auto i = begin(Bms); i != end(Bms); ++i)
|
|
{
|
|
CreatureData const* data = sRandomPlayerbotMgr.GetCreatureDataByEntry(*i);
|
|
if (!data)
|
|
continue;
|
|
|
|
Unit* Bm = PlayerbotAI::GetUnit(data);
|
|
if (!Bm)
|
|
continue;
|
|
|
|
if (bot->GetMapId() != Bm->GetMapId())
|
|
continue;
|
|
|
|
// return first available guid on map if queue from anywhere
|
|
if (!BattlegroundMgr::IsArenaType(bgTypeId))
|
|
{
|
|
battleMasterGUID = Bm->GetGUID();
|
|
break;
|
|
}
|
|
|
|
AreaTableEntry const* zone = sAreaTableStore.LookupEntry(Bm->GetZoneId());
|
|
if (!zone)
|
|
continue;
|
|
|
|
if (zone->team == 4 && bot->GetTeamId() == TEAM_ALLIANCE)
|
|
continue;
|
|
|
|
if (zone->team == 2 && bot->GetTeamId() == TEAM_HORDE)
|
|
continue;
|
|
|
|
if (Bm->getDeathState() == DeathState::Dead)
|
|
continue;
|
|
|
|
float dist2 = ServerFacade::instance().GetDistance2d(bot, data->posX, data->posY);
|
|
if (dist2 < dist1)
|
|
{
|
|
dist1 = dist2;
|
|
battleMasterGUID = Bm->GetGUID();
|
|
}
|
|
}
|
|
|
|
return battleMasterGUID;
|
|
}
|