Compare commits

..

114 Commits

Author SHA1 Message Date
bashermens
6cf7f1aaef [CHORE] Util classes format and simple cleanup with generated resources (#2028)
- Did some basic formatting
- Some generated docs
- Cleaned header/impl Helper.css
- Moved PerfMonitor from util to bot/handler/command

Still a freaking mess though, but its a start i guess. Cant ask ppl to
add more or make use of those when its so messy.
2026-01-18 00:43:44 +01:00
bashermens
9f54d7e702 Removed the expansion folder from dungeons (#2027)
In order to make consistent with raids but also to shorten max used
length directory for windows builds
2026-01-17 21:55:08 +01:00
bashermens
aeaaee15da [FIX] Finalized structure! (do not start fixing PR merge structure conflict till this is merged) (#2025)
Finalized
2026-01-17 14:38:12 +01:00
bashermens
a1137dbddc [FIX] Folder restructure (#2018)
As requested

Poll
```
1. Yes
2. Yes
3. Maybe, but yes
```

---------

Co-authored-by: Celandriel <22352763+Celandriel@users.noreply.github.com>
2026-01-17 10:34:58 +01:00
bashermens
2eb98c3233 [BUILD] Windows shorter build path (#2021) 2026-01-16 17:39:12 +01:00
Keleborn
29613e29b7 Bug - Selfbot can trigger crash in printstats (#2013)
Adding a guard against bots that may be registered in `playerBots`
variable as selfbot have appeared to crash here.

Edit: The exact circumstances that caused the incorrect registration are
under investigation as it seems to be 'atypical' behavior.
2026-01-15 00:42:37 +01:00
privatecore
e2c203a35e Fix the duplicate spells issue during randomization (#2014)
**Original issue:**
Bots/Characters received duplicate spells during randomization process.

**Root cause:**
When `PlayerbotFactory::Randomize` is processed, `InitSkills` is called
AFTER `InitAvailableSpells`, which causes the duplicate spells issue
because the skillline ability spell is already learned when processing
spells from trainers (`InitAvailableSpells`).

We simply need to change the order of the randomization process: skills
should be handled first, then spells. An alternative approach would be
to adjust the skillline abilities and check each spell for every skill,
but that seems redundant since we already have checks for the trainer's
spells.

`InitSkills` -> `SetRandomSkill` -> `SetSkill` ->
`learnSkillRewardedSpells` -> `learnSpell` -> duplicate error...

**Steps to reproduce:**
1. create common character and login with it
2. set level for this character eq. 80 (`.set level 79`)
3. create and run macro:
```
/g .playerbots bot initself
/g .saveall
```
4. logout -> login and run macro again

**Note:**
Also added checks for the trainer's spells since `GetSpell` can return
nullptr. Updated `LearnQuestSpells` after recent changes and used the
same logic and implementation from `Player::learnQuestRewardedSpells`.

Yes, we need to cast spells, or we should handle all spell effects that
quests/trainers have (for ex.: `SPELL_EFFECT_SKILL_STEP`,
`SPELL_EFFECT_UNLEARN_SPECIALIZATION`, `SPELL_EFFECT_BIND`).
2026-01-15 00:42:19 +01:00
bashermens
1b1ed18a23 Minor fix (#2015) 2026-01-15 00:42:09 +01:00
Not
2ab73c1fd5 Fix deprecated vsprintf usage in Engine logging 2nd edition (#1978)
Replace deprecated vsprintf with vsnprintf to eliminate compiler warning
and prevent potential buffer overflow. Updated to latest commit.

Tested in game and it seemed to log actions just fine. I just basically
added a buffer size by using the current vsnprintf lib instead.
2026-01-14 18:41:56 +01:00
OlegKungurov
6b97c379ba Fix fishing AI position matching precision (#1992)
**Problem**: FishingAction::isUseful() used strict `pos ==
bot->GetPosition()` comparison causing "Rog A:go fishing - USELESS" when
bot was within 0.5-1m of fishing spot (normal positioning tolerance).

**Solution**: Replace exact coordinate match with 1.5m tolerance using
`fabs(posX - botX) < 1.5f` for all axes. Added `#include "Config.h"` for
AI_VALUE stability.

**Result**: Master fishing now reliably triggers and completes cycle:
- move near water → go fishing → use bobber ✓

Closes #fishing-useless-bug

---------

Co-authored-by: Кунгуров Олег <okungurov@rapidsoft.ru>
2026-01-14 18:41:06 +01:00
bashermens
965d300203 [HOTFIX] Revert "Fix/Feat: Stop bots in party from PVP when master isn't, and … (#2003)
…PVP probablity system (#1914)"

This reverts commit 02e8465a3b.


[worldserver_gdb_20260111_000520.log](https://github.com/user-attachments/files/24547263/worldserver_gdb_20260111_000520.log)


Noticed the server kept crashing all the sudden, first thought it was
local issue but see dump.
2026-01-11 01:52:25 +01:00
privatecore
dc55ecfd9c Fix wrong logic when processing quest reward spells during maintenance level-up action (#2001)
The previous behavior failed cuz if a spell has no
`SPELL_EFFECT_LEARN_SPELL`, bots learned the original spell from
`quest_template` rewards w/o any checks, when they should never learn
any spells specified in the `RewardSpell` and/or `RewardDisplaySpell`.

PR: https://github.com/mod-playerbots/mod-playerbots/pull/1996 Thx
@Wishmaster117 for the collaboration!

Fix issue https://github.com/mod-playerbots/mod-playerbots/issues/1759
2026-01-10 01:31:54 +01:00
Crow
59d6eb139e Exclude additional test enchants from bots (#1952)
Added two test enchants to exclude from the list of enchants that bots
can apply: 19927 and 44119. I also reordered the list to be in
alphanumeric order.

The important exclusion to add is 44119:
https://www.wowhead.com/wotlk/spell=44119/enchant-bracer-template

It is a test enchant that gives 34 AP on bracers; this is stronger than
any other bracer enchant for level 70 physical attackers so bots are
applying it.
2026-01-09 22:16:33 +01:00
Crow
00171a8c82 Minor fixes to .dist descriptions (#1945)
Clarified that MinEnchantingBotLevel also determines whether maintenance
will socket gems, in addition to applying enchants.

Fixed max iLevel for ZA gear in RandomGearScoreLimit description (iLevel
138 is the max that drops from the final boss, but the 3rd timed chest
gives iLevel 141 equipment).
2026-01-09 19:57:29 +01:00
NoxMax
02e8465a3b Fix/Feat: Stop bots in party from PVP when master isn't, and PVP probablity system (#1914)
There are two related PVP components in this PR. First is the simple yet
fundamental change to bot behaviour when they are in party. Right now
bots with a master will go into PVP when there's a nearby PVP target,
even if master is not in PVP. This absolutely should not happen. Bots
should not consider PVP at all if master is not in PVP. The fix is only
3 lines in EnemyPlayerValue

The second component is introducing PVP probabilities, to make decisions
more realistic. Right now even a level 1 bot will 100% go into PVP if it
sees a level 80 PVP target. They can't help themselves. So the change
here addresses that insanity. Several thresholds (subject to community
review) are introduced:

1. Bots will not fight a target 5 or more levels higher than them
2. Bots have a 25% chance starting a fight with a target +/- 4 levels
from them.
3. Bots have a 50% chance starting a fight with a target +/- 3 levels
from them.
4. Bots have a 75% chance starting a fight with a target +/- 2 levels
from them.
5. Bots have a 100% chance starting a fight with a target +/- 1 level
from them.
6. Bots have a 25% chance starting a fight with a target 5 or more
levels below them (ganking. thought it would be funny, and technically
realistic of player behaviour)

Exception of course exist for BG/Arena/Duel, and in capitals where bots
will always PVP. Also bots will always defend themselves if attacked.

Few notes: 
1. The if/ else if logic can be further simplified, but only if we use
thresholds that are different by one. So current logic allows for
flexibility of using values like 10/7/5/3 instead of 5/4/3/2.
2. The caching system is per-bot basis. So for some target X, if some
bot decides to attack it, another bot will make its own decision. At
first I used a simplified global system (thinking there might be
performance concerns) where if one bot decides to attack a target then
they all do, but when I switched to the more realistic per-bot basis, I
didn't see an effect on performance.
3. Variables are obviously not configurable right now. I'm starting to
see Bash's POV that maybe we have too many configs 😬 Still,
they can be easily exposed in the future, and if someone is reading this
then, remember to change constexpr to const.
2026-01-08 22:44:09 +01:00
Crow
f53a8704eb Remove InitSpells() (#1983)
InitSpells() is never called, and even if it were to be, it would do
nothing but call InitAvailableSpells().
2026-01-08 11:46:36 -08:00
bashermens
e5525958c8 [BUG FIX] Movement flag update called for bots while rooted (#1991)
Fix for the error "Attempted sending heartbeat with root flag for guid"

The core does not allow movement flag updates when unit/player has the
MOVEMENTFLAG_ROOT flag. Change scope bots alone.
2026-01-08 20:38:49 +01:00
Alex Dcnh
9ae457d069 [BUG FIX] - Preserve buffs on summon by avoiding aura clears (#1958)
### Summary

- Add a preserveAuras flag to summon teleport helpers.
- Keep auras when using the summon command.
- Leave meeting stone behavior unchanged (auras can still be cleared
there).

### Motivation
Summon command was clearing buffs (e.g., Hellscream’s Warsong in ICC)
because auras were explicitly interrupted before teleporting. This
change keeps existing meeting stone behavior but preserves auras for the
summon path to avoid losing valid buffs.

### Details
Solve : 
https://github.com/mod-playerbots/mod-playerbots/issues/1862
https://github.com/mod-playerbots/mod-playerbots/issues/1942

### Testing
Go to ICC and téléport Bots
2026-01-07 15:24:41 +01:00
privatecore
c9e98a6b4e [BUG FIX] the issue where bots on vehicles cant move (#1969)
Fix the [issue
#1964](https://github.com/mod-playerbots/mod-playerbots/issues/1964)
where bots on vehicles cant move. Transfer the allowed movement logic to
`PlayerbotAI::CanMove` -- Ulduar raid strategy also used this method for
the Malady of the Mind trigger.

The only remaining `IsRooted` check is in
`MovementAction::UpdateMovementState`. I'm not sure if this also needs
to be updated for the vehicles case, but on paper, everything should
work as intended. A more complex solution to fix this issue can be found
in the
[comment](https://github.com/mod-playerbots/mod-playerbots/issues/1902#issuecomment-3703795779),
but for now, these changes are safer.
2026-01-07 15:24:18 +01:00
bashermens
3d9623f119 [CRASH FIX] Various crash fixes in regard to defensive checks around unit/targets and finer grained checks on tele ack (#1951)
Some basic defense checks on mainly unit/targets used in public
functions, and some minor tweaks with teleport ack.
https://github.com/mod-playerbots/mod-playerbots/issues/1934
https://github.com/mod-playerbots/mod-playerbots/issues/1957
2026-01-07 15:22:36 +01:00
Nicolas Lebacq
c9cc4324d3 NextAction refactoring to eliminate sentinel arrays and pointers (#1923)
<!--
> [!WARNING]
> **This is a DRAFT PR.**
> The structure is not definitive. The code might not be optimised yet.
It might not even start nor compile yet.
> **Don't panic.  It's going to be ok. 👌 We can make modifications, we
can fix things.** 😁
-->
# Description

This PR aims to refactor the NextAction declaration to achieve two
goals:

## Eliminate C-style sentinel arrays

Currently, a double pointer (`NextAction**`) approach is being used.
This an old pre-C++11 (< 2011) trick before `std::vector<>` became a
thing.
This approach is painful for developers because they constantly need to
declare their `NextAction` arrays as:
```cpp
NextAction::array(0, new NextAction("foo", 1.0f), nullptr)
```
Instead of:
```cpp
{ new NextAction("foo", 1.0f) }
```
The first argument of `NextAction::array` is actually a hack. It is used
to have a named argument so `va_args` can find the remaining arguments.
It is set to 0 everywhere but in fact does nothing. This is very
confusing to people unfamiliar with this antiquated syntax.
The last argument `nullptr` is what we call a sentinel. It's a `nullptr`
because `va_args` is looking for a `nullptr` to stop iterating. It's
also a hack and also leads to confusion.

## Eliminate unnecessary pointers for `NextAction`

Pointers can be used for several reasons, to cite a few:
- Indicate strong, absolute identity.
- Provide strong but transferable ownership (unlike references).
- When a null value is acceptable (`nullptr`).
- When copy is expensive.

`NextAction` meets none of these criteria:
- It has no identity because it is purely behavioural.
- It is never owned by anything as it is passed around and never fetched
from a registry.
- The only situations where it can be `nullptr` are errors that should
in fact throw an `std::invalid_argument` instead.
- They are extremely small objects that embark a single `std::string`
and a single `float`.

Pointers should be avoided when not strictly necessary because they can
quickly lead to undefined behaviour due to unhandled `nullptr`
situations. They also make the syntax heavier due to the necessity to
constantly check for `nullptr`. Finally, they aren't even good for
performance in that situation because shifting a pointer so many times
is likely more expensive than copying such a trivial object.

# End goal

The end goal is to declare `NextAction` arrays this way:
```cpp
{ NextAction("foo", 1.0f) }
```

> [!NOTE]
> Additional note: `NextAction` is nothing but a hacky proxy to an
`Action` constructor. This should eventually be reworked to use handles
instead of strings. This would make copying `NextAction` even cheaper
and remove the need for the extremely heavy stringly typed current
approach. Stringly typed entities are a known anti-pattern so we need to
move on from those.
2026-01-06 12:37:39 +01:00
Keleborn
b13fb7d12a Bugfix - PlayerbotGuildMgr not respecting real player guilds. (#1972)
Minor fix to address small bug introduced by #1913
2026-01-06 01:31:02 +01:00
Revision
962fdeb3d1 Updated to support latest master (#1965)
This needs extensive testing.

What's important is spells given to bots. Class spells, mounts,
professions etc. Make sure they get the spells they should, when they
should.

Requires https://github.com/mod-playerbots/azerothcore-wotlk/pull/132
2026-01-05 15:06:45 +01:00
Keleborn
83c6977de5 Refactor guild managment into a singleton (#1913)
The idea is to centralize the creation, assignment, and management of
bot guilds into a single class that can be referenced.

The way this is intended to work.

when the manager is created, if the config option to delete guilds is
set, then it deletes all bot guilds.

On startup 
1. Load all guild names from database. Shuffle keys for some
randomization.
2. Load Guilds from database
3. For existing guilds, identify the guild faction, number of members,
and assess if the guild is 'full' based on the number of bots set in
config.
4. Determine if the leader of the guild is a real player based on the
leader account.
5. Mark any playerbot guild names as not available (false).

The validation process (2-5) is set to run once an hour. 

Guild Creation.
Now guild creation occurs on an as needed bases during the
initialization process. Previously, all of the guilds would be created
at once, and then randomly assigned.
When a bot is not in a guild during initialization, it will check if
there are any partially filled guilds of that bots faction where the bot
can be assigned to. If not, and the cache of bot guilds is less than the
set number in config, it will randomly return the available name. This
then goes to the CreateGuild function where the core guild manager
creates a guild, the guild emblem is set, and the cache updated.
If a bot is assigned to guild, but fails to join then it throws an
error.

Checking for real player guilds function now lives in the guild manager.

---------

Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2026-01-03 15:15:28 +01:00
Revision
686fe513b2 Removed strategies for Naxxramas until they can be rewritten without core changes (#1961)
Merging this isn't required but rather to provide an option to core
changes.

The scripts can be written from scratch when someone feels like doing it
without touching the core.

This shouldn't be merged unless
https://github.com/mod-playerbots/azerothcore-wotlk/pull/130 is merged
too.
2026-01-03 14:50:35 +01:00
Revision
61402e83a1 Fixed filename (#1963)
Added missing underscores to the filename
2026-01-03 14:47:31 +01:00
Crow
b16789fa54 Remove "potion" strategy from arenas + code cleanups (#1922)
Lines 481 through 484 are the only substantive changes. Bots were having
the potion strategy added in arenas, and since potions are not allowed
in arenas, this caused them to lock up and attempt to repeatedly drink
potions when under the applicable health or mana trigger thresholds. Now
they won't have the potion strategy in arenas.

Otherwise, I noticed a bunch of magic numbers for spec tabs and so went
ahead and did some refactoring:
1. All references to spec tab numbers now use the appropriate constant
name.
2. A bunch of extra braces were deleted.
3. DEATHKNIGHT_TAB was changed to DEATH_KNIGHT_TAB, and
HUNTER_TAB_BEASTMASTERY was changed to HUNTER_TAB_BEAST_MASTERY, to
reflect the correct names of the class and spec.
4. Deleted some comments that were clearly unneeded. There's much more
that can be cleaned up, and probably the entire logic sequence for
adding/removing strategies in AiFactory.cpp can be redone, but that's
above my pay grade.
2025-12-30 21:17:14 +01:00
BeardBear
8f638b6a66 Add secure login handling for playerbots (#1953)
Introduces PlayerbotsSecureLoginServerScript to handle secure login
scenarios for playerbots. Ensures that if a playerbot is already online
when a login is attempted, it is properly logged out before allowing the
new session. Registers the new script in the Playerbots initialization.
2025-12-30 21:13:36 +01:00
Keleborn
33f5e733dc feat. Enable bots to respond to GM messages. (#1909)
I found an existing config option that allows bots to speak without a
master, and extended its use so that protected commands (nc, co) are
taken from a bot by GM players. This is primarily a debugging tool and
shouldnt be on generally.

My testing of this is the following. 
With the option off, no error is seen, but no text output is shown. 

With the option on 
GM accounts (Whether GM is on or off) will respond to nc and co commands
by just saying the output directly. Strategies can be changed by GMs for
bots whom they are not masters for (Note, im pretty sure that a GM could
change the strategies even before this change, you just couldnt see the
output of that.)

For non GM characters you get "Invite me to your group first" with the
option on/off.
2025-12-30 00:53:37 +01:00
Keleborn
9917863ca1 Feat. Add Fishing action and fish with master. (#1433)
### Update :Thank you to @notOrrytrout from prompting me to work on
this. Its been a huge learning experience.

With @notOrrytrout I started working on enabling bot fishing with
master, but also on their own.
The first commit didnt crash, showing that it was possible to have a bot
cast when master does. Currently it compiles but crashes when you try to
fish with a bot in the group, whether the bot has fishing or not. It
makes me think that the check in FishingValues is broken somehow, but I
cant figure out how.

---------

Co-authored-by: bash <31279994+hermensbas@users.noreply.github.com>
2025-12-27 19:50:18 +01:00
bashermens
2317652d72 Full revert: no xp bot when master has xp disabled (#1948)
Will reimplement the feature later in time.
2025-12-25 03:00:21 +01:00
bashermens
1fcd6c5cda Quickfix: performance problems (XP gain when master has XP turned off) (#1947)
Disable bot XP gain when master has XP turned off feature has a big
performance impact.

Instead of reverting placed back to original code before the feature
PR's for now as quickfix
2025-12-25 02:07:59 +01:00
privatecore
88016789ba Quick fix for CMSG_FORCE_MOVE_ROOT_ACK and CMSG_FORCE_MOVE_UNROOT_ACK (#1937)
**Original issue:**
https://github.com/mod-playerbots/mod-playerbots/issues/1902
**Original cause:** charmed bot (with lost client control) got rooted at
the same time.

**How to reproduce:**
1. Spawn creatures 11350 (x3) and 11830 (x3) using the command: `.npc
add <entry>` in a quiet place.
2. Create any party with random bots or alt bots (should be 60-65
levels), make sure there is at least one healer.
3. Set the healer's mana to a high value, like 100M, using command:
`.mod mana <value>`.
4. Start the fight while continuously respawning creatures with: `.resp
all`.
5. When console starts to spam 'heartbeat' messages, check your party
members' movement flags to identify which one has `MOVEMENTFLAG_ROOT`
0x00000800 (2048) using the command: `.debug move`.

This PR will not fix ALL 'heartbeat' issues, as
`ServerFacade::SetFacingTo` still sends `SendMovementFlagUpdate` while
bots can have the `MOVEMENTFLAG_ROOT` flag.
2025-12-25 00:01:42 +01:00
bashermens
6be860c967 [Stability] Various crash fixes based on Regrad fixes and crashlogs. (#1928)
These contains various fixes, fixes that have history worked one in past
more then once as person as group, aswell @Wishmaster117. But due
various reasons we had to drop them due priority or simply timewise.
These fixes have recollected again by @Regrad based on his crash logs.

Most crash logs we have, i am talking 30+ of them, to many to post here.
@Regrad running a larger server 100+ real players with bots, which means
he will walk into issues that most of us wont or are extremely difficult
to reproduce.

@Regrad used LLM to solve them based on crash log and mentioned his
server crashes almost disappeared, instead of redoing every single PR
and pull them apart. I tried to keep his bunch of changes together as
whole, reviewed them, some redone, verified again etc etc. This is not
how would normally do this. But since i want @Regrad being able to
confirm, we need this in a package as a whole. Pulling them apart in the
current situation is simply to much, to complicated in the verification
process.

So this PR is open and in my opinion has priority above others, but
@Regrad is only person who can give the green light for the
mod-playerbot changes for now.

I, we spend huge amount of time into these issues over last couple of
months. I will put other PR's on hold for abit.

---------

Signed-off-by: Engardium <regradius@gmail.com>
Co-authored-by: Engardium <regradius@gmail.com>
2025-12-24 13:24:29 +01:00
Alex Dcnh
9971622093 Core - Fixe raid markers persists after target dead causes issue with bots running off (#1845)
This PR fixes #1833 where bots could keep chasing a raid target icon
(usually skull) across very large distances or even different maps after
the mark was set. It replaces the previous attempt with a simpler design
that keeps values generic and moves context logic into triggers/actions.

Changes

- RtiTargetValue now ignores RTI targets that are farther than
sPlayerbotAIConfig->sightDistance from the bot or the master (same map
only), while still using AttackersValue::IsValidTarget and LOS checks.
- AttackersValue is reverted to its original behavior (no special RTI +
IsInCombat logic).
- RTI triggers only react to "rti target" in combat, as suggested in
review (“that condition should be check in trigger…”).
- AttackRtiTargetAction keeps a small local fallback: if "rti target" is
null, it resolves the raid icon directly from the group so chat commands
like “attack rti target” still work, including out of combat.

Behavior

- Bots no longer run across the world to chase a stale skull far away
from the group.
- When the group comes back near the marked mob (within sight distance),
the skull is again used as a normal focus hint.
- Automatic RTI behavior is limited to combat via triggers, while
explicit chat commands still work out of combat.

Testing

- Marked a mob with skull, killed it, moved far away / changed zone,
then let it respawn: bots did not chase it from afar.
- In a normal dungeon/raid pack, bots still focus skull in combat.
- Passing near a previously marked mob: bots only react once it is back
within sight distance.

---------

Co-authored-by: Keleborn <22352763+Celandriel@users.noreply.github.com>
2025-12-23 20:42:29 +01:00
Crow
895df9b197 Magtheridon timer keys fix (#1936)
Same deal as Karazhan--changing map id to instance id for timer keys to
prevent conflicts from multiple groups (plus other tweaks with respect
to the timers). This strategy could use a broader refactor, but that's
for another day--this PR is just to address the timer logic.
2025-12-23 09:04:36 +01:00
Crow
467b63b840 Fix Karazhan timers (#1926)
I'm not 100% sure but think that the current implementation of timers
for Attumen, Netherspite, and Nightbane with the instance's map id as
the key would result in the timers being shared across separate raids
running the instance at the same time. I've refactored to use the
instance id as the key for all shared timers so to the extent this was
an issue, it shouldn't be anymore.

I also made some minor tweaks to the tank positioning for BBW and
Curator as I noticed there was some weirdness in the logic that I
neglected to address with the refactor.
2025-12-21 21:40:19 +01:00
HennyWilly
66f5f597bb Re-enable no-xp-feature with better checks (#1935)
The feature `Disable bot XP gain when master has XP turned off` (#1910)
was unintentionally disabled in #1929.
This PR re-enables it with better checks in order to prevent crashes
when playing together with the individual progression module.
2025-12-21 21:28:39 +01:00
privatecore
cafbd4681e Hotfix after recent changes related to the refactoring of IsMovingAllowed (#1933)
Hotfix for the issue where bots can't enter/move on transports
(elevators, zeppelins, ships, etc.). Change the logic to determine if
bot is on a vehicle. According to the current implementation in
AzerothCore, `GetVehicle()` is the most common approach for this.
Additionally, other checks related to the vehicle: `GetBase`, `IsAlive`,
`GetVehicleInfo`, and `GetSeatForPassenger` -- are processed inside
`IsInVehicle`.

This should be more than enough to determine if the bot is on a vehicle
and can/cant control it.

Issue: https://github.com/mod-playerbots/mod-playerbots/issues/1927
2025-12-20 23:51:32 +01:00
bashermens
f5c84ee7ff server_crash_fix: OnPlayerGiveXP (#1929)
[fef0ed4072c8_worldserver.exe_.18-12_23-30-44.txt](https://github.com/user-attachments/files/24258638/fef0ed4072c8_worldserver.exe_.18-12_23-30-44.txt)

- Just added common logic and defense coding
- Optimized isRandombot while at it

Just for clarification i am aware the trigger of bug lies with the
progression mod. Regardless it should not happen.
2025-12-19 23:08:18 +01:00
Keleborn
b6f882886d FixListSpellsWithCache (#1931)
This is based off of Wishmasters rewrite of spell PR. #1912 and #1843,
and partial #1918
I created a new cache singleton with the required code to reference as
needed.
I clarified some variable names for additional clarity in how they
functioned.

This requires a wiki update to better describe the functionality that
was already defined in code.

Commands
Spells - Returns all spells
Spells <Profession> Returns only the spells in that profession
+<profession> Returns only the recipies that the bot has materials to
craft
<profession> `x - y` Craftable items within those levels
<profession> <slot> e.g. Chest, returns craftable items within that
slot.

Its messy whether what combinations work for commands, but fixing that
will come when bot professions are enabled.

Edit:
To test you can teach a bot various professions by going to a trainer
with them.
Using gm command .setskill you increase their skill level and with
maintenance teach the commands.

From wishmasters PR he detailed various commands to test

spells
spells first aid
spells tailoring
spells 20-40
spells +tailoring
spells head

---------

Co-authored-by: Wishmaster117 <140754794+Wishmaster117@users.noreply.github.com>
2025-12-19 23:08:01 +01:00
HennyWilly
c1222da8b0 Disable bot XP gain when master has XP turned off (#1910)
Addresses #1846

This PR disables XP gain for bots whose non-bot master has disabled XP
gain via an "Experience Eliminator" NPC.

If the current master has disabled XP gain, randombots in the group and
addclass-bots belonging to the player won't gain XP.
Randombots not grouped with a player will continue to gain XP as normal.

Discussed points:
- `Should this feature only be enabled via a new configuration value?`:
Comments currently tend towards no config value.

Open points:
- Should bots be allowed to gain XP until they reach the player's level,
even if the player has disabled XP gain?
2025-12-16 22:23:34 +01:00
NoxMax
00cb177c86 Fix: Allow bots to duel in PVP prohibited areas (#1906)
Noticed that if you ask a bot to duel in a PVP prohibited area, it will
accept, and do nothing. I thought about making the bot reject the
request, but if you (the real player) want to duel with it, the duel
should happen. This is just a minor fix to allow bots to duel if you ask
them to in such areas.

Tested with bots in party, random bots of the same faction, and random
bots of the opposite faction. All behaved the same before and after fix.
An example place to test is Zim'Torga in Zul'Drak which is by default is
a PVP prohibited area.

- Before fix, you challenge a bot, they accept and turn red, then they
either just stay where they are or wander off.

- After fix, bot attacks you within the PVP prohibited area when the
duel starts.
2025-12-16 18:38:50 +01:00
privatecore
5f697e806e Rerwite is moving allowed logic + fix root flag heartbeat spam (#1908)
Okay, what have been done:

1. Fix heartbeat spam for root flag: check against `MOVEMENTFLAG_ROOT`
flag (`IsRooted`) instead of `HasRootAura`.
2. Rewrite `IsMovingAllowed` - place checks from most common to the
rarest.
3. Remove unnecessary checks: `HasRootAura`, `HasConfuseAura`,
`HasStunAura` - handled by AuraEffects and set unit state flags
`UNIT_STATE_ROOT`, `UNIT_STATE_CONFUSED`, `UNIT_STATE_STUNNED` -
`UNIT_STATE_LOST_CONTROL` already handles confused and stunned (rooted
checked with `IsRooted` method).
4. Combine traveling state checks for taxi flights:
`UNIT_STATE_IN_FLIGHT` + MM flag `FLIGHT_MOTION_TYPE`.
5. Simplify check against being in vehicle: use
`MOVEMENTFLAG_ONTRANSPORT` as an indicator that the unit is in the
vehicle.

Also, update `UpdateMovementState` method with simplified checks and the
updated logic (common > rare).

This should fix issues:
https://github.com/mod-playerbots/mod-playerbots/issues/1903 and
https://github.com/mod-playerbots/mod-playerbots/issues/1902

NOTE: The `PlayerbotAI` class has a method `CanMove` with the same
checks, but this method is only used once in the code. We should decide
how to properly check if the bot can move or not:

1. Place all logic into `IsMovingAllowed` and drop `CanMove`.
2. Place all logic into `CanMove` and use it inside `IsMovingAllowed`.
3. Use them for different approaches: 
- `CanMove`: simple checks (unit flags, CC state, death state, travel
state, vehicle state);
- `IsMovingAllowed`: everything from `CanMove` + MM flags checks (not
sure about rooted since it still checks for movement flags...).
2025-12-15 15:32:49 +01:00
Crow
934e73ae20 Add Ogri'la and Blackwind Landing to PvP Restricted Areas (#1915)
This PR adds a couple of neutral quest hubs in Outland to PvP restricted
areas (and makes a couple of very minor formatting fixes to the Karazhan
files).

3786: Ogri'la
3973: Blackwind Landing (Sha'tari Skyguard quest hub in Skettis)
2025-12-15 15:26:38 +01:00
Crow
f4b4d8967f Karazhan Refactor + Nightbane Strategy (#1847)
This PR completely refactors the Karazhan raid strategies to clean up
the code and polish up/introduce new strategies, including for
Nightbane.

General changes with respect to the code:
- Moved gating checks for action methods to triggers instead of relying
on isUseful functions
- Got rid of the nonsensical helper class I made and switched to a
helper namespace
- Broke up some longer action classes into separate methods and/or
private members
- Deleted/consolidated some excess code
- Made greater use of early returns
- Renamed methods, multipliers, and triggers to make them easier to
understand
- Generally made edits to conform with AC code standards

Below are the implemented strategies. I’m including all of them, not
just new/modified ones, because I don’t think the first set of
strategies was ever tested. So each boss could probably use complete
testing.

### **Trash**

I added strategies for a trash mob that I find particularly annoying:
- Mana Warp: These are, IMO, the most annoying trash mobs in Karazhan.
They blow up when low on health, and having two blow up in quick
succession is enough to take out a decent chunk of your raid. The
strategy directs bots to use pretty much every stun in the book on Mana
Warps when they’re low on HP, which is the only way to prevent the
explosion.

### **Attumen**

- During the first phase, bots will focus on Midnight (taking either
Midnight or Attumen to 25% starts the next phase). When Attumen spawns,
the assist tank will pick him up and move him away so that bots don’t
get cleaved.
- When Attumen mounts Midnight, starting phase 2, threat is wiped, and
bots will pause DPS for a few seconds to allow the main tank to get
aggro. All bots, other than the main tank and any bot that pulls aggro,
will stack behind Attumen (~6 yards for ranged so Hunters can still
attack).

### **Moroes**

- As before, bots will mark and prioritize adds and the boss in the
recommended kill order: Dorothea, Catriona, Keira, Rafe, Robin, Crispin,
and Moroes. In practice, the enemies will probably be stacked up, and
the bots will AoE them down in accordance with their typical AoE
strategies, but classes without AoE capabilities should still prioritize
the skull.
- Based on testing feedback, added a method for the main tank to
prioritize Moroes

### **Maiden of Virtue**

I’ve made only minor changes to Revision’s original strategy here. 

- The tank with aggro will position Maiden in the middle of the room and
move her to a healer when Repentance is cast so that the Holy Ground
will break the healer’s stun.
- Ranged bots have assigned positions between the columns around the
middle of the room (to prevent chain damage from Holy Wrath).

### **The Big Bad Wolf**

- The tank with aggro brings the boss to the front left of the stage. 
- If a bot gets turned into Little Red Riding Hood, it will run around
the stage in a counter-clockwise rectangle until the transformation
fades. I tweaked this strategy a bit; it's still not perfect, but it
works better than before.

### **Romulo and Julianne**

There are no substantive changes to this strategy. As before, in phase
3, when both bosses are active, the bots switch back-and-forth between
the bosses by alternating the skull icon based on which boss has lower
HP (10% differential).

### **The Wizard of Oz**

There are no substantive changes to this strategy. As before, bots mark
the bosses with a skull icon in their recommended kill order: Dorothee,
Tito (assuming he spawns before you kill Dorothee), Roar, Strawman,
Tinhead, and the Crone. Additionally, Mages will spam Scorch on Strawman
to daze him.

### **The Curator**

- The tank will drag the boss to a fixed spot down the hallway. Ranged
bots will spread out to avoid chain damage from Arcing Sear, and bots
will mark Astral Flares with the skull icon to prioritize them down.
- Those strategies already existed, but now I made the assist tank also
focus on the boss to try to stay second in aggro and therefore absorb
Hateful Bolts.
- Added a multiplier to save Bloodlust/Heroism until Evocation.

### **Terestian Illhoof**

There are no substantive changes to this strategy. The bots will mark
targets with the skull icon in the following order: Demonic Chains,
Kil'rek, and Illhoof.

### **Shade of Aran**
I redid the strategies a bit, and I think (hope) they work better.

- Flame Wreath: Bots will stop moving until the aura fades. There is a
bug in which Flame Wreath will sometimes persist long beyond its
supposed 20-second duration. If Aran casts Arcane Explosion during this
time, you will almost certainly wipe, so that’s frustrating. I made it
so that bots will stay in place still and eat the Arcane Explosion
because it’s the lesser of two evils, and if you are overgeared, you may
be able to survive. In the previous strategy, bots would stop actions
entirely, but now they should keep attacking/casting without moving.
- Arcane Explosion: No substantive changes here--bots will run out
immediately and stay out of range until the cast finishes.
- Conjured Elementals: No substantive changes here--they will be marked
one-by-one by the skull icon, except that the marking will skip any
elemental that is banished.
- Ranged Positioning: I redid this strategy. Ranged bots will now
maintain a distance between 11 and 15 yards from the boss. This keeps
them out of the 10-yard radius in which Aran silences while also keeping
them from getting too far away and getting stuck in the alcoves.

### **Netherspite**

I significantly refactored the action methods here, but substantively
the original strategy remains mostly intact.

- Red (Tank) Beam: One tank will be assigned to block the beam for each
Portal Phase. The assigned tank will dance in and out of the beam (5
seconds in, 5 seconds out). Tanks intentionally do not avoid Void Zones
(it was the lesser of two evils for them to take that damage vs. trying
to dynamically avoid them, moving the boss, and possibly getting
everybody out of position.
- Blue (DPS) Beam: DPS other than Rogues and Warriors are eligible to be
assigned (one-by-one) to block this beam. When the assigned blocker
reaches 25 stacks of the debuff, they will leave the beam, and the next
assigned blocker will take their place. If a Void Zone drops under the
assigned blocker, the bot will move along the beam to get out of the
Void Zone so that they do not stop blocking.
- Green (Healer) Beam: This works the same way as the Blue Beam, except
that eligible blockers are healers, Rogues, and DPS Warriors. Healers
that are assigned to block will swap in the same way as Blue Beam
blockers. If a Rogue or DPS Warrior is the assigned blocker, however,
they will stand in the beam for the entire Portal Phase since they do
not suffer any adverse effects from the beam. In this PR, I made the
strategy prioritize Rogues and DPS Warriors over healers to try to avoid
the need for bots to swap (and to avoid the irritating scenario in which
a healer would block the beam for the first half of a phase and then tag
in a Rogue or DPS Warrior, which would be wasted by blocking only half
of a phase).
- Non-Blockers: They will stay at least 5 yards away from each beam
until called to be an assigned blocker. They will also avoid Void Zones.
- Banish Phase: The only strategy I implemented was for bots to avoid
residual Void Zones from the Portal Phase.
- Phase Transitions: Bots should pause DPS at the beginning of the
encounter and whenever Netherspite transitions back from the Banish
Phase to the Portal Phase (which is an aggro reset). Note that this
doesn't wipe DOTs, and there's not much I can do about that.

### **Prince Malchezaar**

The action methods are significantly refactored, but the strategy
substantively is not changed very much.

- Bots will maintain distance from Infernals. The tank has a larger
avoidance radius to give DPS a little bit of margin to work with.
Depending on Infernal placement, it is possible for bots to get stuck in
some bad positions. In that case, the best solution is to put them on
“flee” and lead them to a better position.
- Bots that get Enfeebled will run out of Shadow Nova range. They should
pick a path that does not cross within any Infernal's Hellfire radius.
- Added a multiplier to save Bloodlust/Heroism until Phase 3.

### **Nightbane**

**Disclaimer**: Bots are terrible at this encounter, in large part
because the map is awful (even before the recent Core changes). So the
strategies are not ideal because they need to operate within the bots’
limitations. I STRONGLY suggest you clear the entire Livery Stables
(including the upper level) because the mobs in them have a high risk of
pulling through the floor of the Master’s Terrace. Ideally, you should
clear out the Scullery too.

The strategy uses waypoints toward the Northeastern door to the Master’s
Terrace. I tried several different locations, and that worked best for
me based on where Nightbane lands (note that he has a different landing
spot for the encounter start vs. the start subsequent ground phases).

- Ground Phase, main tank: The main tank uses two waypoints after it
picks up the boss—the movement pattern should be kind of like a reverse
checkmark, where the tank moves back along the inner edge of the
terrace, then pivots and moves at a bit of an angle to the outer edge. I
did this as a way to get the tank to face Nightbane sideways across the
terrace, which is how he’s supposed to be tanked. The main tank will not
get out of Charred Earth. This is intended. The tank cannot move
dynamically enough to avoid it while also not turning the boss and
wiping the raid.
- Ground phase, ranged: Ranged bots rotate between three waypoints. They
start stacked at the same position. If Charred Earth is dropped on that
position, the bots will rotate to the second position. If Charred Earth
is dropped on that position, they will rotate to the third position. The
maximum number of Charred Earths that can be active is two, so if one is
dropped on the third position, the ranged bots should rotate back to the
first position.
- Ground Phase, other melee: Melee bots have no coded Charred Earth
avoidance strategy. They do decently enough with the general “avoid aoe”
strategy.
- Flight Phase: Bots move to Nightbane’s flight position—Nightbane is
bugged and cannot be attacked in the air in AC, but all bots still need
to stay near him or he will wipe the raid with Fireball Barrage. Bots
stack on the same position; when Rain of Bones is cast (one time,
snapshotted on the target’s location), all bots will move away to a
second nearby position. They will then kill the Restless Skeletons. The
Flight Phase lasts for 45 seconds, but Nightbane emotes after 35 seconds
and goes to land—during the final 10-second period, bots are freed from
all strategies and will follow the master. You need to lead them back to
the Northeastern part of the terrace before Nightbane lands so that bots
can get immediately positioned when he lands.
- Phase Changes: Whenever Nightbane lands, bots should pause DPS for the
main tank to get aggro.
- Managing bots/pets: During the Flight Phase, bots and pets have a
tendency to chase Nightbane outside of the boundaries of the map and
pull mobs from other parts of the instance as well as cause Restless
Skeletons to spawn out of bounds (and then further cause bots/pets to go
out of bounds to attack the skeletons). The strategy should solve for
this, but if there are still issues, it should be noted. My resolution
was to implement the following: (1) when Nightbane takes flight, bots
stop attacking and mark him with the moon icon, and Hunters and Warlocks
put pets on passive and recall them (they will put pets on defensive
again when Nightbane lands), and (2) all temporary pets are disabled for
the duration of the fight (Water Elementals, Shaman wolves, Druid
treants, etc.).

**Known Issues:**

- The approach to getting Nightbane to turn sideways is not always spot
on since it’s a workaround to account for what should be dynamic
movement. He can end up at a bit of an angle. I’ve tweaked the positions
such that if he is at an angle, it should not be a situation in which
any ranged position is exposed to Smoldering Breath or Cleave. That does
increase the risk that poor positioning may expose a ranged position to
Tail Sweep. This is obviously suboptimal, but that attack is much more
survivable.
- Ranged bots move between the three waypoints by reading the presence
of the Charred Earth aura on themselves. Sometimes, a bot may reach the
next waypoint with the Charred Earth aura from the previous waypoint
still on them (depending on timing of ticks), in which case the bot will
keep moving to the next waypoint. This can cause an issue in which the
ranged group gets split. So you could have Charred Earth dropped on
position 1, and some ranged bots will go to position 2 but others will
go to position 3, and then if the second Charred Earth is dropped on
position 3, the bots at position 3 will move back to position 1 (which
is still in Charred Earth) before then moving to position 2. Balancing
spacing between waypoints, positioning with respect to the boss, line of
sight, and maximum distance is very difficult to do. I messed with the
positioning a lot, and I am not sure if this possibility cannot be
entirely avoided without creating other issues. I have at least reached
a result in which I have not seen any bots cycle indefinitely (though if
testing observes this, it is obviously a problem).

Ultimately, I wouldn’t say I’m totally satisfied with the strategy.
Wipes are still possible on account of bad RNG. But I think it does make
the fight a lot more manageable, as it is a fight that is very difficult
with IP nerfs in the absence of any strategy. Previously, I needed to
bring 3 healers, and they would still all be out of mana by the first
Flight Phase and reliant on MP5 for the remainder of the fight, and I
would wipe multiple times before a kill.
2025-12-10 21:08:25 +01:00
Nicolas Lebacq
910b8a9c53 fix: Made bots roll in a more reasonable time on group loots. (#1857)
# Description

This PR changes the way loot rolls are being evaluated.

It puts a maximum priority on the loot action so it does not hang for so
long.
2025-12-09 10:29:57 -08:00
Tecc
bb569b4d39 Fix: Arena - PersonalRating and MMR issue for bot teams (#1789)
# Fix: Arena PersonalRating and MMR issue for bot teams

## Problem
Bot arena teams are created with artificial random ratings (1000-2000
range), but when bots join these teams, their personal ratings and
matchmaker ratings (MMR) use default config values instead of being
adjusted to match the team's artificial rating. This causes matchmaking
issues since the system uses personal ratings for queue calculations.

## Root Cause
The issue occurred because `SetRatingForAll()` was called during team
creation but only affected the captain. When additional bots were added
later via `AddMember()`, they received default values from
`CONFIG_ARENA_START_PERSONAL_RATING` and
`CONFIG_ARENA_START_MATCHMAKER_RATING` instead of values appropriate for
the team's artificial rating.

## Solution
After bots are added to arena teams, the fix:

1. Uses `SetRatingForAll()` to align all personal ratings with team
rating
2. Adjusts matchmaker ratings based on team context vs default
configuration
3. Saves changes to both database tables with proper data types

## Impact
- Personal ratings now match team ratings for artificial bot teams
- MMR values are adjusted for artificial bot team ratings instead of
using default config values
- Arena matchmaking functions correctly for bot teams with random
ratings
- Only affects new arena team assignments after deployment
- Existing player teams and normal config behavior are unaffected

## Manual Database Update

For existing installations, the provided SQL script could be used to fix
bot teams created before this patch.

### Update personal rating
```sql
UPDATE arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
SET atm.personalRating = at.rating
WHERE a.username LIKE 'rndbot%'
  AND atm.personalRating != at.rating;
```

### Update MMR for existing entries
```sql
UPDATE character_arena_stats cas
JOIN characters c ON cas.guid = c.guid
JOIN auth.account a ON c.account = a.id
JOIN arena_team_member atm ON cas.guid = atm.guid
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
SET
    cas.matchMakerRating = GREATEST(at.rating, 1500),  -- Use team rating or 1500 minimum
    cas.maxMMR = GREATEST(cas.maxMMR, cas.matchMakerRating)  -- Update maxMMR if needed
WHERE
    a.username LIKE '%rndbot%'
    AND (
        -- Update if MMR doesn't match team context
        (at.rating > 1500 AND cas.matchMakerRating < at.rating) OR
        (at.rating <= 1500 AND cas.matchMakerRating != 1500) OR
        cas.matchMakerRating IS NULL
    )
    AND (
        -- Map arena team type to character_arena_stats slot
        (at.type = 2 AND cas.slot = 0) OR  -- 2v2 teams use slot 0
        (at.type = 3 AND cas.slot = 1) OR  -- 3v3 teams use slot 1
        (at.type = 5 AND cas.slot = 2)     -- 5v5 teams use slot 2
    );
```

### Insert missing MMR records for bots without character_arena_stats
entries
```sql
INSERT INTO character_arena_stats (guid, slot, matchMakerRating, maxMMR)
SELECT
    atm.guid,
    CASE
        WHEN at.type = 2 THEN 0  -- 2v2 -> slot 0
        WHEN at.type = 3 THEN 1  -- 3v3 -> slot 1
        WHEN at.type = 5 THEN 2  -- 5v5 -> slot 2
        ELSE 0
    END as slot,
    GREATEST(at.rating, 1500) as matchMakerRating,
    GREATEST(at.rating, 1500) as maxMMR
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
WHERE
    a.username LIKE '%rndbot%'
    AND NOT EXISTS (
        SELECT 1 FROM character_arena_stats cas2
        WHERE cas2.guid = atm.guid
        AND cas2.slot = CASE
            WHEN at.type = 2 THEN 0
            WHEN at.type = 3 THEN 1
            WHEN at.type = 5 THEN 2
            ELSE 0
        END
    )
    AND at.rating > 0;
```

## Related issues

Fixes: #1787 
Fixes: #1800


## Verification Queries

### Query 1: Check personal rating alignment
```sql
SELECT
    'Personal Rating Check' as check_type,
    COUNT(*) as total_bot_members,
    SUM(CASE WHEN atm.personalRating = at.rating THEN 1 ELSE 0 END) as correct_ratings,
    SUM(CASE WHEN atm.personalRating != at.rating THEN 1 ELSE 0 END) as incorrect_ratings,
    ROUND(AVG(at.rating), 2) as avg_team_rating,
    ROUND(AVG(atm.personalRating), 2) as avg_personal_rating
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
WHERE
    a.username LIKE '%rndbot%';
```

### Query 2: Check MMR alignment
```sql
SELECT
    'MMR Alignment Check' as check_type,
    COUNT(*) as total_mmr_records,
    SUM(CASE
        WHEN at.rating > 1500 AND cas.matchMakerRating >= at.rating THEN 1
        WHEN at.rating <= 1500 AND cas.matchMakerRating = 1500 THEN 1
        ELSE 0
    END) as correct_mmr,
    SUM(CASE
        WHEN at.rating > 1500 AND cas.matchMakerRating < at.rating THEN 1
        WHEN at.rating <= 1500 AND cas.matchMakerRating != 1500 THEN 1
        ELSE 0
    END) as incorrect_mmr,
    ROUND(AVG(at.rating), 2) as avg_team_rating,
    ROUND(AVG(cas.matchMakerRating), 2) as avg_mmr,
    ROUND(AVG(cas.maxMMR), 2) as avg_max_mmr
FROM arena_team_member atm
JOIN arena_team at ON atm.arenaTeamId = at.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
JOIN character_arena_stats cas ON atm.guid = cas.guid
WHERE
    a.username LIKE '%rndbot%'
    AND (
        (at.type = 2 AND cas.slot = 0) OR
        (at.type = 3 AND cas.slot = 1) OR
        (at.type = 5 AND cas.slot = 2)
    );
```

### Query 3: Detailed team-by-team analysis
```sql
SELECT
    at.arenaTeamId,
    at.name as team_name,
    at.type as team_type,
    at.rating as team_rating,
    COUNT(atm.guid) as member_count,
    GROUP_CONCAT(DISTINCT atm.personalRating) as personal_ratings,
    GROUP_CONCAT(DISTINCT cas.matchMakerRating) as mmr_values,
    CASE
        WHEN COUNT(DISTINCT atm.personalRating) = 1 AND MIN(atm.personalRating) = at.rating THEN 'OK'
        ELSE 'MISMATCH'
    END as personal_rating_status,
    CASE
        WHEN COUNT(DISTINCT cas.matchMakerRating) = 1 AND (
            (at.rating > 1500 AND MIN(cas.matchMakerRating) >= at.rating) OR
            (at.rating <= 1500 AND MIN(cas.matchMakerRating) = 1500)
        ) THEN 'OK'
        ELSE 'MISMATCH'
    END as mmr_status
FROM arena_team at
JOIN arena_team_member atm ON at.arenaTeamId = atm.arenaTeamId
JOIN characters c ON atm.guid = c.guid
JOIN auth.account a ON c.account = a.id
LEFT JOIN character_arena_stats cas ON atm.guid = cas.guid
    AND cas.slot = CASE
        WHEN at.type = 2 THEN 0
        WHEN at.type = 3 THEN 1
        WHEN at.type = 5 THEN 2
        ELSE 0
    END
WHERE
    a.username LIKE '%rndbot%'
GROUP BY at.arenaTeamId, at.name, at.type, at.rating
ORDER BY at.rating DESC;
```
2025-12-08 12:35:06 +01:00
NoxMax
dde16674c3 Fix: Stop pets from fighting in PVP prohibited zones (#1829)
Stripped down version of #1818. No new features. Refactors
IsPossibleTarget in AttackersValue.cpp to a better style and makes sure
pets don't attack in prohibited zones.

Testing:
Confirmed that aggro pets no longer attack in PVP prohibited areas, but
still do outside them. Zim'Torga in Zul'Drak is a good example to test
this (ID 4323). Lookout for death knights with a Risen Ally
(uncontrolled and naturally aggro) now they respect PVP prohibition like
their master.

Note: If you manually teleport a bot that is in mid combat to a PVP
prohibited area, its aggro pet might still attack, because its master is
still in combat strategy. Otherwise the pet will not attack if its
master has switched to non-combat.
2025-12-08 12:34:16 +01:00
HennyWilly
e5b2791053 Improve Molten Core Strategy (#1852)
This is my first attempt of implementing playerbot strategies.

A team of 40 can steamroll Molten Core relatively easy, even with lower
item levels.
Regardless, this PR adds resistance triggers and actions to mitigate
some damage.
Additionally, improvements were made for the encounters with Garr, Baron
Geddon, Shazzrah and Golemagg.
A short summary per boss is listed below.
All planned features are included, but feedback is of course
appreciated.

### Lucifron
- Shadow resistance: mitigate damage from [Impending
Doom](https://www.wowhead.com/classic/spell=19702/impending-doom) and
[Shadow
Shock](https://www.wowhead.com/classic/spell=19460/shadow-shock).

### Magmadar
- Fire resistance: mitigate damage from [Lava
Bomb](https://www.wowhead.com/classic/spell=19411/lava-bomb) and [Magma
Spit](https://www.wowhead.com/classic/spell=19450/magma-spit).
- Like King Dred and the fraction commander (Nexus), this fight might
profit from an anti-fear strategy in order to counter
[Panic](https://www.wowhead.com/classic/spell=19408/panic). Not
implemented here.

### Gehennas
- Shadow resistance: mitigate damage from [Shadow
Bolt](https://www.wowhead.com/classic/spell=19728/shadow-bolt) and
increase the chance to resist [Gehennas'
Curse](https://www.wowhead.com/classic/spell=19716/gehennas-curse).

### Garr
- Fire resistance: mitigate damage from the Firesworn adds
([Immolate](https://www.wowhead.com/classic/spell=20294/immolate) and
[Eruption](https://www.wowhead.com/classic/spell=19497/eruption)).
- Disabled dps aoe abilities via multiplier. This one is important
because multiple exploding adds at once might delete bots rather
quick...

### Baron Geddon
- Refactored the existing strategy.
- Fire resistance: mitigate damage from [Ignite
Mana](https://www.wowhead.com/classic/spell=19659/ignite-mana),
[Inferno](https://www.wowhead.com/classic/spell=19695/inferno) and
[Living Bomb](https://www.wowhead.com/classic/spell=20475/living-bomb).
- Better Inferno handling: Before moving away, bots stop attacking and
interrupt their spells. Additionally, the new multiplier prevents bots
from running back to Geddon while Inferno is still active.

### Shazzrah
- Ranged bots now position themselves in a sweet spot that prevents them
from getting hit with [Arcane
Explosion](https://www.wowhead.com/classic/spell=19712/arcane-explosion)
but still close enough to dps and heal.

### Sulfuron Harbinger
- Fire resistance: mitigate damage from [Hand of
Ragnaros](https://www.wowhead.com/classic/spell=19780/hand-of-ragnaros)
and [Immolate](https://www.wowhead.com/classic/spell=20294/immolate). To
be fair, this one is quite negligible...

### Golemagg
- Fire resistance: mitigate damage from [Magma
Splash](https://www.wowhead.com/classic/spell=13880/magma-splash) and
[Pyroblast](https://www.wowhead.com/classic/spell=20228/pyroblast).
- Disabled dps aoe abilities via multiplier. Kind of a preference on my
side. Otherwise, the Core Ragers spam emotes about not wanting to die.

### Majordomo Executus
- Shadow resistance: mitigate damage from [Aegis of
Ragnaros](https://www.wowhead.com/classic/spell=20620/aegis-of-ragnaros),
[Shadow Shock](https://www.wowhead.com/classic/spell=20603/shadow-shock)
and [Shadow
Bolt](https://www.wowhead.com/classic/spell=21077/shadow-bolt). This one
is also negligible, TBF.

### Ragnaros
- Fire resistance: mitigate damage from [Wrath of
Ragnaros](https://www.wowhead.com/classic/spell=20566/wrath-of-ragnaros)
and [Lava
Burst](https://www.wowhead.com/classic/spell=21158/lava-burst).
2025-12-08 12:29:07 +01:00
Keleborn
353c29dfc4 Bug: Fix bots leaving LFG groups before master (#1876)
I removed bots checking if they should leave group every tick, and will
rely on the LeaveGroupFarAway action. I also increased the timer from 5
seconds to 20 seconds. No need to check this that often.
2025-12-08 12:25:40 +01:00
Keleborn
52c3e96641 Rename groupmaster to groupleader and related variables. (#1875)
Fix the naming conventions. 
Master should be reserved to identify a bots Master. 
groupleaders are not necessarily group masters and it should be clear
what the bot is looking for. (In most solo cases leader=master)
2025-12-03 13:25:01 +01:00
Crow
38e2d8584b Add missing break in ApplyInstanceStrategies (#1887)
Well, I hope nobody has tried Magtheridon lately. It looks like this got
inadvertently deleted during the merge.
2025-11-28 19:00:12 +01:00
Keleborn
d5dbc4ddd7 Hotfix: prevent server crash when whisper 'logout' (#1874)
Temp Hotfix to resolve #1870.
2025-11-24 21:49:55 +01:00
Keleborn
2424f73bc4 Core Merge PR - Replace OnPlayerChat with OnPlayerCanUseChat (#1838)
First stab at getting this working. Im not sure if Im missing something,
but it seemed to be a pretty simple change overall.

Based on testing the bots do respond to commands via whisper and group.

Edit: Relevant PR this addresses.

50f8f145d2 (diff-baadebd8cd1117ca48225f316a5ab3fd5fd55b20963394d302341147183db067)
2025-11-23 20:45:31 +01:00
Crow
cf743a186a Fix Wrong Misdirection Spell ID for Gruul's Lair and Magtheridon Strategies (#1867)
Lol oops.

Confirmed with logs/in-game that the prior one was wrong (and thus
always returning false) and current one is correct.
2025-11-23 10:06:19 +01:00
blinkysc
10213d8381 Add thread safety for group operations (#1816)
Fixes crashes and race conditions when bots perform group/guild/arena
operations by moving thread-unsafe code to world thread.

Potentially fixes #1124

## Changes

- Added operation queue system that runs in world thread
- Group operations (invite, remove, convert to raid, set leader) now
queued
- Arena formation refactored to use queue
- Guild operations changed to use packet queueing

## Testing

Set `MapUpdate.Threads` > 1 in worldserver.conf to enable multiple map
threads, then test:
- Group formation and disbanding
- Arena team formation
- Guild operations (invite, promote, demote, remove)

- Run with TSAN
cmake ../ \
  -DCMAKE_CXX_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_C_FLAGS="-fsanitize=thread -g -O1" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread" \
  -DCMAKE_INSTALL_PREFIX=/path/to/install \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo

build

export
TSAN_OPTIONS="log_path=tsan_report:halt_on_error=0:second_deadlock_stack=1"
./worldserver

The crashes/race conditions should no longer occur with concurrent map
threads.

## New Files

- `PlayerbotOperation.h` - Base class defining the operation interface
(Execute, IsValid, GetPriority)
- `PlayerbotOperations.h` - Concrete implementations:
GroupInviteOperation, GroupRemoveMemberOperation,
GroupConvertToRaidOperation, GroupSetLeaderOperation,
ArenaGroupFormationOperation
- `PlayerbotWorldThreadProcessor.h/cpp` - Singleton processor with
mutex-protected queue, processes operations in WorldScript::OnUpdate
hook, handles batch processing and validation

---------

Co-authored-by: blinkysc <blinkysc@users.noreply.github.com>
Co-authored-by: SaW <swerkhoven@outlook.com>
Co-authored-by: bash <hermensb@gmail.com>
2025-11-21 21:55:55 +01:00
bashermens
d97870facd fix: warning updating movement flags while rooted (#1858)
fixes https://github.com/mod-playerbots/mod-playerbots/issues/1854
-----
Also includes fixes for:
-----
* Bots swimming with waterWalk kept switching between swimming and
walking, as result jittering effect swimming under water when water
walking active
* Bots flying close above water they would land on water and start
walking, now they stay flying unless on solid ground they will land and
start walking by design

-----
Moved all flag setting to updateMovementState:
* So all movement flag are handled in updateMovementState which also
contains the restricted movement logic.
* Handle restricted movement logic and preventing SendMovementFlagUpdate
while being restricted.


-----
Known issue when flying the following bots feel a bit jittering, wont
touch for now at least till core movement changes quirks has been dealt
with.

The current code is the extended version of what is originally was
before core merge with refactored movements. Once the core movement
refactors are settled a bit more i would like to revisit this code; as i
would expect more imperative code and less manual flag setting e.g.
bot->SetWaterWalking, SetGravitiy..SetCanFly etc.
2025-11-21 20:45:23 +01:00
Alex Dcnh
0c1700c117 CORE - Improved language detection for bots (#1784)
I've had this problem for a long time, my bots only speak English even
though I'm playing on a French client.
I suppose this must be the case for some other people who do not have a
large number of players with the same local client.
If we use French DBCs, the bots bug because they only recognize US DBCs.
From what I understand, the language is chosen as follows:
On load, the module reads the entire `ai_playerbot_texts` table and
stores each text variant in a dictionary indexed by the locale ID: the
`text` column remains the default value (English), and the `text_loc1`
to `text_loc8` columns fill slots 1 through 8.

Whenever a real player connects, the module increments a counter for
that player's DBC locale using
`AddLocalePriority(player->GetSession()->GetSessionDbcLocale())`.

When a bot needs a text, `GetLocalePriority()` returns the most
frequently used locale index among currently connected players. The
corresponding string is then retrieved. if the box is empty, we fall
back to the English version (text[0]).

### This PR improve language detection.
**Summary**

- log both the client DBC locale and the account database locale when a
player logs in
- fall back to the account locale when the client reports enUS but the
account is configured for another locale
- keep the existing vote-based selection so bots always speak the
majority language among connected players

**Therefore, the original behavior is maintained. Bots still choose the
most represented language among connected players (the counter is simply
more efficient by prioritizing the account's locale when it differs from
the client's English). For example, if more English-speaking players are
connected, the language will revert to English, as the bots always share
the majority locale.**
2025-11-21 15:56:03 +01:00
Alex Dcnh
0b1b0eaecc Core - Fix RTSC SeeSpellAction crash on malformed WorldPacket (#1841)
## Summary

This PR fixes the Crash 1 Source from Issue
[#1840](https://github.com/mod-playerbots/mod-playerbots/issues/1840)
posted in a @Regrad posted logs, in `SeeSpellAction::Execute` when an
RTSC "see spell" event arrives with an empty or malformed `WorldPacket`.
In that case the code used to read from the packet without any
validation, causing a `ByteBufferException` and a crash in the map
thread.

## Fix

- Reset the packet read position and check that the RTSC header
(castCount + spellId + castFlags) fits into the packet before reading.
- Wrap `SpellCastTargets::Read` in a `try { } catch (ByteBufferException
const&) { }` block so truncated RTSC payloads are handled gracefully.
- Check that `targets.GetDst()` is not `nullptr` before accessing its
position.

For valid RTSC packets the behavior is unchanged; malformed packets are
now safely ignored instead of crashing the server.

## Testing

- Sent bots to multiple locations using RTSC and verified they still
move as before.
- Reproduced the previous crash scenario with malformed RTSC packets:
the worldserver no longer crashes and the event is simply ignored.

---------

Co-authored-by: bash <hermensb@gmail.com>
Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com>
2025-11-21 13:18:14 +01:00
Gonzalo
8e03371147 Balance-Druid-improve-Starfall-usage-and-add-CC-safety (#1713)
- Move Starfall from default actions to AOE strategy only
- Require 2+ enemies for Starfall usage (prevents single-target casting)
- Add CC safety: avoid casting Starfall near current CC targets
- Prioritize Starfall over Hurricane in medium AOE situations
- Remove Starfall from pull/opener rotation to prevent early
single-target usage

This prevents Balance druids from wasting Starfall on single targets
and breaking crowd control effects in group content.
2025-11-19 21:00:59 +01:00
SaW
27311b734d Revert "feat: Improve bot mount behavior to faster close distance between bot and master" (#1855)
Reverts mod-playerbots/mod-playerbots#1760

This, as it is causing issues in BG, where bots just don't dismount and
just stand there instead.

<img width="2336" height="1374" alt="image"
src="https://github.com/user-attachments/assets/b61d7a77-1561-4f05-a438-edbb9321e113"
/>
2025-11-18 20:06:24 +01:00
SaW
bb5ed37cd3 Update codestyle_cpp.yml for review events and concurrency (#1836)
Include ready_for_review type, to run when draft is converted to ready.

Include concurrency; cancels obsolete tasks on new commits.
2025-11-18 18:48:52 +01:00
Jay
e88c1b779b Minor README.md corrections (#1849)
- Removed double "is" in acknowledgements
- Made the acknowledgements section more grammatical by using "based on"
instead of "based off"
- Corrected misspelling of "implemented"
- Standardized instances of "Playerbots" to `mod-playerbots` or properly
capitalized `Playerbots`
- Minor change of "for the continued contributions" to "for their
continued contributions" in the Acknowledgements section
2025-11-18 18:08:16 +01:00
Tecc
05057ae9b5 feat: Improve bot mount behavior to faster close distance between bot and master (#1760)
# Fix bot mount behavior when master dismounts

## Summary

Improves bot mount/dismount logic to ensure better coordination with the
master player. The bot now remains mounted when closing distance to a
recently dismounted master and mounts up to assist the master in combat.

The changes have been tested using the testcases described in the second
half of the PR description, which provide some directions on how this PR
can be tested and verified.

Closes: #1660

## Changes

- Add masterInCombat variable to track master combat state
- Modify target-based logic to consider master combat state
- Change mount priority when bot is not in combat to favor
master-following
- Remove combatReach from distance calculations (duplicate)

## Implementation

Added two methods:
- `StayMountedToCloseDistance()` - prevents premature dismounting when
the master dismounts
- `ShouldMountToCloseDistance()` - determines when to mount for master
assistance, even if master is not mounted at this time

Modified Execute() method:
- Target-based shouldMount/shouldDismount considers the master's combat
state
- Combat assistance logic separated from general following
- Mount when master in combat, but the bot is not

Distance logic:
- Combat: dismount at CalculateDismountDistance() (18-25 yards)
- Non-combat: dismount at 10 yards
- Mount threshold: 21+ yards

## Result
- Bots mount to assist masters in combat
- Bots stay mounted longer during travel
- Different dismount distances based on context
- Existing mount selection logic unchanged


# Mount Behavior Testing

## Prerequisites

1. Add a test bot: `.playerbots bot add <charactername>` or `.playerbots
bot addclass <class>`
2. Test in a level-appropriate outdoor area (mobs should not die with
one hit, as this makes testing combat assistance impossible)
3. Both master and bot must have mounts available

## Test 1: Combat Assistance Mounting

**Objective**: Verify bot mounts to assist the master in combat

**Detailed Steps**:
1. Position both master and bot dismounted
2. Command bot to stay: `/w <botname> stay`
3. Move master 35+ yards away from bot
4. Target a nearby mob and attack the mob
5. Command bot to assist: `/w <botname> follow`

**Expected Results**:
- Bot immediately mounts up (within 1-2 seconds cast time)
- Bot rides toward master while mounted
- Bot dismounts at ~18-25 yards from master/mob
- Bot engages in combat to assist

**Failure Indicators**:
- Bot runs dismounted (old behavior)
- Bot never mounts despite distance
- Bot mounts but doesn't dismount at proper assist range

## Test 2: Non-Combat Dismount Distance

**Objective**: Verify bot stays mounted longer during peaceful travel

**Detailed Steps**:
1. Both master and bot start dismounted
2. Mount up: use any mount
3. Verify bot also mounts: `/w <botname> follow`
4. Travel together for 10+ seconds to establish following
5. While moving, dismount master but continue running
6. Observe bot behavior as you move away

**Expected Results**:
- Bot stays mounted while master runs dismounted
- Bot only dismounts when within ~10 yards of master

## Test 3: Target Interference Prevention

**Objective**: Verify target selection doesn't prevent mounting when
master needs help

**Detailed Steps**:
1. Position bot 35+ yards from master: `/w <botname> stay` then move
away
2. Find a mob visible to both master and bot
3. Target the mob (do not attack): click on mob to select it
4. Verify bot can see the target: `/w <botname> attack` (bot should
respond about target)
5. Cancel bot attack: `/w <botname> follow`
6. Now attack the mob yourself (master enters combat)
7. Observe bot behavior immediately after master engages

**Expected Results**:
- Bot mounts up despite having the same target selected
- Bot rides toward combat area while mounted
- Bot dismounts at assist range (~18-25 yards)
- Target selection does not prevent proper mount behavior

**Failure Indicators**:
- Bot runs dismounted toward target (target interference)
- Bot doesn't mount because it's "focused on target"

## Test 4: Basic Mount Following

**Objective**: Verify fundamental mount matching still works

**Detailed Steps**:
1. Start both master and bot dismounted
2. Ensure bot is following: `/w <botname> follow`
3. Mount up on any available mount
4. Wait 2-3 seconds and observe bot
5. Test different mount speeds if available (60%, 100%, 280% speed)
6. Test dismounting and remounting

**Expected Results**:
- Bot mounts within 2-3 seconds of master mounting
- Bot uses appropriate mount speed to match master
- Bot dismounts when master dismounts (basic following)
- No regression in basic mount following behavior

## Test 5: Distance-Based Mounting Threshold

**Objective**: Verify bot mounts at efficient distance threshold (21+
yards)

**Detailed Steps**:
1. Both master and bot start dismounted in safe area (no mobs)
2. Command bot to stay: `/w <botname> stay`
3. Record starting position: `.gps`
4. Walk exactly 15 yards away (short distance)
5. Command bot to follow: `/w <botname> follow`
6. Observe: bot should run dismounted (distance too short)
7. Command bot to stay again: `/w <botname> stay`
8. Walk to 25+ yards away from bot
9. Record new position: `.gps`
10. Command bot to follow: `/w <botname> follow`

**Expected Results**:
- At 15 yards: Bot runs dismounted (inefficient to mount)
- At 25+ yards: Bot mounts up immediately (efficient distance)
- Distance threshold should be ~21 yards based on mount cast time
efficiency

**Distance Calculation**: Use coordinates from `.gps` to verify exact
distances

## Test 6: Druid Form Integration (Druid Master Required)

**Objective**: Verify druid form compatibility with mount system

**Detailed Steps**:
1. Master must be druid with travel form available
2. Start both dismounted: `/w <botname> follow`
3. Shift master to travel form
4. Observe bot response (should mount or shift if druid)
5. Travel together for 10+ seconds
6. Shift master to normal form while moving
7. Observe bot dismount behavior
8. If available, test flight form in appropriate zones

**Expected Results**:
- Druid bot: matches master's form (travel form)
- Non-druid bot: uses equivalent mount speed
- Form changes trigger appropriate bot responses
- Speed matching works between forms and mounts

## Test 7: Battleground Mount Behavior

**Objective**: Verify mount behavior works in PvP environments

**Detailed Steps**:
1. Queue for battleground with bot in party
2. Enter battleground together
3. Test basic mount following in BG
4. Test flag-carrying restrictions (WSG/EotS)
5. Test mounting during BG combat scenarios

**Expected Results**:
- Bot mounts appropriately in battlegrounds
- Flag carrying prevents mounting
- Combat assistance mounting still works in PvP
2025-11-16 22:49:12 +01:00
bashermens
610a032379 Bots fly/follow (movePoint core refactor), water walking fixes (#1825)
Closer the original solution, i dont wanna drift to much away without
really good reason. At this point i still see NPC and bots moving
through the levels or even falling out the levels here and there. I
verified whether the MovePoint signature changes and related params
itself in playerbots has anything todo with it, even when params are
hardcoded the behavior remains. It could be deeper problem, but for now
it seems core problem. Therefore i dont wanna change to much until the
dust has settled a bit in core itself.

I havent implemented moveTakeOff or moveLand which are basically
responsible for the transitions phases between ground and air visa
versa. I have version where i use takeOff for the animation, which means
moving vertical. For now i am gonna leave for what it is.

PS: also includes additional movement fix for AreaTriggerAction which we
missed first time after the core update on movements.

@Wishmaster117 Been testing and trying a lot in the background on find
solutions and causes. The general solutions remains removed some tweaks,
altered code here and there. With the general idea to keep closer to the
original code for now at least.

Testing:
- Class abilities: Slow fall (priest), Flight Form (druid) : Green
- BG: Green
- Swimming and walking shallow waters: Green
- Takeoff and land when following master: Green
- Boat and zeppelins: Green
- Flymount and ground walking: Green
- Water Walking (shaman), Path of Frost (DK): Green
- Water Walking (shaman), Path of Frost (DK) transisions; flying,
swimming, water walking: Green


Skipping pets when group water walking, path of frost, once core pathing
changes has settled more i will add it. Which is easy but i dont wanna
add more edge cases and code branches for now.
2025-11-16 22:39:46 +01:00
Jay
ce2a990495 README.md edits (#1779)
I've noticed some sprawl in the README.md. The following edits have been
made around the goal of clarity, brevity, and emphases when needed:

- Removed dead link to the Spanish README.md at the top. The
README_ES.md file itself is not removed from the repo, but is however
out of date and does not seem useful at this time
- Removed the addons section. This seems to be covered by the Wiki in
the Documentation section unless I'm mistaken? We can add these back if
needed
- Reorganized installation instructions to emphasize the importance of
using the Playerbots branch of AzerothCore for all installations
- Moved the platform support from the Frequently Asked Questions section
to the installation instructions
- Modified punctuation of bulleted list. These can return if it irks
someone, but it seems easier to read without them
- Added a encouragement to star the project for updates and gain more
visibility
- Removed the Frequently Asked Questions section. I encourage this to be
covered in a wiki page but it can return if deemed necessary. I think a
lot of it can be moved to other sections if we really need them in there
- Added a "Contributing" section that replaces the Coding Standards
section
- Coding Standards are included in the Contributing section
- Added a second link to the Discord server in the Contributing section
- Removed a link to the main Playerbots branch from the introduction
section. This is covered and emphasized in the installation section
instead
- Migrated the encouragements to make bug reports from the introduction,
along with the message that this is still in active development, to the
contributing section
- Reduced redundant language when not necessary (e.g. "As noted above,")
- Updated language around custom branch and require branch for
uniformity. This will make it more clear to the users about the subject
- Removed "Classic" from the "Classic Installation" terminology for
being inaccurate. The subject is now known simply as "Cloning the
Repositories" while "Docker Installation" still remains distinct. This
will make what was formerly known as "Classic Installation" as de facto
default
- Appended the "Documentation" section with add-on information
- Unified all instances of AddOn to the Blizzard spelling without a
hyphen following the WoW 3.3.5a client spelling
- Appended "AzerothCore" to instances of "branch" to ensure readers they
are, in fact, installing AzerothCore here from our required branch
2025-11-15 22:00:19 +01:00
Alex Dcnh
6effabfa42 Core - Fix Bots can pickup the flag in Eye of the Strom instantly, without cast time from the spawn point (#1704)
# Eye of the Storm Flag Capture Behavior
 
## Previous Behavior
- Bots used to interact with the Netherstorm flag the moment they
reached interaction range, leading to instant pickups even before they
were correctly positioned.
- They did not respect the requirement to stand within the capture ring
or dismount before channeling, so the interaction finished immediately
without the intended delay.
 
## Current Behavior
- Bots now verify they are inside the Eye of the Storm capture circle
and will reposition toward the center flag or base flag until they are
within 2.5 yards of the game object before starting the channel.
- Once inside the circle they dismount, drop shapeshift forms, and come
to a full stop before beginning the channel cast.
- When channeling the center flag capture spell, bots keep checking for
the ongoing `SPELL_CAPTURE_BANNER` cast and wait for it to finish
instead of attempting repeated instant interactions.
- They will be interrupted if they receice any damage
 
These adjustments align the Eye of the Storm flow with the retail
mechanics and prevent bots from taking the flag instantly when it
spawns.

Fixes #1700.
2025-11-15 18:19:16 +01:00
Nicolas Lebacq
cadbcbd447 fix: Resolved an issue where the open spell was being cast by bots on despawned game objects. (#1842)
# Description

This addresses the infamous "Possible hacking attempt" error when bots
were furiously trying to interact with despawned game objects.
The problem was that the game objects were not being as spawned when
evaluating what the bot should do. It was only done when spells were
being cast on entities.
2025-11-15 10:27:33 +01:00
Crow
08c739f918 Align index with section update (#1831)
Update to index to reflect change to section title
2025-11-14 15:17:43 +01:00
privatecore
0729d14787 Fix PlayerbotAI constructors' members order and wrong type comparison (uint32 -> int32) (#1763)
Fix warnings: -Wsign-compare and -Wsign-compare.

I would also like to mention that there are issues with the following
methods:

`IsMainTank` -- in the last loop, it returns the result of comparing the
first alive tank it finds with the current bot, which seems odd.
`IsBotMainTank` -- it is used in only two places in the code (Yogg-Saron
strategy) and has a completely different logic for checking. In the last
loop, it only checks for a suitable tank-bot with a lower index (if it's
not the bot itself). Additionally, there is a logic issue in the loop:
if none of the conditions are met after the first iteration, the method
returns `false`.

Can someone from the maintainers take a look at this section of the code
for possible errors and logic issues?
2025-11-13 00:51:24 +01:00
nick
a37dd2b9ae Clarify random bot timing configuration section and parameter descriptions (#1826)
This update reorganizes and rewrites the random bot timing configuration
section for clarity and accuracy. The previous section was mislabeled as
"INTERVALS" and lacked precise descriptions. The new version:

1. Renames the header to RANDOM BOT TIMING AND BEHAVIOR
2. Adds concise, standardized comments for each parameter
3. Corrects misleading terminology (not all values are intervals)
4. Documents defaults and actual behavior clearly for easier tuning and
maintenance
5. No functional code changes — documentation and readability only.

Note, this is derived information from reading the code.
Please double check if I have captured each param accurately!
2025-11-11 09:21:13 +01:00
SaW
9c8ba42c64 FIX: Battlegrounds - Unset bot's master when current master left BG (#1819)
Adds a check for if current master left the BG and group, if so release
set master and carry on in BG.

This prevents multiple bots in (potentially multiple different) BG's to
still consider you as their master when you yourself have left.

- Applies when you join BGs with a party of bots.
2025-11-11 09:10:09 +01:00
kadeshar
e5bc495dbe Fixed codestyle fix (#1815)
Fix for this changes
<img width="1055" height="193" alt="obraz"
src="https://github.com/user-attachments/assets/a3d64f5b-80b0-4150-9760-0bcd4847f5d2"
/>
2025-11-07 17:49:33 +01:00
kadeshar
85c7009fe1 Codestyle fix (#1797)
Warning:
Dont change this PR as draft to make it testable


DONT REVIEW UNTIL Codestyle C++ workflow dont pass
2025-11-05 21:10:17 +01:00
Keleborn
ce51191e8f Fix. Leave group actions (#1774)
Fix for issue #1768 where the bot master was not getting reset. 
I also cleaned up leave group action to focus up function scope.
I moved the resets from #612 to the actual leaving function.

Disclosure: LLMs were NOT used in authoring this PR.

Test cases to consider for testers.
1. Master disbands group with random bots. Bots should go about their
business.
2. If you can dual box, create a raid where two real player both invite
random bots. One player leaves group, their bots should also leave.
(edge case, if a random bot that is supposed to leave the group becomes
leader they may disband the whole group.
2025-11-05 15:38:14 +01:00
Crow
d02d61e690 Implement Magtheridon strategy (#1721)
I'm marking this as a draft for now because I haven't done a detailed
review of the code, but I'm posting it now in case anybody wants to give
it a try.

Here's what the strategy (should) do.

### **Channeler Phase**

While you can probably AoE down all five Hellfire Channelers, that's
more dicey with IP nerfs and it's no fun, so the strategy takes what
would have still been considered an aggressive approach back in the day
by (1) assigning the Main Tank to the first Channeler, (2) having
Hunters misdirect two more Channelers to the MT, and (3) one Off Tank
picking up each of the fourth and fifth Channelers and dragging them out
of Shadow Volley Range from the main group. Sometimes the pull gets a
little wonky and one of the OTs might end up with one of the Channelers
that was intended for the MT, but it should work out in the end.

DPS will move through Channelers from Square -> Star -> Circle ->
Diamond -> Triangle. Once Square, Star, and Circle are down, the MT will
go sit by Magtheridon and wait for him to become active instead of
helping with the last two Channelers. I could have made the MT help with
the fourth Channeler too, but it's not needed, and positioning to pick
up Magtheridon after the third Channeler is a failsafe for low DPS
groups that aren't able to get four down before Magtheridon breaks free.

The top priority for Warlocks is to banish/fear the Burning Abyssals,
and they will continue to do so even after Magtheridon becomes active
(you are not supposed to kill the Abyssals; they have a gazillion HP and
automatically despawn after a minute). Their next priority is to put
Curse of Tongues on the Channelers.

### **Magtheridon Positioning**

The MT will pick up Magtheridon and pull him (moving backwards because
Magtheridon kind of hits like a bus) to a position up against the far
wall. Ranged DPS will spread out from a point roughly in the center of
the room, and Healers will spread out from a point that is a little
closer to Magtheridon. I have not built in aoe avoidance (except for
cube clickers, see below) because the general avoid aoe strategy seems
to work fine for this fight.

### **Clicking Manticron Cubes**

Now, the fun part. Bots will be assigned to clicking cubes by standard
group selection order (reverse join order), but assignment is done via
two passes. The first pass will look to select five ranged DPS bots that
are _not_ Warlocks. This is because Warlocks may still be needed to keep
Abyssals banished/feared and because Warlocks of all three specs put out
by far the most damage of all ranged DPS at level 70 in pre-raid/T4
gear. If there are not five non-Warlock ranged DPS bots available, then
the logic goes to the second pass, which can pick any bot that is not a
Tank.

Cube clicking works on a timer:

1. 48 seconds after Magtheridon breaks free, assigned cube clickers move
near their cubes (but a few yards outside of interact distance). During
this time, they should move around still to avoid Debris (by maintaining
distance from the triggering NPC) and Conflagration (by maintaining
distance from the triggering gameobject). Blast Nova is on a 54.35s to
55.4s timer, and I found 48s to always be ample time to get to the
cubes, but YMMV so this is a good thing to test. Going to a cube too
early not only takes away DPS but also risks more hazards appearing
on/around the cube that will then cause problems when the cube needs to
be clicked.
2. Blast Nova is a 2s cast, followed by a 10s channel (if not
interrupted by the cubes). As soon as the cast begins, bots will move
into interaction range and click the cube. Well, there is a randomized
delay between 200ms (about the fastest possible human reaction time to
visual stimuli) and 1500ms. It didn’t happen to me in a few runs, but it
may be possible that the delay causes the raid to eat one tick of Blast
Nova (I’m not sure if the first blast comes as soon as the channel
starts). Again, another good thing to test, but also one tick is not
going to kill anybody, and it’s arguably good to introduce some degree
of imperfection.
3. Once Blast Nova stops channeling (i.e., all five cubes have been
clicked and channeled simultaneously), bots will interrupt their cube
clicking and go back to regularly scheduled activities. Again, I’ve
introduced a randomized delay, this time between 200ms and 3000ms. Note
that bots can easily be perfect at this—if I don’t do the randomized
delay, they click and let go so fast that you can barely even see the
beams appear. It’s so atrocious for immersion that I consider the lack
of any randomization to be totally unacceptable for this strategy.
4. 48s after Blast Nova, bots will go back to their near-cube positions,
rinse and repeat.

If an assigned cube clicker dies, another bot should be immediately
assigned. All bots in the raid track the same timer so the new bot
should step into the prior bot’s shoes. Of course, if Blast Nova is
about to go off and a clicker dies next to a cube, you’re probably
wiping because I didn’t assign backups to stand in place. That’s too
much of a dad guild-level strategy even for me.

And that’s about it. Figuring out the cubes was a tremendous pain in the
ass, but I’ve really enjoyed the learning process.

---------

Co-authored-by: bash <31279994+hermensbas@users.noreply.github.com>
2025-11-05 14:53:16 +01:00
avirar
80dbd22ba1 Fixes equip bug with random suffix rings (#1757)
* Check item score of rings/trinkets to determine the correct slot to equip

* Early return, removed unecessary if statements, single line statements

Simplify logic for equipping items by reducing nested conditions.
2025-11-04 23:27:50 +01:00
privatecore
26a135a1ec Rewrite RandomPlayerbotFactory for improved maintainability and future updates (#1758)
* Rewrite RandomPlayerbotFactory - rewrite constructor and utility methods, simplify checks and logic

* Update the comment to clarify the original logic for race selection

* Remove magic numbers from CombineRaceAndGender method (gender)

* Add checks for races and classes disabled during random bot creation
2025-11-04 23:25:59 +01:00
Crow
983a55da86 Implement Gruul's Lair strategy (#1647)
* Implement Gruul's Lair strategy

* minor non-gameplay tweaks to code

* Use multiplier for tank assist

* HKM warlock & Gruul tank tweaks

* Fixed declarations

* rewrote HKM + minor Gruul tweaks

* Update PlayerbotAI.cpp

* modernize code and address comments

* clean ups to tank logic

* Oops.

* Remove post-move delay

For actions like positioning bosses, the standard post-movement delay should be overridden IMO since a player would be sequencing their actions in this type of situation

* Update RaidGruulsLairActions.cpp

* Replace break statements with return true

* enum class to enum

* moved all isuseful checks to triggers

* Split multipliers and improved banish logic

* Update for comments

* changes to int

* use helpers for marking icons

* address compile errors

* correct MoveTo and use RTI helper

* address comments and rename actions/triggers

* adjust alignment of location constants

* fix some crappy returns

* allow return true when changing targets

* change indents and move enums inside namespace

* style changes, trim trailing whitespaces, address comments
2025-11-04 23:01:30 +01:00
kadeshar
e35900f9d0 Merge pull request #1699 from IainD92/RandomBotGuildTotals_fix
Random bot guild count / generation fix
2025-11-04 20:50:02 +01:00
kadeshar
d9f0d5a555 Merge pull request #1808 from Raz0r1337/typo_fix
Update 2025_10_27_00_ai_playerbot_german_texts.sql
2025-11-04 20:19:44 +01:00
St0ny
7d5c9e3ee0 Update 2025_10_27_00_ai_playerbot_german_texts.sql
line error fixed
2025-11-04 14:48:24 +01:00
Revision
43164e74e1 Normalize line endings for 2025_10_27_00_ai_playerbot_german_texts.sql to LF (#1795) 2025-11-02 13:28:26 +01:00
privatecore
f7fea456ca Fix PositionInfo constructors' members order (#1776) 2025-11-02 13:27:25 +01:00
Revision
12a5132c33 Merge pull request #1793 from kadeshar/codestyle-azerothcore
Codestyle cpp azerothcore
2025-11-01 21:15:31 +01:00
kadeshar
bbbf71d40c - added missing var for codestyle azerothcore 2025-11-01 17:38:11 +01:00
kadeshar
586c4d9d05 - Added codestyle azerothcore python script 2025-11-01 17:27:26 +01:00
kadeshar
cb099bcaf4 Update repository condition for C++ job 2025-11-01 17:25:07 +01:00
St0ny
5d3e64800f important bugfix for the last PR #1675 (#1782)
* Update 2025_10_27_00_ai_playerbot_german_texts.sql

Fix bug that adds holes in the german text_loc3 collumn
2025-10-29 17:39:06 +01:00
St0ny
235f0249b2 insert of deDE localized chatter texts into text_loc3 (#1675)
* insert of german chatter texts into text_loc3

restore of original file ai_playerbot_texts.sql
2025-10-28 21:56:42 +01:00
bash
7237b154e0 Added intelliJ project folder on exclusion list (#1764) 2025-10-25 00:54:27 +02:00
NoxMax
c3fd97b6c0 Fix: Prevent addClass bots from getting realm firsts (#1745)
* Random/Addclass bots cannot get first achievements

* Use firsts achievement flags
2025-10-23 20:10:35 +02:00
Alex Dcnh
286213eb8b Fix Playerbots won't fly #1753 (#1761) 2025-10-23 01:33:22 +02:00
Iain Donnelly
1d19dea974 Update RandomPlayerbotFactory.cpp 2025-10-22 23:53:42 +01:00
SaW
6e1c9114df FIX: part of logic in picking quest reward (#1752)
* FIX: part of logic in picking reward

Avoids dereferencing begin() after confirming the set is empty.

* Update TalkToQuestGiverAction.cpp
2025-10-21 10:36:10 +02:00
bash
7e810f8174 Update README.md (#1750) 2025-10-20 21:46:51 +02:00
bash
e0df6558f5 Added shaman default combat strategy names on top of custom (#1739) 2025-10-20 18:20:46 +02:00
bash
50ac6e5b95 core_merge_changes (#1747) 2025-10-20 14:12:14 +02:00
Alex Dcnh
2c5185a7cb Update StatsWeightCalculator.cpp (#1744) 2025-10-20 13:17:59 +02:00
Crow
f874d2c79e Fix typo in server loading message (#1742) 2025-10-20 01:05:45 +02:00
SaW
5a4acbe36c Update PvpValues.cpp (#1746)
Set uninitialized variable
Remove std::move
2025-10-20 01:04:11 +02:00
SaW
e693b208be FIX: ICC - default return position for BQL (#1737) 2025-10-18 22:54:52 +02:00
bash
10ce94e065 Removal space lel (#1740) 2025-10-18 22:32:43 +02:00
kikiviki
553b8276eb Heirloom quality auto-equip calculation implemented (#1732)
* Heirloom quality auto-equip calculation implemented
2025-10-18 22:17:44 +02:00
Crow
f791ab61c4 Update server loading message 2025-10-16 22:43:49 +02:00
Crow
3260ca1429 Cleanups to config and source (#1720)
* general edits

* Clarify comment for bot teleportation map IDs
2025-10-14 15:43:18 +02:00
Yunfan Li
e1fa733aa5 Preparation for project transfer (#1733) 2025-10-14 00:11:54 +08:00
Iain Donnelly
3f050a4a77 Change LOG_INFO to LOG_DEBUG 2025-10-07 22:46:19 +01:00
Iain Donnelly
0e4c759e7f Wishmaster update
Optimised, better use of DB Query, avoids reallocations. Nice.
2025-10-04 12:56:06 +01:00
Iain Donnelly
24f841f728 Verbose logging.
Should now be able to see guild count values as the server inits
2025-10-04 12:56:06 +01:00
Iain Donnelly
444be2994e v2
Neatened some things up, removed obsolete code, added a break out of the loop if an empty guild name (none available) is returned from the playerbots_guild_names table.
2025-10-04 12:56:06 +01:00
Iain Donnelly
8a68de4476 Update RandomPlayerbotFactory.cpp
Fixed a typo
2025-10-04 12:56:06 +01:00
Iain Donnelly
7d50ceef3d Update RandomPlayerbotFactory.cpp
Added a query to count the number of guilds straight from the DB, then filter out player guilds. (instead of relying on accessing guilds from a list of random bots and adding them up)

This needs some formatting / tidying once I make sure we are counting guilds properly.
2025-10-04 12:56:06 +01:00
1264 changed files with 26570 additions and 16595 deletions

40
.github/workflows/codestyle_cpp.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: C++ Codestyle
on:
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
paths:
- src/**
- "!README.md"
- "!docs/**"
concurrency:
group: "codestyle-cppcheck-${{ github.event.pull_request.number }}"
cancel-in-progress: true
jobs:
triage:
runs-on: ubuntu-latest
name: C++
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: AzerothCore codestyle
run: python ./apps/codestyle/codestyle-cpp.py
- name: C++ Advanced
run: |
sudo apt update -y
sudo apt install -y cppcheck
cppcheck --force --inline-suppr --suppressions-list=./.suppress.cppcheck src/ --output-file=report.txt
if [ -s report.txt ]; then # if file is not empty
cat report.txt
exit 1 # let github action fails
fi

View File

@@ -25,21 +25,25 @@ jobs:
with: with:
repository: 'mod-playerbots/azerothcore-wotlk' repository: 'mod-playerbots/azerothcore-wotlk'
ref: 'Playerbot' ref: 'Playerbot'
path: 'ac'
- name: Checkout Playerbot Module - name: Checkout Playerbot Module
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
repository: 'mod-playerbots/mod-playerbots' repository: 'mod-playerbots/mod-playerbots'
path: 'modules/mod-playerbots' #path: 'modules/mod-playerbots'
path: ac/modules/mod-playerbots
- name: ccache - name: ccache
uses: hendrikmuhs/ccache-action@v1.2.13 uses: hendrikmuhs/ccache-action@v1.2.13
- name: Configure OS - name: Configure OS
shell: bash shell: bash
working-directory: ac
env: env:
CONTINUOUS_INTEGRATION: true CONTINUOUS_INTEGRATION: true
run: | run: |
./acore.sh install-deps ./acore.sh install-deps
- name: Build - name: Build
shell: bash shell: bash
working-directory: ac
run: | run: |
export CTOOLS_BUILD=all export CTOOLS_BUILD=all
./acore.sh compiler build ./acore.sh compiler build

1
.gitignore vendored
View File

@@ -49,3 +49,4 @@ local.properties
.project .project
.cproject .cproject
.vscode .vscode
.idea

1
.suppress.cppcheck Normal file
View File

@@ -0,0 +1 @@
cppcheckError

View File

@@ -3,7 +3,7 @@
| |
<a href="https://github.com/mod-playerbots/mod-playerbots/blob/master/README_CN.md">中文</a> <a href="https://github.com/mod-playerbots/mod-playerbots/blob/master/README_CN.md">中文</a>
| |
<a href="https://github.com/brighton-chi/mod-playerbots/blob/readme/README_ES.md">Español</a> <a href="https://github.com/mod-playerbots/mod-playerbots/blob/master/README_ES.md">Español</a>
</p> </p>
@@ -18,25 +18,27 @@
</div> </div>
# Playerbots Module # Playerbots Module
`mod-playerbots` is an [AzerothCore](https://www.azerothcore.org/) module that adds player-like bots to a server. The project is based off [IKE3's Playerbots](https://github.com/ike3/mangosbot) and requires a custom branch of AzerothCore to compile and run: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). `mod-playerbots` is an [AzerothCore](https://www.azerothcore.org/) module that adds player-like bots to a server. The project is based off [IKE3's Playerbots](https://github.com/ike3/mangosbot).
Features include: Features include:
- The ability to log in alt characters as bots, allowing players to interact with their other characters, form parties, level up, and more; - The ability to log in alt characters as bots, allowing players to interact with their other characters, form parties, level up, and more
- Random bots that wander through the world, complete quests, and otherwise behave like players, simulating the MMO experience; - Random bots that wander through the world, complete quests, and otherwise behave like players, simulating the MMO experience
- Bots capable of running most raids and battlegrounds; - Bots capable of running most raids and battlegrounds
- Highly configurable settings to define how bots behave; - Highly configurable settings to define how bots behave
- Excellent performance, even when running thousands of bots. - Excellent performance, even when running thousands of bots
**This project is still under development**. If you encounter any errors or experience crashes, we kindly request that you [report them as GitHub issues](https://github.com/mod-playerbots/mod-playerbots/issues/new?template=bug_report.md). Your valuable feedback will help us improve this project collaboratively. We also have a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can discuss the project, ask questions, and get involved in the community!
`mod-playerbots` has a **[Discord server](https://discord.gg/NQm5QShwf9)** where you can discuss the project, ask questions, and get involved in the community!
## Installation ## Installation
### Classic Installation Supported platforms are Ubuntu, Windows, and macOS. Other Linux distributions may work, but may not receive support.
As noted above, `mod-playerbots` requires a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). To install the module, simply run: **All `mod-playerbots` installations require a custom branch of AzerothCore: [mod-playerbots/azerothcore-wotlk/tree/Playerbot](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot).** This branch allows the `mod-playerbots` module to build and function. Updates from the upstream are implemented regularly to this branch. Instructions for installing this required branch and this module are provided below.
### Cloning the Repositories
To install both the required branch of AzerothCore and the `mod-playerbots` module from source, run the following:
```bash ```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
@@ -48,7 +50,7 @@ For more information, refer to the [AzerothCore Installation Guide](https://www.
### Docker Installation ### Docker Installation
**Docker installation is considered experimental.** To install the module on a Docker installation, run: Docker installations are considered experimental (unofficial with limited support), and previous Docker experience is recommended. To install `mod-playerbots` on Docker, first clone the required branch of AzerothCore and this module:
```bash ```bash
git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot git clone https://github.com/mod-playerbots/azerothcore-wotlk.git --branch=Playerbot
@@ -85,25 +87,23 @@ Use `docker compose up -d --build` to build and run the server. For more informa
## Documentation ## Documentation
The [Playerbots Wiki](https://github.com/mod-playerbots/mod-playerbots/wiki) contains an extensive overview of addons, commands, raids with programmed bot strategies, and recommended performance configurations. Please note that documentation may be incomplete or out-of-date in some sections. Contributions are welcome. The [Playerbots Wiki](https://github.com/mod-playerbots/mod-playerbots/wiki) contains an extensive overview of AddOns, commands, raids with programmed bot strategies, and recommended performance configurations. Please note that documentation may be incomplete or out-of-date in some sections, and contributions are welcome.
## Frequently Asked Questions Bots are controlled via chat commands. For larger bot groups, this can be cumbersome. Because of this, community members have developed client AddOns to allow controlling bots through the in-game UI. We recommend you check out their projects listed in the [AddOns and Submodules](https://github.com/mod-playerbots/mod-playerbots/wiki/Playerbot-Addons-and-Sub%E2%80%90Modules) page.
- **Why aren't my bots casting spells?** Please make sure that the necessary English DBC file (enUS) is present. ## Contributing
- **What platforms are supported?** We support Ubuntu, Windows, and macOS. Other Linux distros may work, but will not receive support.
- **Why isn't my source compiling?** Please ensure that you are compiling with the required [custom branch of AzerothCore](https://github.com/mod-playerbots/azerothcore-wotlk/tree/Playerbot). Additionally, please [check the build status of our CI](https://github.com/mod-playerbots/mod-playerbots/actions). If the latest build is failing, rever to the last successful commit until we address the issue.
## Addons This project is still under development. We encourage anyone to make contributions, anything from pull requests to reporting issues. If you encounter any errors or experience crashes, we encourage you [report them as GitHub issues](https://github.com/mod-playerbots/mod-playerbots/issues/new?template=bug_report.md). Your valuable feedback will help us improve this project collaboratively.
Typically, bots are controlled via chat commands. For larger bot groups, this can be unwieldy. As an alternative, community members have developed client Add-Ons to allow controlling bots through the in-game UI. We recommend you check out their projects: If you make coding contributions, `mod-playerbots` complies with the [C++ Code Standards](https://www.azerothcore.org/wiki/cpp-code-standards) established by AzerothCore. Each Pull Request must include all test scenarios the author performed, along with their results, to demonstrate that the changes were properly verified.
- [Multibot](https://github.com/Macx-Lio/MultiBot) (by Macx-Lio), which includes English, Chinese, French, German, Korean, Russian, and Spanish support [note: active development is temporarily continuing on a fork in Macx-Lio's absence (https://github.com/Wishmaster117/MultiBot)] We recommend joining the [Discord server](https://discord.gg/NQm5QShwf9) to make your contributions to the project easier, as a lot of active support is carried out through this server.
- [Unbot Addon (zh)](https://github.com/liyunfan1223/unbot-addon) (Chinese version by Liyunfan) [note: no longer under active development]
- [Unbot Addon (en)](https://github.com/noisiver/unbot-addon/tree/english) (English version translated by @Revision) [note: no longer under active development] Please click on the "⭐" button to stay up to date and help us gain more visibility on GitHub!
## Acknowledgements ## Acknowledgements
`mod-playerbots` is is based off [ZhengPeiRu21/mod-playerbots](https://github.com/ZhengPeiRu21/mod-playerbots) and [celguar/mangosbot-bots](https://github.com/celguar/mangosbot-bots). We extend our gratitude to [@ZhengPeiRu21](https://github.com/ZhengPeiRu21) and [@celguar](https://github.com/celguar) for the continued efforts in maintaining the module. `mod-playerbots` is based on [ZhengPeiRu21/mod-playerbots](https://github.com/ZhengPeiRu21/mod-playerbots) and [celguar/mangosbot-bots](https://github.com/celguar/mangosbot-bots). We extend our gratitude to [@ZhengPeiRu21](https://github.com/ZhengPeiRu21) and [@celguar](https://github.com/celguar) for their continued efforts in maintaining the module.
Also, a thank you to the many contributors who've helped build this project: Also, a thank you to the many contributors who've helped build this project:

View File

@@ -0,0 +1,263 @@
import io
import os
import sys
import re
# Get the src directory of the project
src_directory = os.path.join(os.getcwd(), 'src')
# Global variables
error_handler = False
results = {
"Multiple blank lines check": "Passed",
"Trailing whitespace check": "Passed",
"GetCounter() check": "Passed",
"Misc codestyle check": "Passed",
"GetTypeId() check": "Passed",
"NpcFlagHelpers check": "Passed",
"ItemFlagHelpers check": "Passed",
"ItemTemplateFlagHelpers check": "Passed"
}
# Main function to parse all the files of the project
def parsing_file(directory: str) -> None:
print("Starting AzerothCore CPP Codestyle check...")
print(" ")
print("Please read the C++ Code Standards for AzerothCore:")
print("https://www.azerothcore.org/wiki/cpp-code-standards")
print(" ")
for root, _, files in os.walk(directory):
for file in files:
if not file.endswith('.ico'): # Skip .ico files that cannot be read
file_path = os.path.join(root, file)
file_name = file
try:
with open(file_path, 'r', encoding='utf-8') as file:
multiple_blank_lines_check(file, file_path)
trailing_whitespace_check(file, file_path)
get_counter_check(file, file_path)
if not file_name.endswith('.cmake') and file_name != 'CMakeLists.txt':
misc_codestyle_check(file, file_path)
if file_name != 'Object.h':
get_typeid_check(file, file_path)
if file_name != 'Unit.h':
npcflags_helpers_check(file, file_path)
if file_name != 'Item.h':
itemflag_helpers_check(file, file_path)
if file_name != 'ItemTemplate.h':
itemtemplateflag_helpers_check(file, file_path)
except UnicodeDecodeError:
print(f"\nCould not decode file {file_path}")
sys.exit(1)
# Output the results
print("")
for check, result in results.items():
print(f"{check} : {result}")
if error_handler:
print("\nPlease fix the codestyle issues above.")
sys.exit(1)
else:
print(f"\nEverything looks good")
# Codestyle patterns checking for multiple blank lines
def multiple_blank_lines_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
consecutive_blank_lines = 0
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if line.strip() == '':
consecutive_blank_lines += 1
if consecutive_blank_lines > 1:
print(f"Multiple blank lines found in {file_path} at line {line_number - 1}")
check_failed = True
else:
consecutive_blank_lines = 0
# Additional check for the end of the file
if consecutive_blank_lines >= 1:
print(f"Multiple blank lines found at the end of: {file_path}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["Multiple blank lines check"] = "Failed"
# Codestyle patterns checking for whitespace at the end of the lines
def trailing_whitespace_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if line.endswith(' \n'):
print(f"Trailing whitespace found: {file_path} at line {line_number}")
if not error_handler:
error_handler = True
results["Trailing whitespace check"] = "Failed"
# Codestyle patterns checking for ObjectGuid::GetCounter()
def get_counter_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'ObjectGuid::GetCounter()' in line:
print(f"Please use ObjectGuid::ToString().c_str() instead ObjectGuid::GetCounter(): {file_path} at line {line_number}")
if not error_handler:
error_handler = True
results["GetCounter() check"] = "Failed"
# Codestyle patterns checking for GetTypeId()
def get_typeid_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'GetTypeId() == TYPEID_ITEM' in line or 'GetTypeId() != TYPEID_ITEM' in line:
print(f"Please use IsItem() instead of GetTypeId(): {file_path} at line {line_number}")
check_failed = True
if 'GetTypeId() == TYPEID_UNIT' in line or 'GetTypeId() != TYPEID_UNIT' in line:
print(f"Please use IsCreature() instead of GetTypeId(): {file_path} at line {line_number}")
check_failed = True
if 'GetTypeId() == TYPEID_PLAYER' in line or 'GetTypeId() != TYPEID_PLAYER' in line:
print(f"Please use IsPlayer() instead of GetTypeId(): {file_path} at line {line_number}")
check_failed = True
if 'GetTypeId() == TYPEID_GAMEOBJECT' in line or 'GetTypeId() != TYPEID_GAMEOBJECT' in line:
print(f"Please use IsGameObject() instead of GetTypeId(): {file_path} at line {line_number}")
check_failed = True
if 'GetTypeId() == TYPEID_DYNOBJECT' in line or 'GetTypeId() != TYPEID_DYNOBJECT' in line:
print(f"Please use IsDynamicObject() instead of GetTypeId(): {file_path} at line {line_number}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["GetTypeId() check"] = "Failed"
# Codestyle patterns checking for NpcFlag helpers
def npcflags_helpers_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'GetUInt32Value(UNIT_NPC_FLAGS)' in line:
print(
f"Please use GetNpcFlags() instead of GetUInt32Value(UNIT_NPC_FLAGS): {file_path} at line {line_number}")
check_failed = True
if 'HasFlag(UNIT_NPC_FLAGS,' in line:
print(
f"Please use HasNpcFlag() instead of HasFlag(UNIT_NPC_FLAGS, ...): {file_path} at line {line_number}")
check_failed = True
if 'SetUInt32Value(UNIT_NPC_FLAGS,' in line:
print(
f"Please use ReplaceAllNpcFlags() instead of SetUInt32Value(UNIT_NPC_FLAGS, ...): {file_path} at line {line_number}")
check_failed = True
if 'SetFlag(UNIT_NPC_FLAGS,' in line:
print(
f"Please use SetNpcFlag() instead of SetFlag(UNIT_NPC_FLAGS, ...): {file_path} at line {line_number}")
check_failed = True
if 'RemoveFlag(UNIT_NPC_FLAGS,' in line:
print(
f"Please use RemoveNpcFlag() instead of RemoveFlag(UNIT_NPC_FLAGS, ...): {file_path} at line {line_number}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["NpcFlagHelpers check"] = "Failed"
# Codestyle patterns checking for ItemFlag helpers
def itemflag_helpers_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE)' in line:
print(
f"Please use IsRefundable() instead of HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_REFUNDABLE): {file_path} at line {line_number}")
check_failed = True
if 'HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE)' in line:
print(
f"Please use IsBOPTradable() instead of HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_BOP_TRADEABLE): {file_path} at line {line_number}")
check_failed = True
if 'HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_WRAPPED)' in line:
print(
f"Please use IsWrapped() instead of HasFlag(ITEM_FIELD_FLAGS, ITEM_FIELD_FLAG_WRAPPED): {file_path} at line {line_number}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["ItemFlagHelpers check"] = "Failed"
# Codestyle patterns checking for ItemTemplate helpers
def itemtemplateflag_helpers_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'Flags & ITEM_FLAG' in line:
print(
f"Please use HasFlag(ItemFlag) instead of 'Flags & ITEM_FLAG_': {file_path} at line {line_number}")
check_failed = True
if 'Flags2 & ITEM_FLAG2' in line:
print(
f"Please use HasFlag2(ItemFlag2) instead of 'Flags2 & ITEM_FLAG2_': {file_path} at line {line_number}")
check_failed = True
if 'FlagsCu & ITEM_FLAGS_CU' in line:
print(
f"Please use HasFlagCu(ItemFlagsCustom) instead of 'FlagsCu & ITEM_FLAGS_CU_': {file_path} at line {line_number}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["ItemTemplateFlagHelpers check"] = "Failed"
# Codestyle patterns checking for various codestyle issues
def misc_codestyle_check(file: io, file_path: str) -> None:
global error_handler, results
file.seek(0) # Reset file pointer to the beginning
check_failed = False
# used to check for "if/else (...) {" "} else" ignores "if/else (...) {...}" "#define ... if/else (...) {"
ifelse_curlyregex = r"^[^#define].*\s+(if|else)(\s*\(.*\))?\s*{[^}]*$|}\s*else(\s*{[^}]*$)"
# used to catch double semicolons ";;" ignores "(;;)"
double_semiregex = r"(?<!\()\s*;;(?!\))"
# used to catch tabs
tab_regex = r"\t"
# Parse all the file
for line_number, line in enumerate(file, start = 1):
if 'const auto&' in line:
print(
f"Please use the 'auto const&' syntax instead of 'const auto&': {file_path} at line {line_number}")
check_failed = True
if re.search(r'\bconst\s+\w+\s*\*\b', line):
print(
f"Please use the 'Class/ObjectType const*' syntax instead of 'const Class/ObjectType*': {file_path} at line {line_number}")
check_failed = True
if [match for match in [' if(', ' if ( '] if match in line]:
print(
f"Please use the 'if (XXXX)' syntax instead of 'if(XXXX)': {file_path} at line {line_number}")
check_failed = True
if re.match(ifelse_curlyregex, line):
print(
f"Curly brackets are not allowed to be leading or trailing if/else statements. Place it on a new line: {file_path} at line {line_number}")
check_failed = True
if re.search(double_semiregex, line):
print(
f"Double semicolon (;;) found in {file_path} at line {line_number}")
check_failed = True
if re.match(tab_regex, line):
print(
f"Tab found! Replace it to 4 spaces: {file_path} at line {line_number}")
check_failed = True
# Handle the script error and update the result output
if check_failed:
error_handler = True
results["Misc codestyle check"] = "Failed"
# Main function
parsing_file(src_directory)

0
code_format.sh Executable file → Normal file
View File

View File

@@ -21,10 +21,11 @@
# THRESHOLDS # THRESHOLDS
# QUESTS # QUESTS
# COMBAT # COMBAT
# PALADIN BUFFS STRATEGIES # GREATER BUFFS STRATEGIES
# CHEATS # CHEATS
# SPELLS # SPELLS
# FLIGHTPATH # FLIGHTPATH
# PROFESSIONS
# RANDOMBOT-SPECIFIC SETTINGS # RANDOMBOT-SPECIFIC SETTINGS
# GENERAL # GENERAL
# LEVELS # LEVELS
@@ -36,7 +37,7 @@
# RPG STRATEGY # RPG STRATEGY
# TELEPORTS # TELEPORTS
# BATTLEGROUND & ARENA & PVP # BATTLEGROUND & ARENA & PVP
# INTERVALS # RANDOM BOT TIMING AND BEHAVIOR
# PREMADE SPECS # PREMADE SPECS
# INFORMATION # INFORMATION
# WARRIOR # WARRIOR
@@ -44,7 +45,7 @@
# HUNTER # HUNTER
# ROGUE # ROGUE
# PRIEST # PRIEST
# DEATHKNIGHT # DEATH KNIGHT
# SHAMAN # SHAMAN
# MAGE # MAGE
# WARLOCK # WARLOCK
@@ -55,7 +56,7 @@
# HUNTER # HUNTER
# ROGUE # ROGUE
# PRIEST # PRIEST
# DEATHKNIGHT # DEATH KNIGHT
# SHAMAN # SHAMAN
# MAGE # MAGE
# WARLOCK # WARLOCK
@@ -90,17 +91,20 @@ AiPlayerbot.MinRandomBots = 500
AiPlayerbot.MaxRandomBots = 500 AiPlayerbot.MaxRandomBots = 500
# Randombot accounts # Randombot accounts
# If you are not using any expansion at all, you may have to set this manually, in which case please ensure that RandomBotAccountCount is at least greater than (MaxRandomBots / 10 + AddClassAccountPoolSize) # If you are not using any expansion at all, you may have to set this manually, in which case please
# ensure that RandomBotAccountCount is at least greater than (MaxRandomBots / 10 + AddClassAccountPoolSize)
# Default: 0 (automatic) # Default: 0 (automatic)
AiPlayerbot.RandomBotAccountCount = 0 AiPlayerbot.RandomBotAccountCount = 0
# Delete all randombot accounts # Delete all randombot accounts
# To apply this, set the number to 1 and run the Worldserver. Once deletion is complete, if you would like to recreate randombots, set the number back to 0 and rerun the Worldserver. # To apply this, set the number to 1 and run the Worldserver. Once deletion is complete, if you would
# like to recreate randombots, set the number back to 0 and rerun the Worldserver.
AiPlayerbot.DeleteRandomBotAccounts = 0 AiPlayerbot.DeleteRandomBotAccounts = 0
# Disable randombots when no real players are logged in # Disable randombots when no real players are logged in
# Default: 0 (randombots will login when server starts) # Default: 0 (randombots will login when server starts)
# If enabled, randombots will only log in 30 seconds (default) after a real player logs in, and will log out 300 seconds (default) after all real players log out # If enabled, randombots will only log in 30 seconds (default) after a real player logs in, and will
# log out 300 seconds (default) after all real players log out
AiPlayerbot.DisabledWithoutRealPlayer = 0 AiPlayerbot.DisabledWithoutRealPlayer = 0
AiPlayerbot.DisabledWithoutRealPlayerLoginDelay = 30 AiPlayerbot.DisabledWithoutRealPlayerLoginDelay = 30
AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300 AiPlayerbot.DisabledWithoutRealPlayerLogoutDelay = 300
@@ -152,7 +156,8 @@ AiPlayerbot.AllowGuildBots = 1
AiPlayerbot.AllowTrustedAccountBots = 1 AiPlayerbot.AllowTrustedAccountBots = 1
# Randombots will create guilds with nearby randombots # Randombots will create guilds with nearby randombots
# Note: currently, randombots will not invite more bots after a guild is created (i.e., randombot guilds will have only the 10 initial randombots needed to sign the charter) # Note: currently, randombots will not invite more bots after a guild is created,
# meaning randombot guilds will have only the 10 initial randombots needed to sign the charter
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.RandomBotGuildNearby = 0 AiPlayerbot.RandomBotGuildNearby = 0
@@ -186,7 +191,8 @@ AiPlayerbot.AutoInitOnly = 0
# Default: 1.0 (same with the player) # Default: 1.0 (same with the player)
AiPlayerbot.AutoInitEquipLevelLimitRatio = 1.0 AiPlayerbot.AutoInitEquipLevelLimitRatio = 1.0
# Bot automatically trains spells when talking to trainer (yes = train all available spells as long as the bot has the money, free = auto trains with no money cost, no = only list spells) # Bot automatically trains spells when talking to trainer
# yes = train all available spells as long as the bot has the money, free = auto trains with no money cost, no = only list spells
AiPlayerbot.AutoTrainSpells = yes AiPlayerbot.AutoTrainSpells = yes
# #
@@ -263,7 +269,7 @@ AiPlayerbot.UseFastFlyMountAtMinLevel = 70
AiPlayerbot.RandomBotShowHelmet = 1 AiPlayerbot.RandomBotShowHelmet = 1
AiPlayerbot.RandomBotShowCloak = 1 AiPlayerbot.RandomBotShowCloak = 1
# Randombots and altbots automatically equip upgrades (bots will equip any item obtained from looting or a quest if they are sufficient upgrades) # Randombots and altbots automatically equip any items in their inventory that are sufficient upgrades
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoEquipUpgradeLoot = 1 AiPlayerbot.AutoEquipUpgradeLoot = 1
@@ -284,9 +290,6 @@ AiPlayerbot.TwoRoundsGearInit = 0
# #
# #
# Bots will say information about items when collecting them
AiPlayerbot.SayWhenCollectingItems = 1
# Bots keep looting when loot system is set to free for all # Bots keep looting when loot system is set to free for all
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.FreeMethodLoot = 0 AiPlayerbot.FreeMethodLoot = 0
@@ -314,7 +317,8 @@ AiPlayerbot.GlobalCooldown = 500
# Max wait time when moving # Max wait time when moving
AiPlayerbot.MaxWaitForMove = 5000 AiPlayerbot.MaxWaitForMove = 5000
# Disable use of MoveSplinePath for bot movement, will result in more erratic bot movement but means stun/snare/root/etc will work on bots (they wont reliably work when MoveSplinePath is enabled, though slowing effects still work ok) # Enable/disable use of MoveSplinePath for bot movement
# Disabling will result in more erratic movement but is required for stuns, snares, and roots to work on bots
# Default: 0 - MoveSplinePath enabled # Default: 0 - MoveSplinePath enabled
# 1 - MoveSplinePath disabled in BG/Arena only # 1 - MoveSplinePath disabled in BG/Arena only
# 2 - MoveSplinePath disabled everywhere # 2 - MoveSplinePath disabled everywhere
@@ -360,15 +364,15 @@ AiPlayerbot.LootDelay = 1000
# #
# #
# Distances are in yards
AiPlayerbot.FarDistance = 20.0 AiPlayerbot.FarDistance = 20.0
AiPlayerbot.SightDistance = 75.0 AiPlayerbot.SightDistance = 100.0
AiPlayerbot.SpellDistance = 28.5 AiPlayerbot.SpellDistance = 28.5
AiPlayerbot.ShootDistance = 5.0 AiPlayerbot.ShootDistance = 5.0
AiPlayerbot.ReactDistance = 150.0
AiPlayerbot.GrindDistance = 75.0
AiPlayerbot.HealDistance = 38.5 AiPlayerbot.HealDistance = 38.5
AiPlayerbot.LootDistance = 15.0 AiPlayerbot.LootDistance = 15.0
AiPlayerbot.FleeDistance = 5.0 AiPlayerbot.FleeDistance = 5.0
AiPlayerbot.AggroDistance = 22
AiPlayerbot.TooCloseDistance = 5.0 AiPlayerbot.TooCloseDistance = 5.0
AiPlayerbot.MeleeDistance = 0.75 AiPlayerbot.MeleeDistance = 0.75
AiPlayerbot.FollowDistance = 1.5 AiPlayerbot.FollowDistance = 1.5
@@ -376,7 +380,8 @@ AiPlayerbot.WhisperDistance = 6000.0
AiPlayerbot.ContactDistance = 0.45 AiPlayerbot.ContactDistance = 0.45
AiPlayerbot.AoeRadius = 10 AiPlayerbot.AoeRadius = 10
AiPlayerbot.RpgDistance = 200 AiPlayerbot.RpgDistance = 200
AiPlayerbot.AggroDistance = 22 AiPlayerbot.GrindDistance = 75.0
AiPlayerbot.ReactDistance = 150.0
# #
# #
@@ -407,10 +412,11 @@ AiPlayerbot.HighMana = 65
# #
# #
# Bots pick their quest rewards (yes = picks the most useful item, no = list all rewards, ask = pick useful item and lists if multiple) # Bots pick their quest rewards
# yes = picks the most useful item, no = list all rewards, ask = pick useful item and lists if multiple
AiPlayerbot.AutoPickReward = yes AiPlayerbot.AutoPickReward = yes
# Sync quests with player (Bots will complete quests the moment you hand them in and will not loot quest items.) # Sync quests with player (bots will complete quests the moment you hand them in and will not loot quest items.)
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.SyncQuestWithPlayer = 1 AiPlayerbot.SyncQuestWithPlayer = 1
@@ -435,7 +441,7 @@ AiPlayerbot.DropObsoleteQuests = 1
# Auto add dungeon/raid strategies when entering the instance if implemented # Auto add dungeon/raid strategies when entering the instance if implemented
AiPlayerbot.ApplyInstanceStrategies = 1 AiPlayerbot.ApplyInstanceStrategies = 1
# Enable auto avoid aoe strategy (experimental) # Enable auto avoid aoe strategy
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoAvoidAoe = 1 AiPlayerbot.AutoAvoidAoe = 1
@@ -462,7 +468,7 @@ AiPlayerbot.FleeingEnabled = 1
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# PALADIN BUFFS STRATEGIES # GREATER BUFFS STRATEGIES
# #
# #
@@ -484,12 +490,14 @@ AiPlayerbot.RPWarningCooldown = 30
# #
# #
# Enable/Disable maintenance command (learn all available spells and skills, supplement consumables, repair, enchant equipment if bot's level is above AiPlayerbot.MinEnchantingBotLevel) # Enable/Disable maintenance command
# Learn all available spells and skills, assign talent points, refresh consumables, repair, enchant equipment, socket gems, etc.
# Applies if bot's level is above AiPlayerbot.MinEnchantingBotLevel
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.MaintenanceCommand = 1 AiPlayerbot.MaintenanceCommand = 1
# Enable/Disable specific maintenance command functionality for alt bots # Enable/Disable specific maintenance command functionality for alt bots
# Disable to prevent players from giving free bags, spells, skill levels etc to their alt bots # Disable to prevent players from giving free bags, spells, skill levels, etc. to their alt bots
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AltMaintenanceAmmo = 1 AiPlayerbot.AltMaintenanceAmmo = 1
AiPlayerbot.AltMaintenanceFood = 1 AiPlayerbot.AltMaintenanceFood = 1
@@ -501,6 +509,7 @@ AiPlayerbot.AltMaintenanceBags = 1
AiPlayerbot.AltMaintenanceMounts = 1 AiPlayerbot.AltMaintenanceMounts = 1
AiPlayerbot.AltMaintenanceSkills = 1 AiPlayerbot.AltMaintenanceSkills = 1
# "Special Spells" consist of any spells listed in AiPlayerbot.RandomBotSpellIds and Death Gate for Death Knights
AiPlayerbot.AltMaintenanceClassSpells = 1 AiPlayerbot.AltMaintenanceClassSpells = 1
AiPlayerbot.AltMaintenanceAvailableSpells = 1 AiPlayerbot.AltMaintenanceAvailableSpells = 1
AiPlayerbot.AltMaintenanceSpecialSpells = 1 AiPlayerbot.AltMaintenanceSpecialSpells = 1
@@ -515,8 +524,8 @@ AiPlayerbot.AltMaintenanceReputation = 1
AiPlayerbot.AltMaintenanceAttunementQuests = 1 AiPlayerbot.AltMaintenanceAttunementQuests = 1
AiPlayerbot.AltMaintenanceKeyring = 1 AiPlayerbot.AltMaintenanceKeyring = 1
# Enable/Disable autogear command, which automatically upgrades bots' gear
# Enable/Disable autogear command, which automatically upgrades bots' gear; the quality is limited by AutoGearQualityLimit and AutoGearScoreLimit # The quality is limited by AutoGearQualityLimit and AutoGearScoreLimit
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.AutoGearCommand = 1 AiPlayerbot.AutoGearCommand = 1
@@ -548,7 +557,7 @@ AiPlayerbot.AutoGearScoreLimit = 0
# "mana" (bots have infinite mana) # "mana" (bots have infinite mana)
# "power" (bots have infinite energy, rage, and runic power) # "power" (bots have infinite energy, rage, and runic power)
# "taxi" (bots may use all flight paths, though they will not actually learn them) # "taxi" (bots may use all flight paths, though they will not actually learn them)
# "raid" (bots use cheats implemented into raid strategies) # "raid" (bots use cheats implemented into raid strategies (currently only for Ulduar))
# To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi") # To use multiple cheats, separate them by commas below (e.g., to enable all, use "gold,health,mana,power,raid,taxi")
# Default: food, taxi, and raid are enabled # Default: food, taxi, and raid are enabled
AiPlayerbot.BotCheats = "food,taxi,raid" AiPlayerbot.BotCheats = "food,taxi,raid"
@@ -580,6 +589,30 @@ AiPlayerbot.BotTaxiGapJitterMs = 100
# #
#################################################################################################### ####################################################################################################
####################################################################################################
# PROFESSIONS
# Note: Random bots currently do not get professions
#
# Automatically adds the 'master fishing' strategy to bots that have the fishing skill when the bots master fishes.
# Default: 1 (Enabled)
AiPlayerbot.EnableFishingWithMaster = 1
# Distance from itself (in yards) that a bot with a master will search for water to fish
AiPlayerbot.FishingDistanceFromMaster = 10.0
# Distance from itself (in yards) that a bot without a master will search for water to fish
# Currently not relevant since masterless bots will not fish
AiPlayerbot.FishingDistance = 40.0
# Distance from water (in yards) beyond which a bot will remove the 'master fishing' strategy
AiPlayerbot.EndFishingWithMaster = 30.0
#
#
#
####################################################################################################
####################################### #######################################
# # # #
# RANDOMBOT-SPECIFIC SETTINGS # # RANDOMBOT-SPECIFIC SETTINGS #
@@ -606,9 +639,11 @@ AiPlayerbot.RandomBotMaxLevel = 80
AiPlayerbot.SyncLevelWithPlayers = 0 AiPlayerbot.SyncLevelWithPlayers = 0
# Mark many quests ≤ bot level as complete (slows down bot creation) # Mark many quests ≤ bot level as complete (slows down bot creation)
# Default: 0 (disabled)
AiPlayerbot.PreQuests = 0 AiPlayerbot.PreQuests = 0
# Enable LFG for randombots # Enable LFG for randombots
# Default: 1 (enabled)
AiPlayerbot.RandomBotJoinLfg = 1 AiPlayerbot.RandomBotJoinLfg = 1
# Enable/Disable periodic online - offline of randombots to mimic the real-world scenario where not all players are online simultaneously # Enable/Disable periodic online - offline of randombots to mimic the real-world scenario where not all players are online simultaneously
@@ -629,7 +664,8 @@ AiPlayerbot.RandomBotHordeRatio = 50
AiPlayerbot.DisableDeathKnightLogin = 0 AiPlayerbot.DisableDeathKnightLogin = 0
# Enable simulated expansion limitation for talents and glyphs # Enable simulated expansion limitation for talents and glyphs
# If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61 and 7 rows plus the middle talent of the 8th row for bots until level 71 # If enabled, limits talent trees to 5 rows plus the middle talent of the 6th row for bots until level 61
# and 7 rows plus the middle talent of the 8th row for bots from level 61 until level 71
# Default: 0 (disabled) # Default: 0 (disabled)
AiPlayerbot.LimitTalentsExpansion = 0 AiPlayerbot.LimitTalentsExpansion = 0
@@ -698,7 +734,7 @@ AiPlayerbot.RandomGearQualityLimit = 3
# Max iLVL Phase 1(MC, Ony, ZG) = 78 | Phase 2(BWL) = 83 | Phase 2.5(AQ40) = 88 | Phase 3(Naxx40) = 92 # Max iLVL Phase 1(MC, Ony, ZG) = 78 | Phase 2(BWL) = 83 | Phase 2.5(AQ40) = 88 | Phase 3(Naxx40) = 92
# TBC # TBC
# Max iLVL Tier 4 = 120 | Tier 5 = 133 | Tier 6 = 164 # Max iLVL Tier 4 = 120 | Tier 5 = 133 | Tier 6 = 164
# Max iLVL Phase 1(Kara, Gruul, Mag) = 125 | Phase 1.5(ZA) = 138 | Phase 2(SC, TK) = 141 | Phase 3(Hyjal, BT) = 156 | Phase 4(Sunwell) = 164 # Max iLVL Phase 1(Kara, Gruul, Mag) = 125 | Phase 2(SSC, TK, ZA) = 141 | Phase 3(Hyjal, BT) = 156 | Phase 4(Sunwell) = 164
# Wotlk # Wotlk
# Max iLVL Tier 7(10/25) = 200/213 | Tier 8(10/25) = 225/232 | Tier 9(10/25) = 232/245 | Tier 10(10/25/HC) = 251/264/290 # Max iLVL Tier 7(10/25) = 200/213 | Tier 8(10/25) = 225/232 | Tier 9(10/25) = 232/245 | Tier 10(10/25/HC) = 251/264/290
# Max iLVL Phase 1(Naxx) = 224 | Phase 2(Ulduar) = 245 | Phase 3(ToC) = 258 | Phase 4(ICC) = 290 # Max iLVL Phase 1(Naxx) = 224 | Phase 2(Ulduar) = 245 | Phase 3(ToC) = 258 | Phase 4(ICC) = 290
@@ -709,12 +745,13 @@ AiPlayerbot.RandomGearScoreLimit = 0
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.IncrementalGearInit = 1 AiPlayerbot.IncrementalGearInit = 1
# Set minimum level of bots that will enchant their equipment (if greater than RandomBotMaxlevel, bots will not enchant equipment) # Set minimum level of bots that will enchant and socket gems into their equipment with maintenance
# If greater than RandomBotMaxlevel, bots will not automatically enchant equipment or socket gems
# Default: 60 # Default: 60
AiPlayerbot.MinEnchantingBotLevel = 60 AiPlayerbot.MinEnchantingBotLevel = 60
# Enable expansion limitation for bot enchants # Enable expansion limitation for bot enchants and gems
# If enabled, bots will not use TBC enchants until level 61 or WotLK enchants until level 71 # If enabled, bots will not use TBC enchants until level 61 or WotLK enchants and gems until level 71
# Default: 1 (enabled) # Default: 1 (enabled)
AiPlayerbot.LimitEnchantExpansion = 1 AiPlayerbot.LimitEnchantExpansion = 1
@@ -810,7 +847,7 @@ AiPlayerbot.botActiveAloneSmartScaleWhenMaxLevel = 80
# #
# Quest that will be completed and rewarded for all randombots # Quest that will be completed and rewarded for all randombots
AiPlayerbot.RandomBotQuestIds = "7848,3802,5505,6502,7761,10277,10285,11492,13188,13189,24499,24511,24710,24712" AiPlayerbot.RandomBotQuestIds = "3802,5505,6502,7761,7848,10277,10285,11492,13188,13189,24499,24511,24710,24712"
# Randombots will group with nearby randombots to do shared quests # Randombots will group with nearby randombots to do shared quests
AiPlayerbot.RandomBotGroupNearby = 0 AiPlayerbot.RandomBotGroupNearby = 0
@@ -861,19 +898,21 @@ AiPlayerbot.OpenGoSpell = 6477
# #
# Additional randombot strategies # Additional randombot strategies
# Strategies added here are applied to all randombots, in addition (or subtraction) to spec/role-based default strategies. These rules are processed after the defaults. # Strategies added here are applied to all randombots, in addition (or subtraction) to spec/role-based default strategies.
# These rules are processed after the defaults.
# Example: "+threat,-potions" # Example: "+threat,-potions"
AiPlayerbot.RandomBotCombatStrategies = "" AiPlayerbot.RandomBotCombatStrategies = ""
AiPlayerbot.RandomBotNonCombatStrategies = "" AiPlayerbot.RandomBotNonCombatStrategies = ""
# Additional altbot strategies # Additional altbot strategies
# Strategies added here are applied to all altbots, in addition (or subtraction) to spec/role-based default strategies. These rules are processed after the defaults. # Strategies added here are applied to all altbots, in addition (or subtraction) to spec/role-based default strategies.
# These rules are processed after the defaults.
AiPlayerbot.CombatStrategies = "" AiPlayerbot.CombatStrategies = ""
AiPlayerbot.NonCombatStrategies = "" AiPlayerbot.NonCombatStrategies = ""
# Remove "healer dps" strategy on specified maps. # Remove "healer dps" strategy on the maps specified below.
# Default: 0 (disabled) # Default: 1 (enabled)
AiPlayerbot.HealerDPSMapRestriction = 0 AiPlayerbot.HealerDPSMapRestriction = 1
# List of Map IDs where "healer dps" strategy will be removed if AiPlayerbot.HealerDPSMapRestriction is enabled # List of Map IDs where "healer dps" strategy will be removed if AiPlayerbot.HealerDPSMapRestriction is enabled
# Default: (Dungeon and Raid maps) "33,34,36,43,47,48,70,90,109,129,209,229,230,329,349,389,429,1001,1004,1007,269,540,542,543,545,546,547,552,553,554,555,556,557,558,560,585,574,575,576,578,595,599,600,601,602,604,608,619,632,650,658,668,409,469,509,531,532,534,544,548,550,564,565,580,249,533,603,615,616,624,631,649,724" # Default: (Dungeon and Raid maps) "33,34,36,43,47,48,70,90,109,129,209,229,230,329,349,389,429,1001,1004,1007,269,540,542,543,545,546,547,552,553,554,555,556,557,558,560,585,574,575,576,578,595,599,600,601,602,604,608,619,632,650,658,668,409,469,509,531,532,534,544,548,550,564,565,580,249,533,603,615,616,624,631,649,724"
@@ -1057,7 +1096,8 @@ AiPlayerbot.ZoneBracket.4197 = 79,80
# #
# #
# Maps where bots can be teleported to # Map IDs where bots can be teleported to
# Defaults: 0 = Eastern Kingdoms, 1 = Kalimdor, 530 = Outland, 571 = Northrend
AiPlayerbot.RandomBotMaps = 0,1,530,571 AiPlayerbot.RandomBotMaps = 0,1,530,571
# Probabilty bots teleport to banker (city) # Probabilty bots teleport to banker (city)
@@ -1182,7 +1222,7 @@ AiPlayerbot.DeleteRandomBotArenaTeams = 0
AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951" AiPlayerbot.PvpProhibitedZoneIds = "2255,656,2361,2362,2363,976,35,2268,3425,392,541,1446,3828,3712,3738,3565,3539,3623,4152,3988,4658,4284,4418,4436,4275,4323,4395,3703,4298,3951"
# PvP Restricted Areas (bots don't pvp) # PvP Restricted Areas (bots don't pvp)
AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754" AiPlayerbot.PvpProhibitedAreaIds = "976,35,392,2268,4161,4010,4317,4312,3649,3887,3958,3724,4080,3938,3754,3786,3973"
# Improve reaction speeds in battlegrounds and arenas (may cause lag) # Improve reaction speeds in battlegrounds and arenas (may cause lag)
AiPlayerbot.FastReactInBG = 1 AiPlayerbot.FastReactInBG = 1
@@ -1193,24 +1233,46 @@ AiPlayerbot.FastReactInBG = 1
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# INTERVALS # RANDOM BOT TIMING AND BEHAVIOR
# #
# #
# All in seconds # How often (in seconds) the random bot manager runs its main update loop
# Default: 20
AiPlayerbot.RandomBotUpdateInterval = 20 AiPlayerbot.RandomBotUpdateInterval = 20
# Minimum and maximum seconds before the manager re-evaluates and adjusts total random bot count
# Defaults: 1800 (min), 7200 (max)
AiPlayerbot.RandomBotCountChangeMinInterval = 1800 AiPlayerbot.RandomBotCountChangeMinInterval = 1800
AiPlayerbot.RandomBotCountChangeMaxInterval = 7200 AiPlayerbot.RandomBotCountChangeMaxInterval = 7200
# Minimum and maximum seconds a random bot will stay online before logging out
# Defaults: 600 (min), 28800 (max)
AiPlayerbot.MinRandomBotInWorldTime = 600 AiPlayerbot.MinRandomBotInWorldTime = 600
AiPlayerbot.MaxRandomBotInWorldTime = 28800 AiPlayerbot.MaxRandomBotInWorldTime = 28800
# Minimum and maximum seconds before a bot is eligible for re-randomization (gear, class, talents, etc.)
# Defaults: 7200 (min), 1209600 (max)
AiPlayerbot.MinRandomBotRandomizeTime = 7200 AiPlayerbot.MinRandomBotRandomizeTime = 7200
AiPlayerbot.MaxRandomBotRandomizeTime = 1209600 AiPlayerbot.MaxRandomBotRandomizeTime = 1209600
# Number of bots processed (login, logout, update) per manager update cycle
# Default: 60
AiPlayerbot.RandomBotsPerInterval = 60 AiPlayerbot.RandomBotsPerInterval = 60
# Minimum and maximum seconds after death before a bot revives
# Defaults: 60 (min), 300 (max)
AiPlayerbot.MinRandomBotReviveTime = 60 AiPlayerbot.MinRandomBotReviveTime = 60
AiPlayerbot.MaxRandomBotReviveTime = 300 AiPlayerbot.MaxRandomBotReviveTime = 300
# Minimum and maximum seconds between bot teleports to new areas or zones
# Defaults: 3600 (min), 18000 (max)
AiPlayerbot.MinRandomBotTeleportInterval = 3600 AiPlayerbot.MinRandomBotTeleportInterval = 3600
AiPlayerbot.MaxRandomBotTeleportInterval = 18000 AiPlayerbot.MaxRandomBotTeleportInterval = 18000
AiPlayerbot.PermanantlyInWorldTime = 31104000
# Number of seconds bots flagged as permanent stay in the world (31,104,000 ≈ 1 year)
# Default: 31104000
AiPlayerbot.PermanentlyInWorldTime = 31104000
# #
# #
@@ -1430,7 +1492,7 @@ AiPlayerbot.PremadeSpecLink.5.5.80 = 50332031003--005323241223112003102311351
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# DEATHKNIGHT # DEATH KNIGHT
# #
# #
@@ -1753,7 +1815,7 @@ AiPlayerbot.RandomClassSpecIndex.5.2 = 2
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
# DEATHKNIGHT # DEATH KNIGHT
# #
# #

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
DELETE FROM ai_playerbot_texts WHERE name IN (
'pet_usage_error',
'pet_no_pet_error',
'pet_stance_report',
'pet_no_target_error',
'pet_target_dead_error',
'pet_invalid_target_error',
'pet_pvp_prohibited_error',
'pet_attack_success',
'pet_attack_failed',
'pet_follow_success',
'pet_stay_success',
'pet_unknown_command_error',
'pet_stance_set_success',
'pet_type_pet',
'pet_type_guardian',
'pet_stance_aggressive',
'pet_stance_defensive',
'pet_stance_passive',
'pet_stance_unknown'
);
DELETE FROM ai_playerbot_texts_chance WHERE name IN (
'pet_usage_error',
'pet_no_pet_error',
'pet_stance_report',
'pet_no_target_error',
'pet_target_dead_error',
'pet_invalid_target_error',
'pet_pvp_prohibited_error',
'pet_attack_success',
'pet_attack_failed',
'pet_follow_success',
'pet_stay_success',
'pet_unknown_command_error',
'pet_stance_set_success',
'pet_type_pet',
'pet_type_guardian',
'pet_stance_aggressive',
'pet_stance_defensive',
'pet_stance_passive',
'pet_stance_unknown'
);
INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES
(1717, 'pet_usage_error', "Usage: pet <aggressive|defensive|passive|stance|attack|follow|stay>", 0, 0,
"사용법: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Utilisation: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Verwendung: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Использование: pet <aggressive|defensive|passive|stance|attack|follow|stay>"),
(1718, 'pet_no_pet_error', "You have no pet or guardian pet.", 0, 0,
"펫이나 수호자 펫이 없습니다.",
"Vous n'avez pas de familier ou gardien.",
"Du hast kein Tier oder Wächter.",
"你没有宠物或守护者宠物。",
"你沒有寵物或守護者寵物。",
"No tienes mascota o mascota guardián.",
"No tienes mascota o mascota guardián.",
"У вас нет питомца или защитника."),
(1719, 'pet_stance_report', "Current stance of %type \"%name\": %stance.", 0, 0,
"%type \"%name\"의 현재 태세: %stance.",
"Position actuelle du %type \"%name\": %stance.",
"Aktuelle Haltung des %type \"%name\": %stance.",
"%type \"%name\" 的当前姿态: %stance。",
"%type \"%name\" 的當前姿態: %stance。",
"Postura actual del %type \"%name\": %stance.",
"Postura actual del %type \"%name\": %stance.",
"Текущая позиция %type \"%name\": %stance."),
(1720, 'pet_no_target_error', "No valid target selected by master.", 0, 0,
"주인이 유효한 대상을 선택하지 않았습니다.",
"Aucune cible valide sélectionnée par le maître.",
"Kein gültiges Ziel vom Meister ausgewählt.",
"主人未选择有效目标。",
"主人未選擇有效目標。",
"No hay objetivo válido seleccionado por el maestro.",
"No hay objetivo válido seleccionado por el maestro.",
"Хозяин не выбрал действительную цель."),
(1721, 'pet_target_dead_error', "Target is not alive.", 0, 0,
"대상이 살아있지 않습니다.",
"La cible n'est pas vivante.",
"Das Ziel ist nicht am Leben.",
"目标未存活。",
"目標未存活。",
"El objetivo no está vivo.",
"El objetivo no está vivo.",
"Цель не жива."),
(1722, 'pet_invalid_target_error', "Target is not a valid attack target for the bot.", 0, 0,
"대상이 봇에게 유효한 공격 대상이 아닙니다.",
"La cible n'est pas une cible d'attaque valide pour le bot.",
"Das Ziel ist kein gültiges Angriffsziel für den Bot.",
"目标不是机器人的有效攻击目标。",
"目標不是機器人的有效攻擊目標。",
"El objetivo no es un objetivo de ataque válido para el bot.",
"El objetivo no es un objetivo de ataque válido para el bot.",
"Цель не является допустимой целью атаки для бота."),
(1723, 'pet_pvp_prohibited_error', "I cannot command my pet to attack players in PvP prohibited areas.", 0, 0,
"PvP 금지 지역에서는 펫에게 플레이어 공격 명령을 내릴 수 없습니다.",
"Je ne peux pas commander à mon familier d'attaquer des joueurs dans les zones où le PvP est interdit.",
"Ich kann meinem Tier nicht befehlen, Spieler in PvP-verbotenen Gebieten anzugreifen.",
"我不能命令我的宠物在禁止PvP的区域攻击玩家。",
"我不能命令我的寵物在禁止PvP的區域攻擊玩家。",
"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.",
"No puedo ordenar a mi mascota atacar jugadores en áreas donde el PvP está prohibido.",
"Я не могу приказать своему питомцу атаковать игроков в зонах, где PvP запрещено."),
(1724, 'pet_attack_success', "Pet commanded to attack your target.", 0, 0,
"펫이 당신의 대상을 공격하도록 명령했습니다.",
"Le familier a reçu l'ordre d'attaquer votre cible.",
"Tier wurde befohlen, dein Ziel anzugreifen.",
"宠物已命令攻击你的目标。",
"寵物已命令攻擊你的目標。",
"Mascota ordenada a atacar tu objetivo.",
"Mascota ordenada a atacar tu objetivo.",
"Питомцу приказано атаковать вашу цель."),
(1725, 'pet_attack_failed', "Pet did not attack. (Already attacking or unable to attack target)", 0, 0,
"펫이 공격하지 않았습니다. (이미 공격 중이거나 대상 공격 불가)",
"Le familier n'a pas attaqué. (Attaque déjà en cours ou impossible d'attaquer la cible)",
"Tier hat nicht angegriffen. (Greift bereits an oder kann Ziel nicht angreifen)",
"宠物未攻击。(已在攻击或无法攻击目标)",
"寵物未攻擊。(已在攻擊或無法攻擊目標)",
"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)",
"La mascota no atacó. (Ya está atacando o no puede atacar al objetivo)",
"Питомец не атаковал. (Уже атакует или не может атаковать цель)"),
(1726, 'pet_follow_success', "Pet commanded to follow.", 0, 0,
"펫이 따라오도록 명령했습니다.",
"Le familier a reçu l'ordre de suivre.",
"Tier wurde befohlen zu folgen.",
"宠物已命令跟随。",
"寵物已命令跟隨。",
"Mascota ordenada a seguir.",
"Mascota ordenada a seguir.",
"Питомцу приказано следовать."),
(1727, 'pet_stay_success', "Pet commanded to stay.", 0, 0,
"펫이 머물도록 명령했습니다.",
"Le familier a reçu l'ordre de rester.",
"Tier wurde befohlen zu bleiben.",
"宠物已命令停留。",
"寵物已命令停留。",
"Mascota ordenada a quedarse.",
"Mascota ordenada a quedarse.",
"Питомцу приказано остаться."),
(1728, 'pet_unknown_command_error', "Unknown pet command: %param. Use: pet <aggressive|defensive|passive|stance|attack|follow|stay>", 0, 0,
"알 수 없는 펫 명령: %param. 사용법: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Commande de familier inconnue: %param. Utilisation: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Unbekannter Tierbefehl: %param. Verwendung: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"未知宠物命令: %param。用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"未知寵物命令: %param。用法: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Comando de mascota desconocido: %param. Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Comando de mascota desconocido: %param. Uso: pet <aggressive|defensive|passive|stance|attack|follow|stay>",
"Неизвестная команда питомца: %param. Использование: pet <aggressive|defensive|passive|stance|attack|follow|stay>"),
(1729, 'pet_stance_set_success', "Pet stance set to %stance.", 0, 0,
"펫 태세가 %stance(으)로 설정되었습니다.",
"Position du familier définie sur %stance.",
"Tierhaltung auf %stance gesetzt.",
"宠物姿态设置为 %stance。",
"寵物姿態設置為 %stance。",
"Postura de mascota establecida en %stance.",
"Postura de mascota establecida en %stance.",
"Позиция питомца установлена на %stance."),
(1730, 'pet_type_pet', "pet", 0, 0,
"",
"familier",
"Tier",
"宠物",
"寵物",
"mascota",
"mascota",
"питомец"),
(1731, 'pet_type_guardian', "guardian", 0, 0,
"수호자",
"gardien",
"Wächter",
"守护者",
"守護者",
"guardián",
"guardián",
"защитник"),
(1732, 'pet_stance_aggressive', "aggressive", 0, 0,
"공격적",
"agressif",
"aggressiv",
"进攻",
"進攻",
"agresivo",
"agresivo",
"агрессивная"),
(1733, 'pet_stance_defensive', "defensive", 0, 0,
"방어적",
"défensif",
"defensiv",
"防御",
"防禦",
"defensivo",
"defensivo",
"защитная"),
(1734, 'pet_stance_passive', "passive", 0, 0,
"수동적",
"passif",
"passiv",
"被动",
"被動",
"pasivo",
"pasivo",
"пассивная"),
(1735, 'pet_stance_unknown', "unknown", 0, 0,
"알 수 없음",
"inconnu",
"unbekannt",
"未知",
"未知",
"desconocido",
"desconocido",
"неизвестная");
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES
('pet_usage_error', 100),
('pet_no_pet_error', 100),
('pet_stance_report', 100),
('pet_no_target_error', 100),
('pet_target_dead_error', 100),
('pet_invalid_target_error', 100),
('pet_pvp_prohibited_error', 100),
('pet_attack_success', 100),
('pet_attack_failed', 100),
('pet_follow_success', 100),
('pet_stay_success', 100),
('pet_unknown_command_error', 100),
('pet_stance_set_success', 100),
('pet_type_pet', 100),
('pet_type_guardian', 100),
('pet_stance_aggressive', 100),
('pet_stance_defensive', 100),
('pet_stance_passive', 100),
('pet_stance_unknown', 100);

View File

@@ -0,0 +1,15 @@
DELETE FROM ai_playerbot_texts WHERE name IN ('no_fishing_pole_error');
DELETE FROM ai_playerbot_texts_chance WHERE name IN ('no_fishing_pole_error');
INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES
(1736, 'no_fishing_pole_error', "I don't have a Fishing Pole", 0, 0,
"낚싯대가 없습니다",
"Je nai pas de canne à pêche",
"Ich habe keine Angelrute",
"我沒有釣魚竿",
"我没有钓鱼竿",
"No tengo una caña de pescar",
"No tengo una caña de pescar",
"У меня нет удочки");
INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('no_fishing_pole_error', 100);

View File

@@ -46,7 +46,6 @@
#include "OutfitAction.h" #include "OutfitAction.h"
#include "PositionAction.h" #include "PositionAction.h"
#include "DropQuestAction.h" #include "DropQuestAction.h"
#include "RaidNaxxActions.h"
#include "RandomBotUpdateAction.h" #include "RandomBotUpdateAction.h"
#include "ReachTargetActions.h" #include "ReachTargetActions.h"
#include "ReleaseSpiritAction.h" #include "ReleaseSpiritAction.h"
@@ -64,6 +63,7 @@
#include "WorldBuffAction.h" #include "WorldBuffAction.h"
#include "XpGainAction.h" #include "XpGainAction.h"
#include "NewRpgAction.h" #include "NewRpgAction.h"
#include "FishingAction.h"
#include "CancelChannelAction.h" #include "CancelChannelAction.h"
class PlayerbotAI; class PlayerbotAI;
@@ -121,7 +121,7 @@ public:
creators["shoot"] = &ActionContext::shoot; creators["shoot"] = &ActionContext::shoot;
creators["follow"] = &ActionContext::follow; creators["follow"] = &ActionContext::follow;
creators["move from group"] = &ActionContext::move_from_group; creators["move from group"] = &ActionContext::move_from_group;
creators["flee to master"] = &ActionContext::flee_to_master; creators["flee to group leader"] = &ActionContext::flee_to_group_leader;
creators["runaway"] = &ActionContext::runaway; creators["runaway"] = &ActionContext::runaway;
creators["stay"] = &ActionContext::stay; creators["stay"] = &ActionContext::stay;
creators["sit"] = &ActionContext::sit; creators["sit"] = &ActionContext::sit;
@@ -191,6 +191,11 @@ public:
creators["buy tabard"] = &ActionContext::buy_tabard; creators["buy tabard"] = &ActionContext::buy_tabard;
creators["guild manage nearby"] = &ActionContext::guild_manage_nearby; creators["guild manage nearby"] = &ActionContext::guild_manage_nearby;
creators["clean quest log"] = &ActionContext::clean_quest_log; creators["clean quest log"] = &ActionContext::clean_quest_log;
creators["move near water"] = &ActionContext::move_near_water;
creators["go fishing"] = &ActionContext::go_fishing;
creators["use fishing bobber"] = &ActionContext::use_fishing_bobber;
creators["end master fishing"] = &ActionContext::end_master_fishing;
creators["remove bobber strategy"] = &ActionContext::remove_bobber_strategy;
creators["roll"] = &ActionContext::roll_action; creators["roll"] = &ActionContext::roll_action;
creators["cancel channel"] = &ActionContext::cancel_channel; creators["cancel channel"] = &ActionContext::cancel_channel;
@@ -318,7 +323,7 @@ private:
static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); } static Action* runaway(PlayerbotAI* botAI) { return new RunAwayAction(botAI); }
static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); } static Action* follow(PlayerbotAI* botAI) { return new FollowAction(botAI); }
static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); } static Action* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupAction(botAI); }
static Action* flee_to_master(PlayerbotAI* botAI) { return new FleeToMasterAction(botAI); } static Action* flee_to_group_leader(PlayerbotAI* botAI) { return new FleeToGroupLeaderAction(botAI); }
static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); } static Action* add_gathering_loot(PlayerbotAI* botAI) { return new AddGatheringLootAction(botAI); }
static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); } static Action* add_loot(PlayerbotAI* botAI) { return new AddLootAction(botAI); }
static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); } static Action* add_all_loot(PlayerbotAI* botAI) { return new AddAllLootAction(botAI); }
@@ -380,6 +385,11 @@ private:
static Action* buy_tabard(PlayerbotAI* botAI) { return new BuyTabardAction(botAI); } static Action* buy_tabard(PlayerbotAI* botAI) { return new BuyTabardAction(botAI); }
static Action* guild_manage_nearby(PlayerbotAI* botAI) { return new GuildManageNearbyAction(botAI); } static Action* guild_manage_nearby(PlayerbotAI* botAI) { return new GuildManageNearbyAction(botAI); }
static Action* clean_quest_log(PlayerbotAI* botAI) { return new CleanQuestLogAction(botAI); } static Action* clean_quest_log(PlayerbotAI* botAI) { return new CleanQuestLogAction(botAI); }
static Action* move_near_water(PlayerbotAI* botAI) { return new MoveNearWaterAction(botAI); }
static Action* go_fishing(PlayerbotAI* botAI) { return new FishingAction(botAI);}
static Action* use_fishing_bobber(PlayerbotAI* botAI) { return new UseBobberAction(botAI);}
static Action* end_master_fishing(PlayerbotAI* botAI) { return new EndMasterFishingAction(botAI); }
static Action* remove_bobber_strategy(PlayerbotAI* botAI) { return new RemoveBobberStrategyAction(botAI); }
static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); }
// BG Tactics // BG Tactics

View File

@@ -49,7 +49,7 @@ bool AcceptInvitationAction::Execute(Event event)
if (sRandomPlayerbotMgr->IsRandomBot(bot)) if (sRandomPlayerbotMgr->IsRandomBot(bot))
botAI->SetMaster(inviter); botAI->SetMaster(inviter);
// else // else
// sPlayerbotDbStore->Save(botAI); // sPlayerbotRepository->Save(botAI);
botAI->ResetStrategies(); botAI->ResetStrategies();
botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT); botAI->ChangeStrategy("+follow,-lfg,-bg", BOT_STATE_NON_COMBAT);
@@ -59,7 +59,7 @@ bool AcceptInvitationAction::Execute(Event event)
if (sPlayerbotAIConfig->summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig->sightDistance) if (sPlayerbotAIConfig->summonWhenGroup && bot->GetDistance(inviter) > sPlayerbotAIConfig->sightDistance)
{ {
Teleport(inviter, bot); Teleport(inviter, bot, true);
} }
return true; return true;
} }

View File

@@ -23,7 +23,5 @@ bool AcceptResurrectAction::Execute(Event event)
packet << uint8(1); // accept packet << uint8(1); // accept
bot->GetSession()->HandleResurrectResponseOpcode(packet); // queue the packet to get around race condition bot->GetSession()->HandleResurrectResponseOpcode(packet); // queue the packet to get around race condition
botAI->ChangeEngine(BOT_STATE_NON_COMBAT);
return true; return true;
} }

View File

@@ -40,7 +40,14 @@ bool ReachAreaTriggerAction::Execute(Event event)
return true; return true;
} }
bot->GetMotionMaster()->MovePoint(at->map, at->x, at->y, at->z); bot->GetMotionMaster()->MovePoint(
/*id*/ at->map,
/*coords*/ at->x, at->y, at->z,
/*forcedMovement*/ FORCED_MOVEMENT_NONE,
/*speed*/ 0.0f, // default speed (not handled here)
/*orientation*/ 0.0f, // keep current orientation of bot
/*generatePath*/ true, // true => terrain path (2d mmap); false => straight spline (3d vmap)
/*forceDestination*/ false);
float distance = bot->GetDistance(at->x, at->y, at->z); float distance = bot->GetDistance(at->x, at->y, at->z);
float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig->reactDelay; float delay = 1000.0f * distance / bot->GetSpeed(MOVE_RUN) + sPlayerbotAIConfig->reactDelay;

View File

@@ -15,20 +15,19 @@
#include "SharedDefines.h" #include "SharedDefines.h"
#include "Unit.h" #include "Unit.h"
bool AttackAction::Execute(Event event) bool AttackAction::Execute(Event /*event*/)
{ {
Unit* target = GetTarget(); Unit* target = GetTarget();
if (!target) if (!target)
return false; return false;
if (!target->IsInWorld()) if (!target->IsInWorld())
{
return false; return false;
}
return Attack(target); return Attack(target);
} }
bool AttackMyTargetAction::Execute(Event event) bool AttackMyTargetAction::Execute(Event /*event*/)
{ {
Player* master = GetMaster(); Player* master = GetMaster();
if (!master) if (!master)
@@ -51,7 +50,7 @@ bool AttackMyTargetAction::Execute(Event event)
return result; return result;
} }
bool AttackAction::Attack(Unit* target, bool with_pet /*true*/) bool AttackAction::Attack(Unit* target, bool /*with_pet*/ /*true*/)
{ {
Unit* oldTarget = context->GetValue<Unit*>("current target")->Get(); Unit* oldTarget = context->GetValue<Unit*>("current target")->Get();
bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot); bool shouldMelee = bot->IsWithinMeleeRange(target) || botAI->IsMelee(bot);
@@ -81,12 +80,15 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is no longer in the world."); botAI->TellError(std::string(target->GetName()) + " is no longer in the world.");
return false; return false;
} }
if ((sPlayerbotAIConfig->IsInPvpProhibitedZone(bot->GetZoneId()) || // Check if bot OR target is in prohibited zone/area (skip for duels)
sPlayerbotAIConfig->IsInPvpProhibitedArea(bot->GetAreaId())) if ((target->IsPlayer() || target->IsPet()) &&
&& (target->IsPlayer() || target->IsPet())) (!bot->duel || bot->duel->Opponent != target) &&
(sPlayerbotAIConfig->IsPvpProhibited(bot->GetZoneId(), bot->GetAreaId()) ||
sPlayerbotAIConfig->IsPvpProhibited(target->GetZoneId(), target->GetAreaId())))
{ {
if (verbose) if (verbose)
botAI->TellError("I cannot attack other players in PvP prohibited areas."); botAI->TellError("I cannot attack other players in PvP prohibited areas.");
@@ -98,6 +100,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is friendly to me."); botAI->TellError(std::string(target->GetName()) + " is friendly to me.");
return false; return false;
} }
@@ -105,6 +108,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is dead."); botAI->TellError(std::string(target->GetName()) + " is dead.");
return false; return false;
} }
@@ -112,6 +116,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError(std::string(target->GetName()) + " is not in my sight."); botAI->TellError(std::string(target->GetName()) + " is not in my sight.");
return false; return false;
} }
@@ -119,6 +124,7 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
{ {
if (verbose) if (verbose)
botAI->TellError("I am already attacking " + std::string(target->GetName()) + "."); botAI->TellError("I am already attacking " + std::string(target->GetName()) + ".");
return false; return false;
} }
@@ -153,25 +159,28 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
bot->StopMoving(); bot->StopMoving();
} }
if (IsMovingAllowed() && !bot->HasInArc(CAST_ANGLE_IN_FRONT, target)) if (botAI->CanMove() && !bot->HasInArc(CAST_ANGLE_IN_FRONT, target))
{
sServerFacade->SetFacingTo(bot, target); sServerFacade->SetFacingTo(bot, target);
}
botAI->ChangeEngine(BOT_STATE_COMBAT); botAI->ChangeEngine(BOT_STATE_COMBAT);
bot->Attack(target, shouldMelee); bot->Attack(target, shouldMelee);
/* prevent pet dead immediately in group */ /* prevent pet dead immediately in group */
// if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat()) { // if (bot->GetMap()->IsDungeon() && bot->GetGroup() && !target->IsInCombat())
// {
// with_pet = false; // with_pet = false;
// } // }
// if (Pet* pet = bot->GetPet()) // if (Pet* pet = bot->GetPet())
// { // {
// if (with_pet) { // if (with_pet)
// {
// pet->SetReactState(REACT_DEFENSIVE); // pet->SetReactState(REACT_DEFENSIVE);
// pet->SetTarget(target->GetGUID()); // pet->SetTarget(target->GetGUID());
// pet->GetCharmInfo()->SetIsCommandAttack(true); // pet->GetCharmInfo()->SetIsCommandAttack(true);
// pet->AI()->AttackStart(target); // pet->AI()->AttackStart(target);
// } else { // }
// else
// {
// pet->SetReactState(REACT_PASSIVE); // pet->SetReactState(REACT_PASSIVE);
// pet->GetCharmInfo()->SetIsCommandFollow(true); // pet->GetCharmInfo()->SetIsCommandFollow(true);
// pet->GetCharmInfo()->IsReturning(); // pet->GetCharmInfo()->IsReturning();
@@ -182,4 +191,4 @@ bool AttackAction::Attack(Unit* target, bool with_pet /*true*/)
bool AttackDuelOpponentAction::isUseful() { return AI_VALUE(Unit*, "duel target"); } bool AttackDuelOpponentAction::isUseful() { return AI_VALUE(Unit*, "duel target"); }
bool AttackDuelOpponentAction::Execute(Event event) { return Attack(AI_VALUE(Unit*, "duel target")); } bool AttackDuelOpponentAction::Execute(Event /*event*/) { return Attack(AI_VALUE(Unit*, "duel target")); }

View File

@@ -75,37 +75,80 @@ void AutoMaintenanceOnLevelupAction::LearnSpells(std::ostringstream* out)
void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out) void AutoMaintenanceOnLevelupAction::LearnTrainerSpells(std::ostringstream* out)
{ {
PlayerbotFactory factory(bot, bot->GetLevel()); PlayerbotFactory factory(bot, bot->GetLevel());
factory.InitSkills();
factory.InitClassSpells(); factory.InitClassSpells();
factory.InitAvailableSpells(); factory.InitAvailableSpells();
factory.InitSkills();
factory.InitPet(); factory.InitPet();
} }
void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out) void AutoMaintenanceOnLevelupAction::LearnQuestSpells(std::ostringstream* out)
{ {
// CreatureTemplate const* co = sCreatureStorage.LookupEntry<CreatureTemplate>(id);
ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates(); ObjectMgr::QuestMap const& questTemplates = sObjectMgr->GetQuestTemplates();
for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i) for (ObjectMgr::QuestMap::const_iterator i = questTemplates.begin(); i != questTemplates.end(); ++i)
{ {
//uint32 questId = i->first; //not used, line marked for removal.
Quest const* quest = i->second; Quest const* quest = i->second;
if (!quest->GetRequiredClasses() || quest->IsRepeatable() || quest->GetMinLevel() < 10) // only process class-specific quests to learn class-related spells, cuz
// we don't want all these bunch of entries to be handled!
if (!quest->GetRequiredClasses())
continue; continue;
if (!bot->SatisfyQuestClass(quest, false) || quest->GetMinLevel() > bot->GetLevel() || // skip quests that are repeatable, too low level, or above bots' level
!bot->SatisfyQuestRace(quest, false)) if (quest->IsRepeatable() || quest->GetMinLevel() < 10 || quest->GetMinLevel() > bot->GetLevel())
continue; continue;
if (quest->GetRewSpellCast() > 0) // skip if bot doesnt satisfy class, race, or skill requirements
if (!bot->SatisfyQuestClass(quest, false) || !bot->SatisfyQuestRace(quest, false) ||
!bot->SatisfyQuestSkill(quest, false))
continue;
// use the same logic and impl from Player::learnQuestRewardedSpells
int32 spellId = quest->GetRewSpellCast();
if (!spellId)
continue;
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId);
if (!spellInfo)
continue;
// xinef: find effect with learn spell and check if we have this spell
bool found = false;
for (uint8 i = 0; i < MAX_SPELL_EFFECTS; ++i)
{ {
LearnSpell(quest->GetRewSpellCast(), out); if (spellInfo->Effects[i].Effect == SPELL_EFFECT_LEARN_SPELL && spellInfo->Effects[i].TriggerSpell &&
} !bot->HasSpell(spellInfo->Effects[i].TriggerSpell))
else if (quest->GetRewSpell() > 0)
{ {
LearnSpell(quest->GetRewSpell(), out); // pusywizard: don't re-add profession specialties!
if (SpellInfo const* triggeredInfo = sSpellMgr->GetSpellInfo(spellInfo->Effects[i].TriggerSpell))
if (triggeredInfo->Effects[0].Effect == SPELL_EFFECT_TRADE_SKILL)
break; // pussywizard: break and not cast the spell (found is false)
found = true;
break;
} }
} }
// xinef: we know the spell, continue
if (!found)
continue;
bot->CastSpell(bot, spellId, true);
// Check if RewardDisplaySpell is set to output the proper spell learned
// after processing quests. Output the original RewardSpell otherwise.
uint32 rewSpellId = quest->GetRewSpell();
if (rewSpellId)
{
if (SpellInfo const* rewSpellInfo = sSpellMgr->GetSpellInfo(rewSpellId))
{
*out << FormatSpell(rewSpellInfo) << ", ";
continue;
}
}
*out << FormatSpell(spellInfo) << ", ";
}
} }
std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* sInfo) std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* sInfo)
@@ -121,39 +164,6 @@ std::string const AutoMaintenanceOnLevelupAction::FormatSpell(SpellInfo const* s
return out.str(); return out.str();
} }
void AutoMaintenanceOnLevelupAction::LearnSpell(uint32 spellId, std::ostringstream* out)
{
SpellInfo const* proto = sSpellMgr->GetSpellInfo(spellId);
if (!proto)
return;
bool learned = false;
for (uint8 j = 0; j < 3; ++j)
{
if (proto->Effects[j].Effect == SPELL_EFFECT_LEARN_SPELL)
{
uint32 learnedSpell = proto->Effects[j].TriggerSpell;
if (!bot->HasSpell(learnedSpell))
{
bot->learnSpell(learnedSpell);
*out << FormatSpell(sSpellMgr->GetSpellInfo(learnedSpell)) << ", ";
}
learned = true;
}
}
if (!learned)
{
if (!bot->HasSpell(spellId))
{
bot->learnSpell(spellId);
*out << FormatSpell(proto) << ", ";
}
}
}
void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip() void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
{ {
if (!sPlayerbotAIConfig->autoUpgradeEquip || !sRandomPlayerbotMgr->IsRandomBot(bot)) if (!sPlayerbotAIConfig->autoUpgradeEquip || !sRandomPlayerbotMgr->IsRandomBot(bot))
@@ -176,4 +186,3 @@ void AutoMaintenanceOnLevelupAction::AutoUpgradeEquip()
factory.InitEquipment(true); factory.InitEquipment(true);
} }
} }

View File

@@ -28,7 +28,6 @@ protected:
void LearnSpells(std::ostringstream* out); void LearnSpells(std::ostringstream* out);
void LearnTrainerSpells(std::ostringstream* out); void LearnTrainerSpells(std::ostringstream* out);
void LearnQuestSpells(std::ostringstream* out); void LearnQuestSpells(std::ostringstream* out);
void LearnSpell(uint32 spellId, std::ostringstream* out);
std::string const FormatSpell(SpellInfo const* sInfo); std::string const FormatSpell(SpellInfo const* sInfo);
}; };

View File

@@ -17,7 +17,7 @@ bool BankAction::Execute(Event event)
for (GuidVector::iterator i = npcs.begin(); i != npcs.end(); i++) for (GuidVector::iterator i = npcs.begin(); i != npcs.end(); i++)
{ {
Unit* npc = botAI->GetUnit(*i); Unit* npc = botAI->GetUnit(*i);
if (!npc || !npc->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_BANKER)) if (!npc || !npc->HasNpcFlag(UNIT_NPC_FLAG_BANKER))
continue; continue;
return ExecuteBank(text, npc); return ExecuteBank(text, npc);

View File

@@ -4,10 +4,12 @@
*/ */
#include "BattleGroundTactics.h" #include "BattleGroundTactics.h"
#include "BattleGroundJoinAction.h"
#include <algorithm>
#include "ArenaTeam.h" #include "ArenaTeam.h"
#include "ArenaTeamMgr.h" #include "ArenaTeamMgr.h"
#include "BattleGroundJoinAction.h"
#include "Battleground.h" #include "Battleground.h"
#include "BattlegroundAB.h" #include "BattlegroundAB.h"
#include "BattlegroundAV.h" #include "BattlegroundAV.h"
@@ -22,11 +24,12 @@
#include "BattlegroundSA.h" #include "BattlegroundSA.h"
#include "BattlegroundWS.h" #include "BattlegroundWS.h"
#include "Event.h" #include "Event.h"
#include "GameObject.h"
#include "IVMapMgr.h" #include "IVMapMgr.h"
#include "PathGenerator.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "PositionValue.h" #include "PositionValue.h"
#include "PvpTriggers.h" #include "PvpTriggers.h"
#include "PathGenerator.h"
#include "ServerFacade.h" #include "ServerFacade.h"
#include "Vehicle.h" #include "Vehicle.h"
@@ -1894,13 +1897,13 @@ bool BGTactics::selectObjective(bool reset)
bool isDefender = role < defendersProhab; bool isDefender = role < defendersProhab;
bool isAdvanced = !isDefender && role > 8; bool isAdvanced = !isDefender && role > 8;
const auto& attackObjectives = auto const& attackObjectives =
(team == TEAM_HORDE) ? AV_AttackObjectives_Horde : AV_AttackObjectives_Alliance; (team == TEAM_HORDE) ? AV_AttackObjectives_Horde : AV_AttackObjectives_Alliance;
const auto& defendObjectives = auto const& defendObjectives =
(team == TEAM_HORDE) ? AV_DefendObjectives_Horde : AV_DefendObjectives_Alliance; (team == TEAM_HORDE) ? AV_DefendObjectives_Horde : AV_DefendObjectives_Alliance;
uint32 destroyedNodes = 0; uint32 destroyedNodes = 0;
for (const auto& [nodeId, _] : defendObjectives) for (auto const& [nodeId, _] : defendObjectives)
if (av->GetAVNodeInfo(nodeId).State == POINT_DESTROYED) if (av->GetAVNodeInfo(nodeId).State == POINT_DESTROYED)
destroyedNodes++; destroyedNodes++;
@@ -2000,7 +2003,7 @@ bool BGTactics::selectObjective(bool reset)
std::vector<GameObject*> contestedObjectives; std::vector<GameObject*> contestedObjectives;
std::vector<GameObject*> availableObjectives; std::vector<GameObject*> availableObjectives;
for (const auto& [nodeId, goId] : defendObjectives) for (auto const& [nodeId, goId] : defendObjectives)
{ {
const BG_AV_NodeInfo& node = av->GetAVNodeInfo(nodeId); const BG_AV_NodeInfo& node = av->GetAVNodeInfo(nodeId);
if (node.State == POINT_DESTROYED) if (node.State == POINT_DESTROYED)
@@ -2026,7 +2029,7 @@ bool BGTactics::selectObjective(bool reset)
if (!BgObjective) if (!BgObjective)
{ {
uint32 towersDown = 0; uint32 towersDown = 0;
for (const auto& [nodeId, _] : attackObjectives) for (auto const& [nodeId, _] : attackObjectives)
if (av->GetAVNodeInfo(nodeId).State == POINT_DESTROYED) if (av->GetAVNodeInfo(nodeId).State == POINT_DESTROYED)
towersDown++; towersDown++;
@@ -2053,7 +2056,7 @@ bool BGTactics::selectObjective(bool reset)
{ {
std::vector<GameObject*> candidates; std::vector<GameObject*> candidates;
for (const auto& [nodeId, goId] : attackObjectives) for (auto const& [nodeId, goId] : attackObjectives)
{ {
const BG_AV_NodeInfo& node = av->GetAVNodeInfo(nodeId); const BG_AV_NodeInfo& node = av->GetAVNodeInfo(nodeId);
GameObject* go = bg->GetBGObject(goId); GameObject* go = bg->GetBGObject(goId);
@@ -2105,13 +2108,13 @@ bool BGTactics::selectObjective(bool reset)
Position objPos = BgObjective->GetPosition(); Position objPos = BgObjective->GetPosition();
Optional<uint8> linkedNodeId; Optional<uint8> linkedNodeId;
for (const auto& [nodeId, goId] : attackObjectives) for (auto const& [nodeId, goId] : attackObjectives)
if (bg->GetBGObject(goId) == BgObjective) if (bg->GetBGObject(goId) == BgObjective)
linkedNodeId = nodeId; linkedNodeId = nodeId;
if (!linkedNodeId) if (!linkedNodeId)
{ {
for (const auto& [nodeId, goId] : defendObjectives) for (auto const& [nodeId, goId] : defendObjectives)
if (bg->GetBGObject(goId) == BgObjective) if (bg->GetBGObject(goId) == BgObjective)
linkedNodeId = nodeId; linkedNodeId = nodeId;
} }
@@ -2543,7 +2546,7 @@ bool BGTactics::selectObjective(bool reset)
float bestDist = FLT_MAX; float bestDist = FLT_MAX;
uint32 bestTrigger = 0; uint32 bestTrigger = 0;
for (const auto& [nodeId, _, areaTrigger] : EY_AttackObjectives) for (auto const& [nodeId, _, areaTrigger] : EY_AttackObjectives)
{ {
if (!IsOwned(nodeId)) if (!IsOwned(nodeId))
continue; continue;
@@ -2610,7 +2613,7 @@ bool BGTactics::selectObjective(bool reset)
// --- PRIORITY 2: Nearby unowned contested node --- // --- PRIORITY 2: Nearby unowned contested node ---
if (!foundObjective) if (!foundObjective)
{ {
for (const auto& [nodeId, _, __] : EY_AttackObjectives) for (auto const& [nodeId, _, __] : EY_AttackObjectives)
{ {
if (IsOwned(nodeId)) if (IsOwned(nodeId))
continue; continue;
@@ -2711,7 +2714,7 @@ bool BGTactics::selectObjective(bool reset)
if (!foundObjective && strategy == EY_STRATEGY_FLAG_FOCUS) if (!foundObjective && strategy == EY_STRATEGY_FLAG_FOCUS)
{ {
bool ownsAny = false; bool ownsAny = false;
for (const auto& [nodeId, _, __] : EY_AttackObjectives) for (auto const& [nodeId, _, __] : EY_AttackObjectives)
{ {
if (IsOwned(nodeId)) if (IsOwned(nodeId))
{ {
@@ -2739,7 +2742,7 @@ bool BGTactics::selectObjective(bool reset)
float bestDist = FLT_MAX; float bestDist = FLT_MAX;
Optional<uint32> bestNode; Optional<uint32> bestNode;
for (const auto& [nodeId, _, __] : EY_AttackObjectives) for (auto const& [nodeId, _, __] : EY_AttackObjectives)
{ {
if (IsOwned(nodeId)) if (IsOwned(nodeId))
continue; continue;
@@ -2974,7 +2977,7 @@ bool BGTactics::selectObjective(bool reset)
uint32 len = end(IC_AttackObjectives) - begin(IC_AttackObjectives); uint32 len = end(IC_AttackObjectives) - begin(IC_AttackObjectives);
for (uint32 i = 0; i < len; i++) for (uint32 i = 0; i < len; i++)
{ {
const auto& objective = auto const& objective =
IC_AttackObjectives[(i + role) % IC_AttackObjectives[(i + role) %
len]; // use role to determine which objective checked first len]; // use role to determine which objective checked first
if (isleOfConquestBG->GetICNodePoint(objective.first).nodeState != NODE_STATE_CONTROLLED_H) if (isleOfConquestBG->GetICNodePoint(objective.first).nodeState != NODE_STATE_CONTROLLED_H)
@@ -3126,7 +3129,7 @@ bool BGTactics::selectObjective(bool reset)
uint32 len = end(IC_AttackObjectives) - begin(IC_AttackObjectives); uint32 len = end(IC_AttackObjectives) - begin(IC_AttackObjectives);
for (uint32 i = 0; i < len; i++) for (uint32 i = 0; i < len; i++)
{ {
const auto& objective = auto const& objective =
IC_AttackObjectives[(i + role) % IC_AttackObjectives[(i + role) %
len]; // use role to determine which objective checked first len]; // use role to determine which objective checked first
if (isleOfConquestBG->GetICNodePoint(objective.first).nodeState != NODE_STATE_CONTROLLED_H) if (isleOfConquestBG->GetICNodePoint(objective.first).nodeState != NODE_STATE_CONTROLLED_H)
@@ -3578,6 +3581,16 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
GuidVector closePlayers; GuidVector closePlayers;
float flagRange = 0.0f; float flagRange = 0.0f;
// Eye of the Storm helpers used later when handling capture positioning
BattlegroundEY* eyeBg = nullptr;
GameObject* eyCenterFlag = nullptr;
if (bgType == BATTLEGROUND_EY)
{
eyeBg = static_cast<BattlegroundEY*>(bg);
if (eyeBg)
eyCenterFlag = eyeBg->GetBGObject(BG_EY_OBJECT_FLAG_NETHERSTORM);
}
// Set up appropriate search ranges and object lists based on BG type // Set up appropriate search ranges and object lists based on BG type
switch (bgType) switch (bgType)
{ {
@@ -3607,27 +3620,82 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
if (closeObjects.empty()) if (closeObjects.empty())
return false; return false;
auto keepStationaryWhileCapturing = [&](CurrentSpellTypes spellType)
{
Spell* currentSpell = bot->GetCurrentSpell(spellType);
if (!currentSpell || !currentSpell->m_spellInfo || currentSpell->m_spellInfo->Id != SPELL_CAPTURE_BANNER)
return false;
// If the capture target is no longer available (another bot already captured it), stop channeling
if (GameObject* targetFlag = currentSpell->m_targets.GetGOTarget())
{
if (!targetFlag->isSpawned() || targetFlag->GetGoState() != GO_STATE_READY)
{
bot->InterruptNonMeleeSpells(true);
resetObjective();
return false;
}
}
else
{
bot->InterruptNonMeleeSpells(true);
resetObjective();
return false;
}
if (bot->IsMounted())
{
bot->RemoveAurasByType(SPELL_AURA_MOUNTED);
}
if (bot->IsInDisallowedMountForm())
{
bot->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);
}
if (bot->isMoving())
{
bot->StopMoving();
}
return true;
};
// If we are already channeling the capture spell, keep the bot stationary and dismounted
if (keepStationaryWhileCapturing(CURRENT_CHANNELED_SPELL) || keepStationaryWhileCapturing(CURRENT_GENERIC_SPELL))
return true;
// First identify which flag/base we're trying to interact with // First identify which flag/base we're trying to interact with
GameObject* targetFlag = nullptr; GameObject* targetFlag = nullptr;
for (ObjectGuid const guid : closeObjects) for (ObjectGuid const guid : closeObjects)
{ {
GameObject* go = botAI->GetGameObject(guid); GameObject* go = botAI->GetGameObject(guid);
if (!go) if (!go)
{
continue; continue;
}
bool const isEyCenterFlag = eyeBg && eyCenterFlag && eyCenterFlag->GetGUID() == go->GetGUID();
// Check if this object is a valid capture target // Check if this object is a valid capture target
std::vector<uint32>::const_iterator f = find(vFlagIds.begin(), vFlagIds.end(), go->GetEntry()); std::vector<uint32>::const_iterator f = std::find(vFlagIds.begin(), vFlagIds.end(), go->GetEntry());
if (f == vFlagIds.end()) if (f == vFlagIds.end() && !isEyCenterFlag)
{
continue; continue;
}
// Verify the object is active and ready // Verify the object is active and ready
if (!go->isSpawned() || go->GetGoState() != GO_STATE_READY) if (!go->isSpawned() || go->GetGoState() != GO_STATE_READY)
{
continue; continue;
}
// Check if we're in range (using double range for enemy detection) // Check if we're in range (using double range for enemy detection)
float const dist = bot->GetDistance(go); float const dist = bot->GetDistance(go);
if (flagRange && dist > flagRange * 2.0f) if (flagRange && dist > flagRange * 2.0f)
{
continue; continue;
}
targetFlag = go; targetFlag = go;
break; break;
@@ -3655,7 +3723,7 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
} }
// Check if friendly players are already capturing // Check if friendly players are already capturing
if (!closePlayers.empty()) if (!closePlayers.empty() && bgType != BATTLEGROUND_EY)
{ {
// Track number of friendly players capturing and the closest one // Track number of friendly players capturing and the closest one
uint32 numCapturing = 0; uint32 numCapturing = 0;
@@ -3664,17 +3732,20 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
{ {
if (Unit* pFriend = botAI->GetUnit(guid)) if (Unit* pFriend = botAI->GetUnit(guid))
{ {
// Check if they're casting the capture spell // Check if they're casting or channeling the capture spell
if (Spell* spell = pFriend->GetCurrentSpell(CURRENT_GENERIC_SPELL)) Spell* spell = pFriend->GetCurrentSpell(CURRENT_GENERIC_SPELL);
if (!spell)
{ {
if (spell->m_spellInfo->Id == SPELL_CAPTURE_BANNER) spell = pFriend->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
}
if (spell && spell->m_spellInfo && spell->m_spellInfo->Id == SPELL_CAPTURE_BANNER)
{ {
numCapturing++; numCapturing++;
capturingPlayer = pFriend; capturingPlayer = pFriend;
} }
} }
} }
}
// If friendlies are capturing, stay to defend but don't capture // If friendlies are capturing, stay to defend but don't capture
if (numCapturing > 0 && capturingPlayer && bot->GetGUID() != capturingPlayer->GetGUID()) if (numCapturing > 0 && capturingPlayer && bot->GetGUID() != capturingPlayer->GetGUID())
@@ -3704,9 +3775,11 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
if (!go) if (!go)
continue; continue;
bool const isEyCenterFlag = eyeBg && eyCenterFlag && eyCenterFlag->GetGUID() == go->GetGUID();
// Validate this is a capture target // Validate this is a capture target
std::vector<uint32>::const_iterator f = find(vFlagIds.begin(), vFlagIds.end(), go->GetEntry()); std::vector<uint32>::const_iterator f = std::find(vFlagIds.begin(), vFlagIds.end(), go->GetEntry());
if (f == vFlagIds.end()) if (f == vFlagIds.end() && !isEyCenterFlag)
continue; continue;
// Check object is active // Check object is active
@@ -3722,12 +3795,40 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
continue; continue;
// Special handling for WSG and EY base flags // Special handling for WSG and EY base flags
bool atBase = bgType == BATTLEGROUND_WS ? go->GetEntry() == vFlagsWS[bot->GetTeamId()] bool isWsBaseFlag = bgType == BATTLEGROUND_WS && go->GetEntry() == vFlagsWS[bot->GetTeamId()];
: bgType == BATTLEGROUND_EY ? go->GetEntry() == vFlagsEY[0] bool isEyBaseFlag = bgType == BATTLEGROUND_EY && go->GetEntry() == vFlagsEY[0];
: false;
// Ensure bots are inside the Eye of the Storm capture circle before casting
if (bgType == BATTLEGROUND_EY)
{
GameObject* captureFlag = (isEyBaseFlag && eyCenterFlag) ? eyCenterFlag : go;
float const requiredRange = 2.5f;
if (!bot->IsWithinDistInMap(captureFlag, requiredRange))
{
// Stay mounted while relocating to avoid mount/dismount loops
return MoveTo(bot->GetMapId(), captureFlag->GetPositionX(), captureFlag->GetPositionY(),
captureFlag->GetPositionZ());
}
// Once inside the circle, dismount and stop before starting the channel
if (bot->IsMounted())
{
bot->RemoveAurasByType(SPELL_AURA_MOUNTED);
}
if (bot->IsInDisallowedMountForm())
{
bot->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);
}
if (bot->isMoving())
{
bot->StopMoving();
}
}
// Don't capture own flag in WSG unless carrying enemy flag // Don't capture own flag in WSG unless carrying enemy flag
if (atBase && bgType == BATTLEGROUND_WS && if (isWsBaseFlag && bgType == BATTLEGROUND_WS &&
!(bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) || bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG))) !(bot->HasAura(BG_WS_SPELL_WARSONG_FLAG) || bot->HasAura(BG_WS_SPELL_SILVERWING_FLAG)))
continue; continue;
@@ -3772,7 +3873,7 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
if (dist < INTERACTION_DISTANCE) if (dist < INTERACTION_DISTANCE)
{ {
// Handle flag capture at base // Handle flag capture at base
if (atBase) if (isWsBaseFlag)
{ {
if (bot->GetTeamId() == TEAM_HORDE) if (bot->GetTeamId() == TEAM_HORDE)
{ {
@@ -3811,28 +3912,50 @@ bool BGTactics::atFlag(std::vector<BattleBotPath*> const& vPaths, std::vector<ui
} }
} }
case BATTLEGROUND_EY: case BATTLEGROUND_EY:
{ // Issue: Currently bots in EY take flag instantly without casttime { // Handle Netherstorm flag capture requiring a channel
if (dist < INTERACTION_DISTANCE) if (dist < INTERACTION_DISTANCE)
{ {
// Dismount before interacting // Dismount before interacting
if (bot->IsMounted()) if (bot->IsMounted())
{
bot->RemoveAurasByType(SPELL_AURA_MOUNTED); bot->RemoveAurasByType(SPELL_AURA_MOUNTED);
}
if (bot->IsInDisallowedMountForm()) if (bot->IsInDisallowedMountForm())
{
bot->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT); bot->RemoveAurasByType(SPELL_AURA_MOD_SHAPESHIFT);
}
// Handle center flag differently (requires spell cast) // Handle center flag differently (requires spell cast)
if (atBase) if (isEyCenterFlag)
{ {
for (uint8 type = CURRENT_MELEE_SPELL; type <= CURRENT_CHANNELED_SPELL; ++type)
{
if (Spell* currentSpell = bot->GetCurrentSpell(static_cast<CurrentSpellTypes>(type)))
{
// m_spellInfo may be null in some states: protect access
if (currentSpell->m_spellInfo && currentSpell->m_spellInfo->Id == SPELL_CAPTURE_BANNER)
{
bot->StopMoving();
botAI->SetNextCheckDelay(500);
return true;
}
}
}
SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_CAPTURE_BANNER); SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(SPELL_CAPTURE_BANNER);
if (!spellInfo) if (!spellInfo)
return false; return false;
Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE); Spell* spell = new Spell(bot, spellInfo, TRIGGERED_NONE);
spell->m_targets.SetGOTarget(go); spell->m_targets.SetGOTarget(go);
bot->StopMoving();
spell->prepare(&spell->m_targets); spell->prepare(&spell->m_targets);
botAI->WaitForSpellCast(spell); botAI->WaitForSpellCast(spell);
//return true; Intended to make a bot cast SPELL_CAPTURE_BANNER and wait for spell finish, but doesn't work and causes infinite loop resetObjective();
return true;
} }
// Pick up dropped flag // Pick up dropped flag

View File

@@ -224,33 +224,28 @@ bool BuyAction::Execute(Event event)
bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto) bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, ItemTemplate const* proto)
{ {
uint32 oldCount = AI_VALUE2(uint32, "item count", proto->Name1); if (!tItems || !proto)
if (!tItems)
return false; return false;
uint32 itemId = proto->ItemId; uint32 itemId = proto->ItemId;
for (uint32 slot = 0; slot < tItems->GetItemCount(); slot++) uint32 oldCount = bot->GetItemCount(itemId, false);
{
if (tItems->GetItem(slot)->item == itemId) for (uint32 slot = 0; slot < tItems->GetItemCount(); ++slot)
{ {
if (tItems->GetItem(slot)->item != itemId)
continue;
uint32 botMoney = bot->GetMoney(); uint32 botMoney = bot->GetMoney();
if (botAI->HasCheat(BotCheatMask::gold)) if (botAI->HasCheat(BotCheatMask::gold))
{
bot->SetMoney(10000000); bot->SetMoney(10000000);
}
bot->BuyItemFromVendorSlot(vendorguid, slot, itemId, 1, NULL_BAG, NULL_SLOT); bot->BuyItemFromVendorSlot(vendorguid, slot, itemId, 1, NULL_BAG, NULL_SLOT);
if (botAI->HasCheat(BotCheatMask::gold)) if (botAI->HasCheat(BotCheatMask::gold))
{
bot->SetMoney(botMoney); bot->SetMoney(botMoney);
}
if (oldCount < uint32 newCount = bot->GetItemCount(itemId, false);
AI_VALUE2( if (newCount > oldCount)
uint32, "item count",
proto->Name1)) // BuyItem Always returns false (unless unique) so we have to check the item counts.
{ {
std::ostringstream out; std::ostringstream out;
out << "Buying " << ChatHelper::FormatItem(proto); out << "Buying " << ChatHelper::FormatItem(proto);
@@ -260,7 +255,6 @@ bool BuyAction::BuyItem(VendorItemData const* tItems, ObjectGuid vendorguid, Ite
return false; return false;
} }
}
return false; return false;
} }

View File

@@ -6,7 +6,7 @@
#include "ChangeStrategyAction.h" #include "ChangeStrategyAction.h"
#include "Event.h" #include "Event.h"
#include "PlayerbotDbStore.h" #include "PlayerbotRepository.h"
#include "Playerbots.h" #include "Playerbots.h"
bool ChangeCombatStrategyAction::Execute(Event event) bool ChangeCombatStrategyAction::Execute(Event event)
@@ -24,7 +24,7 @@ bool ChangeCombatStrategyAction::Execute(Event event)
case '+': case '+':
case '-': case '-':
case '~': case '~':
sPlayerbotDbStore->Save(botAI); sPlayerbotRepository->Save(botAI);
break; break;
case '?': case '?':
break; break;
@@ -62,7 +62,7 @@ bool ChangeNonCombatStrategyAction::Execute(Event event)
case '+': case '+':
case '-': case '-':
case '~': case '~':
sPlayerbotDbStore->Save(botAI); sPlayerbotRepository->Save(botAI);
break; break;
case '?': case '?':
break; break;

View File

@@ -241,20 +241,6 @@ bool MaxDpsChatShortcutAction::Execute(Event event)
return true; return true;
} }
bool NaxxChatShortcutAction::Execute(Event event)
{
Player* master = GetMaster();
if (!master)
return false;
botAI->Reset();
botAI->ChangeStrategy("+naxx", BOT_STATE_NON_COMBAT);
botAI->ChangeStrategy("+naxx", BOT_STATE_COMBAT);
botAI->TellMasterNoFacing("Add Naxx Strategies!");
// bot->Say("Add Naxx Strategies!", LANG_UNIVERSAL);
return true;
}
bool BwlChatShortcutAction::Execute(Event event) bool BwlChatShortcutAction::Execute(Event event)
{ {
Player* master = GetMaster(); Player* master = GetMaster();

View File

@@ -85,13 +85,6 @@ public:
bool Execute(Event event) override; bool Execute(Event event) override;
}; };
class NaxxChatShortcutAction : public Action
{
public:
NaxxChatShortcutAction(PlayerbotAI* ai) : Action(ai, "naxx chat shortcut") {}
virtual bool Execute(Event event);
};
class BwlChatShortcutAction : public Action class BwlChatShortcutAction : public Action
{ {
public: public:

View File

@@ -81,7 +81,7 @@ bool CheckMountStateAction::isUseful()
// to mostly be an issue in tunnels of WSG and AV) // to mostly be an issue in tunnels of WSG and AV)
float posZ = bot->GetPositionZ(); float posZ = bot->GetPositionZ();
float groundLevel = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), posZ); float groundLevel = bot->GetMapWaterOrGroundLevel(bot->GetPositionX(), bot->GetPositionY(), posZ);
if (!bot->IsMounted() && posZ < groundLevel) if (!bot->IsMounted() && !bot->HasWaterWalkAura() && posZ < groundLevel)
return false; return false;
// Not useful when bot does not have mount strat and is not currently mounted // Not useful when bot does not have mount strat and is not currently mounted
@@ -370,7 +370,7 @@ bool CheckMountStateAction::TryPreferredMount(Player* master) const
bool CheckMountStateAction::TryRandomMountFiltered(const std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const bool CheckMountStateAction::TryRandomMountFiltered(const std::map<int32, std::vector<uint32>>& spells, int32 masterSpeed) const
{ {
for (const auto& pair : spells) for (auto const& pair : spells)
{ {
int32 currentSpeed = pair.first; int32 currentSpeed = pair.first;
@@ -378,7 +378,7 @@ bool CheckMountStateAction::TryRandomMountFiltered(const std::map<int32, std::ve
continue; continue;
// Pick a random mount from the candidate group. // Pick a random mount from the candidate group.
const auto& ids = pair.second; auto const& ids = pair.second;
if (!ids.empty()) if (!ids.empty())
{ {
// Required here as otherwise bots won't mount in BG's due to them constant moving // Required here as otherwise bots won't mount in BG's due to them constant moving

View File

@@ -78,20 +78,17 @@ float ChooseRpgTargetAction::getMaxRelevance(GuidPosition guidP)
if (!trigger->IsActive()) if (!trigger->IsActive())
continue; continue;
NextAction** nextActions = triggerNode->getHandlers(); std::vector<NextAction> nextActions = triggerNode->getHandlers();
bool isRpg = false; bool isRpg = false;
for (int32 i = 0; i < NextAction::size(nextActions); i++) for (NextAction nextAction : nextActions)
{ {
NextAction* nextAction = nextActions[i]; Action* action = botAI->GetAiObjectContext()->GetAction(nextAction.getName());
Action* action = botAI->GetAiObjectContext()->GetAction(nextAction->getName());
if (dynamic_cast<RpgEnabled*>(action)) if (dynamic_cast<RpgEnabled*>(action))
isRpg = true; isRpg = true;
} }
NextAction::destroy(nextActions);
if (isRpg) if (isRpg)
{ {
@@ -311,7 +308,7 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldObject* target)
bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos) bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos)
{ {
PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot); PlayerbotAI* botAI = GET_PLAYERBOT_AI(bot);
Player* gmaster = botAI->GetGroupMaster(); Player* groupLeader = botAI->GetGroupLeader();
Player* realMaster = botAI->GetMaster(); Player* realMaster = botAI->GetMaster();
AiObjectContext* context = botAI->GetAiObjectContext(); AiObjectContext* context = botAI->GetAiObjectContext();
@@ -327,30 +324,30 @@ bool ChooseRpgTargetAction::isFollowValid(Player* bot, WorldPosition pos)
return false; return false;
} }
if (!gmaster || bot == gmaster) if (!groupLeader || bot == groupLeader)
return true; return true;
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
return true; return true;
if (bot->GetDistance(gmaster) > sPlayerbotAIConfig->rpgDistance * 2) if (bot->GetDistance(groupLeader) > sPlayerbotAIConfig->rpgDistance * 2)
return false; return false;
Formation* formation = AI_VALUE(Formation*, "formation"); Formation* formation = AI_VALUE(Formation*, "formation");
float distance = gmaster->GetDistance2d(pos.getX(), pos.getY()); float distance = groupLeader->GetDistance2d(pos.getX(), pos.getY());
if (!botAI->HasActivePlayerMaster() && distance < 50.0f) if (!botAI->HasActivePlayerMaster() && distance < 50.0f)
{ {
Player* player = gmaster; Player* player = groupLeader;
if (gmaster && !gmaster->isMoving() || if (groupLeader && !groupLeader->isMoving() ||
PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance) PAI_VALUE(WorldPosition, "last long move").distance(pos) < sPlayerbotAIConfig->reactDistance)
return true; return true;
} }
if ((inDungeon || !gmaster->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == gmaster && distance > 5.0f) if ((inDungeon || !groupLeader->HasPlayerFlag(PLAYER_FLAGS_RESTING)) && realMaster == groupLeader && distance > 5.0f)
return false; return false;
if (!gmaster->isMoving() && distance < 25.0f) if (!groupLeader->isMoving() && distance < 25.0f)
return true; return true;
if (distance < formation->GetMaxDistance()) if (distance < formation->GetMaxDistance())

View File

@@ -10,6 +10,7 @@
#include "LootObjectStack.h" #include "LootObjectStack.h"
#include "NewRpgStrategy.h" #include "NewRpgStrategy.h"
#include "Playerbots.h" #include "Playerbots.h"
#include "RtiTargetValue.h"
#include "PossibleRpgTargetsValue.h" #include "PossibleRpgTargetsValue.h"
#include "PvpTriggers.h" #include "PvpTriggers.h"
#include "ServerFacade.h" #include "ServerFacade.h"
@@ -87,10 +88,8 @@ bool DropTargetAction::Execute(Event event)
{ {
Spell const* spell = bot->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL); // Get the current spell being cast by the bot Spell const* spell = bot->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL); // Get the current spell being cast by the bot
if (spell && spell->m_spellInfo->Id == 75) //Check spell is not nullptr before accessing m_spellInfo if (spell && spell->m_spellInfo->Id == 75) //Check spell is not nullptr before accessing m_spellInfo
{
bot->InterruptSpell(CURRENT_AUTOREPEAT_SPELL); // Interrupt Auto Shot bot->InterruptSpell(CURRENT_AUTOREPEAT_SPELL); // Interrupt Auto Shot
} }
}
bot->AttackStop(); bot->AttackStop();
// if (Pet* pet = bot->GetPet()) // if (Pet* pet = bot->GetPet())
@@ -142,6 +141,23 @@ bool AttackRtiTargetAction::Execute(Event event)
{ {
Unit* rtiTarget = AI_VALUE(Unit*, "rti target"); Unit* rtiTarget = AI_VALUE(Unit*, "rti target");
// Fallback: if the "rti target" value did not resolve a valid unit yet,
// try to resolve the raid icon directly from the group.
if (!rtiTarget)
{
if (Group* group = bot->GetGroup())
{
std::string const rti = AI_VALUE(std::string, "rti");
int32 const index = RtiTargetValue::GetRtiIndex(rti);
if (index >= 0)
{
ObjectGuid const guid = group->GetTargetIcon(index);
if (!guid.IsEmpty())
rtiTarget = botAI->GetUnit(guid);
}
}
}
if (rtiTarget && rtiTarget->IsInWorld() && rtiTarget->GetMapId() == bot->GetMapId()) if (rtiTarget && rtiTarget->IsInWorld() && rtiTarget->GetMapId() == bot->GetMapId())
{ {
botAI->GetAiObjectContext()->GetValue<GuidVector>("prioritized targets")->Set({rtiTarget->GetGUID()}); botAI->GetAiObjectContext()->GetValue<GuidVector>("prioritized targets")->Set({rtiTarget->GetGUID()});
@@ -153,9 +169,7 @@ bool AttackRtiTargetAction::Execute(Event event)
} }
} }
else else
{
botAI->TellError("I dont see my rti attack target"); botAI->TellError("I dont see my rti attack target");
}
return false; return false;
} }

View File

@@ -120,7 +120,6 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge
} }
} }
//Continue current target. 90% chance //Continue current target. 90% chance
if (!foundTarget && urand(1, 100) > 10) if (!foundTarget && urand(1, 100) > 10)
{ {
@@ -181,7 +180,7 @@ void ChooseTravelTargetAction::getNewTarget(TravelTarget* newTarget, TravelTarge
void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget) void ChooseTravelTargetAction::setNewTarget(TravelTarget* newTarget, TravelTarget* oldTarget)
{ {
// Tell the master where we are going. // Tell the master where we are going.
if (!bot->GetGroup() || (botAI->GetGroupMaster() == bot)) if (!bot->GetGroup() || (botAI->GetGroupLeader() == bot))
ReportTravelTarget(newTarget, oldTarget); ReportTravelTarget(newTarget, oldTarget);
// If we are heading to a creature/npc clear it from the ignore list. // If we are heading to a creature/npc clear it from the ignore list.
@@ -673,7 +672,7 @@ bool ChooseTravelTargetAction::SetExploreTarget(TravelTarget* target)
//271 south shore //271 south shore
//35 booty bay //35 booty bay
//380 The Barrens The Crossroads //380 The Barrens The Crossroads
if(((ExploreTravelDestination * )activeTarget)->getAreaId() == 380) if (((ExploreTravelDestination * )activeTarget)->getAreaId() == 380)
{ {
activePoints.push_back(activeTarget->getPoints(true)[0]); activePoints.push_back(activeTarget->getPoints(true)[0]);
} }

View File

@@ -58,7 +58,6 @@ bool DropQuestAction::Execute(Event event)
return true; return true;
} }
bool CleanQuestLogAction::Execute(Event event) bool CleanQuestLogAction::Execute(Event event)
{ {
Player* requester = event.getOwner() ? event.getOwner() : GetMaster(); Player* requester = event.getOwner() ? event.getOwner() : GetMaster();
@@ -165,7 +164,6 @@ bool CleanQuestLogAction::Execute(Event event)
return true; return true;
} }
void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isGreen, bool hasProgress, bool isComplete) void CleanQuestLogAction::DropQuestType(uint8& numQuest, uint8 wantNum, bool isGreen, bool hasProgress, bool isComplete)
{ {
std::vector<uint8> slots; std::vector<uint8> slots;

View File

@@ -837,8 +837,8 @@ uint32 TalkAction::GetRandomEmote(Unit* unit, bool textEmote)
types.push_back(TEXT_EMOTE_TALKEX); types.push_back(TEXT_EMOTE_TALKEX);
types.push_back(TEXT_EMOTE_TALKQ); types.push_back(TEXT_EMOTE_TALKQ);
if (unit && (unit->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_TRAINER) || if (unit && (unit->HasNpcFlag(UNIT_NPC_FLAG_TRAINER) ||
unit->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_QUESTGIVER))) unit->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER)))
{ {
types.push_back(TEXT_EMOTE_SALUTE); types.push_back(TEXT_EMOTE_SALUTE);
} }
@@ -864,8 +864,8 @@ uint32 TalkAction::GetRandomEmote(Unit* unit, bool textEmote)
types.push_back(EMOTE_ONESHOT_EXCLAMATION); types.push_back(EMOTE_ONESHOT_EXCLAMATION);
types.push_back(EMOTE_ONESHOT_QUESTION); types.push_back(EMOTE_ONESHOT_QUESTION);
if (unit && (unit->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_TRAINER) || if (unit && (unit->HasNpcFlag(UNIT_NPC_FLAG_TRAINER) ||
unit->HasFlag(UNIT_NPC_FLAGS, UNIT_NPC_FLAG_QUESTGIVER))) unit->HasNpcFlag(UNIT_NPC_FLAG_QUESTGIVER)))
{ {
types.push_back(EMOTE_ONESHOT_SALUTE); types.push_back(EMOTE_ONESHOT_SALUTE);
} }

View File

@@ -271,19 +271,38 @@ void EquipAction::EquipItem(Item* item)
{ {
if (equippedItems[1]) if (equippedItems[1])
{ {
// Both slots are full - pick the worst item to replace // Both slots are full - pick the worst item to replace, but only if new item is better
StatsWeightCalculator calc(bot); StatsWeightCalculator calc(bot);
calc.SetItemSetBonus(false); calc.SetItemSetBonus(false);
calc.SetOverflowPenalty(false); calc.SetOverflowPenalty(false);
float firstItemScore = calc.CalculateItem(equippedItems[0]->GetTemplate()->ItemId); // Calculate new item score with random properties
float secondItemScore = calc.CalculateItem(equippedItems[1]->GetTemplate()->ItemId); int32 newItemRandomProp = item->GetItemRandomPropertyId();
float newItemScore = calc.CalculateItem(itemId, newItemRandomProp);
// If the second slot is worse, place the new item there // Calculate equipped items scores with random properties
if (firstItemScore > secondItemScore) int32 firstRandomProp = equippedItems[0]->GetItemRandomPropertyId();
int32 secondRandomProp = equippedItems[1]->GetItemRandomPropertyId();
float firstItemScore = calc.CalculateItem(equippedItems[0]->GetTemplate()->ItemId, firstRandomProp);
float secondItemScore = calc.CalculateItem(equippedItems[1]->GetTemplate()->ItemId, secondRandomProp);
// Determine which slot (if any) should be replaced
bool betterThanFirst = newItemScore > firstItemScore;
bool betterThanSecond = newItemScore > secondItemScore;
// Early return if new item is not better than either equipped item
if (!betterThanFirst && !betterThanSecond)
return;
if (betterThanFirst && betterThanSecond)
{ {
dstSlot++; // New item is better than both - replace the worse of the two equipped items
if (firstItemScore > secondItemScore)
dstSlot++; // Replace second slot (worse)
// else: keep dstSlot as-is (replace first slot)
} }
else if (betterThanSecond)
dstSlot++; // Only better than second slot - replace it
} }
else else
{ {
@@ -309,7 +328,6 @@ void EquipAction::EquipItem(Item* item)
botAI->TellMaster(out); botAI->TellMaster(out);
} }
bool EquipUpgradesAction::Execute(Event event) bool EquipUpgradesAction::Execute(Event event)
{ {
if (!sPlayerbotAIConfig->autoEquipUpgradeLoot && !sRandomPlayerbotMgr->IsRandomBot(bot)) if (!sPlayerbotAIConfig->autoEquipUpgradeLoot && !sRandomPlayerbotMgr->IsRandomBot(bot))
@@ -326,6 +344,27 @@ bool EquipUpgradesAction::Execute(Event event)
return false; return false;
} }
if (event.GetSource() == "item push result")
{
WorldPacket p(event.getPacket());
p.rpos(0);
ObjectGuid playerGuid;
uint32 received, created, sendChatMessage, itemSlot, itemId;
uint8 bagSlot;
p >> playerGuid;
p >> received;
p >> created;
p >> sendChatMessage;
p >> bagSlot;
p >> itemSlot;
p >> itemId;
ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId);
if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT)
return false;
}
CollectItemsVisitor visitor; CollectItemsVisitor visitor;
IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS);
@@ -338,9 +377,12 @@ bool EquipUpgradesAction::Execute(Event event)
int32 randomProperty = item->GetItemRandomPropertyId(); int32 randomProperty = item->GetItemRandomPropertyId();
uint32 itemId = item->GetTemplate()->ItemId; uint32 itemId = item->GetTemplate()->ItemId;
std::string itemUsageParam; std::string itemUsageParam;
if (randomProperty != 0) { if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
} else { }
else
{
itemUsageParam = std::to_string(itemId); itemUsageParam = std::to_string(itemId);
} }
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);
@@ -369,9 +411,12 @@ bool EquipUpgradeAction::Execute(Event event)
int32 randomProperty = item->GetItemRandomPropertyId(); int32 randomProperty = item->GetItemRandomPropertyId();
uint32 itemId = item->GetTemplate()->ItemId; uint32 itemId = item->GetTemplate()->ItemId;
std::string itemUsageParam; std::string itemUsageParam;
if (randomProperty != 0) { if (randomProperty != 0)
{
itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty); itemUsageParam = std::to_string(itemId) + "," + std::to_string(randomProperty);
} else { }
else
{
itemUsageParam = std::to_string(itemId); itemUsageParam = std::to_string(itemId);
} }
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam); ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", itemUsageParam);

View File

@@ -0,0 +1,510 @@
/*
* 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 "FishingAction.h"
#include "FishValues.h"
#include "Event.h"
#include "GridNotifiers.h"
#include "GridNotifiersImpl.h"
#include "ItemPackets.h"
#include "LastMovementValue.h"
#include "Map.h"
#include "MovementActions.h"
#include "Object.h"
#include "PlayerbotAI.h"
#include "PlayerbotTextMgr.h"
#include "Playerbots.h"
#include "Position.h"
uint32 const FISHING_SPELL = 7620;
uint32 const FISHING_POLE = 6256;
uint32 const FISHING_BOBBER = 35591;
float const MIN_DISTANCE_TO_WATER = 10.0f; // Minimum spell distance
float const MAX_DISTANCE_TO_WATER = 20.0f; // Maximum spell distance
float const HEIGHT_ABOVE_WATER_TOLERANCE = 1.0f; // Can stand in up to 1 unit of water and still fish.
float const SEARCH_INCREMENT = 2.5f;
float const HEIGHT_SEARCH_BUFFER = 10.0f; // Height buffer to prevent potentially missing the model the bot is standing on.
float const SEARCH_LAND_BUFFER = 0.5f;
uint32 const FISHING_LOCATION_TIMEOUT = 180000; //Three minutes
static bool IsFishingPole(Item* const item)
{
if (!item)
return false;
const ItemTemplate* proto = item->GetTemplate();
return proto && proto->Class == ITEM_CLASS_WEAPON &&
proto->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE;
}
float HasFishableWaterOrLand(float x, float y, float z, Map* map, uint32 phaseMask, bool checkForLand=false)
{
if (!map)
return INVALID_HEIGHT;
LiquidData const& liq = map->GetLiquidData(phaseMask, x, y, z+HEIGHT_ABOVE_WATER_TOLERANCE, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS);
float ground = map->GetHeight(phaseMask, x, y, z + HEIGHT_SEARCH_BUFFER, true);
if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER)
{
if (checkForLand)
return ground;
return INVALID_HEIGHT;
}
if (checkForLand)
{
if (ground > liq.Level - HEIGHT_ABOVE_WATER_TOLERANCE)
return ground;
return INVALID_HEIGHT;
}
if (liq.Level + HEIGHT_ABOVE_WATER_TOLERANCE > ground)
{
if (abs(liq.DepthLevel) < 0.5f) // too shallow to fish in.
return INVALID_HEIGHT;
return liq.Level;
}
return INVALID_HEIGHT;
}
bool HasLosToWater(Player* bot, float wx, float wy, float waterZ)
{
float z = bot->GetCollisionHeight() + bot->GetPositionZ();
return bot->GetMap()->isInLineOfSight(
bot->GetPositionX(), bot->GetPositionY(), z,
wx, wy, waterZ,
bot->GetPhaseMask(),
LINEOFSIGHT_ALL_CHECKS,
VMAP::ModelIgnoreFlags::Nothing);
}
WorldPosition FindLandFromPosition(PlayerbotAI* botAI, float startDistance, float endDistance, float increment, float orientation, WorldPosition targetPos, float fishingSearchWindow, bool checkLOS = true)
{
Player* bot = botAI->GetBot();
Map* map = bot->GetMap();
uint32 phaseMask = bot->GetPhaseMask();
Player* master = botAI->GetMaster();
float targetX = targetPos.GetPositionX();
float targetY = targetPos.GetPositionY();
float targetZ = targetPos.GetPositionZ();
for (float dist = startDistance; dist <= endDistance; dist += increment)
{
//step backwards from position to bot to find edge of shore.
float checkX = targetX - dist * cos(orientation);
float checkY = targetY - dist * sin(orientation);
float groundZ = map->GetHeight(phaseMask, checkX, checkY, targetZ + HEIGHT_SEARCH_BUFFER, true);
if (groundZ == INVALID_HEIGHT)
continue;
LiquidData const& liq = map->GetLiquidData(phaseMask, checkX, checkY, targetZ, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS);
if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER || groundZ > liq.DepthLevel + HEIGHT_ABOVE_WATER_TOLERANCE)
{
if (checkLOS)
{
bool hasLOS = map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing);
if (!hasLOS)
continue;
}
// Add a distance check for the position to prevent the bot from moving out of range to the master.
if (master && botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER)
continue;
return WorldPosition(bot->GetMapId(), checkX, checkY, groundZ);
}
}
return WorldPosition();
}
WorldPosition FindLandRadialFromPosition (PlayerbotAI* botAI, WorldPosition targetPos, float startDistance, float endDistance, float increment, float fishingSearchWindow, int angles = 16)
{
Player* bot = botAI->GetBot();
const int numDirections = angles;
std::vector<WorldPosition> boundaryPoints;
Player* master = botAI->GetMaster();
if (!master)
return WorldPosition();
Map* map = bot->GetMap();
uint32 phaseMask = bot->GetPhaseMask();
float targetX = targetPos.GetPositionX();
float targetY = targetPos.GetPositionY();
float targetZ = targetPos.GetPositionZ();
for (float dist = startDistance; dist <= endDistance; dist += increment)
{
for (int i = 0; i < numDirections; ++i)
{
float angle = (2.0f * M_PI * i) / numDirections;
float checkX = targetX - cos(angle) * dist;
float checkY = targetY - sin(angle) * dist;
float groundZ = HasFishableWaterOrLand(checkX, checkY, targetZ, map, phaseMask, true);
if (groundZ == INVALID_HEIGHT)
continue;
if (map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER)
continue;
boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, groundZ));
}
if (!boundaryPoints.empty())
break;
}
if (boundaryPoints.empty())
return WorldPosition();
if (boundaryPoints.size() == 1)
return boundaryPoints[0];
float minDistance = FLT_MAX;
WorldLocation closestPoint = WorldPosition();
for (auto const& pos : boundaryPoints)
{
float distance = bot->GetExactDist2d(&pos);
if (distance < minDistance)
{
minDistance = distance;
closestPoint = pos;
}
}
return closestPoint;
}
WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS, int numDirections)
{
std::vector<WorldPosition> boundaryPoints;
float dist = minDistance;
while (dist <= maxDistance)
{
for (int i = 0; i < numDirections; ++i)
{
float angle = (2.0f * M_PI * i) / numDirections;
float checkX = x + cos(angle) * dist;
float checkY = y + sin(angle) * dist;
float waterZ = HasFishableWaterOrLand(checkX, checkY, z, map, phaseMask);
if (waterZ == INVALID_HEIGHT)
continue;
if (checkLOS && !HasLosToWater(bot, checkX, checkY, waterZ))
continue;
boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, waterZ));
}
if (!boundaryPoints.empty())
break;
dist += increment;
}
if (boundaryPoints.empty())
return WorldPosition();
if (boundaryPoints.size() == 1)
return boundaryPoints[0];
// return the central point in the identified positions in to try to be perpendicular to the shore.
return boundaryPoints[boundaryPoints.size() / 2];
}
WorldPosition FindFishingHole(PlayerbotAI* botAI)
{
Player* player = botAI->GetBot();
GuidVector gos = PAI_VALUE(GuidVector, "nearest game objects no los");
GameObject* nearestFishingHole = nullptr;
float minDist = std::numeric_limits<float>::max();
for (auto const& guid : gos)
{
GameObject* go = botAI->GetGameObject(guid);
if (!go)
continue;
if (go->GetGoType() == GAMEOBJECT_TYPE_FISHINGHOLE)
{
float dist = player->GetDistance2d(go);
if (dist < minDist)
{
minDist = dist;
nearestFishingHole = go;
}
}
}
if (nearestFishingHole)
return WorldPosition(nearestFishingHole->GetMapId(), nearestFishingHole->GetPositionX(), nearestFishingHole->GetPositionY(), nearestFishingHole->GetPositionZ());
return WorldPosition();
}
bool MoveNearWaterAction::Execute(Event event)
{
WorldPosition landSpot = AI_VALUE(WorldPosition, "fishing spot");
if (landSpot.IsValid())
return MoveTo(landSpot.GetMapId(), landSpot.GetPositionX(), landSpot.GetPositionY(), landSpot.GetPositionZ());
return false;
}
bool MoveNearWaterAction::isUseful()
{
if (!AI_VALUE(bool, "can fish"))
return false;
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
return !pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) ||
bot->GetExactDist(&pos) < 0.1f;
}
bool MoveNearWaterAction::isPossible()
{
Player* master = botAI->GetMaster();
float fishingSearchWindow;
if (master)
fishingSearchWindow = sPlayerbotAIConfig->fishingDistanceFromMaster;
else
fishingSearchWindow = sPlayerbotAIConfig->fishingDistance;
WorldPosition fishingHole = FindFishingHole(botAI);
if (fishingHole.IsValid())
{
float distance = bot->GetExactDist2d(&fishingHole);
bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ());
// Water spot is in range, and we have LOS to it. Set bot position to fishing spot and do not move
if (distance >= MIN_DISTANCE_TO_WATER &&
distance <= MAX_DISTANCE_TO_WATER && hasLOS)
{
SET_AI_VALUE(WorldPosition, "fishing spot", WorldPosition(WorldPosition(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ())));
return false;
}
// Water spot is out of range, lets look for a spot to move to for the fishing hole.
if (distance > MAX_DISTANCE_TO_WATER || distance < MIN_DISTANCE_TO_WATER)
{
float angle = bot->GetAngle(fishingHole.GetPositionX(), fishingHole.GetPositionY());
WorldPosition landSpot = FindLandRadialFromPosition(botAI, fishingHole, MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, fishingSearchWindow, 32);
if (landSpot.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot", landSpot);
return true;
}
}
}
// Can the bot fish from current position?
WorldPosition waterAtCurrentPos =
FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMap(),
bot->GetPhaseMask(), MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, true);
if (waterAtCurrentPos.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot",
WorldPosition(WorldPosition(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ())));
return false;
}
// Lets find some water where we can fish.
WorldPosition water = FindWaterRadial(
bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
bot->GetMap(), bot->GetPhaseMask(),
MIN_DISTANCE_TO_WATER,
fishingSearchWindow + MAX_DISTANCE_TO_WATER,
SEARCH_INCREMENT, false);
if (!water.IsValid())
return false;
bool hasLOS = bot->IsWithinLOS(water.GetPositionX(), water.GetPositionY(), water.GetPositionZ());
float angle = bot->GetAngle(water.GetPositionX(), water.GetPositionY());
WorldPosition landSpot =
FindLandFromPosition(botAI, 0.0f, MAX_DISTANCE_TO_WATER, 1.0f, angle, water, fishingSearchWindow, false);
if (landSpot.IsValid())
{
SET_AI_VALUE(WorldPosition, "fishing spot", landSpot);
return true;
}
return false;
}
bool EquipFishingPoleAction::Execute(Event event)
{
if (!_pole)
return false;
WorldPacket eqPacket(CMSG_AUTOEQUIP_ITEM_SLOT, 2);
eqPacket << _pole->GetGUID() << uint8(EQUIPMENT_SLOT_MAINHAND);
WorldPackets::Item::AutoEquipItemSlot nicePacket(std::move(eqPacket));
nicePacket.Read();
bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket);
return true;
}
bool EquipFishingPoleAction::isUseful()
{
Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND);
if (IsFishingPole(mainHand))
return false;
for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot)
{
if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot))
{
if (IsFishingPole(item))
{
_pole = item;
return true;
}
}
}
for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag)
{
if (Bag* pBag = bot->GetBagByPos(bag))
{
for (uint32 j = 0; j < pBag->GetBagSize(); ++j)
{
if (Item* item = pBag->GetItemByPos(j))
{
if (IsFishingPole(item))
{
_pole = item;
return true;
}
}
}
}
}
if (sRandomPlayerbotMgr->IsRandomBot(bot))
{
bot->StoreNewItemInBestSlots(FISHING_POLE, 1); // Try to get a fishing pole
return true;
}
Player* master = botAI->GetMaster();
if (!master)
return false;
std::string masterName = master->GetName();
std::string text = sPlayerbotTextMgr->GetBotTextOrDefault(
"no_fishing_pole_error", "I don't have a Fishing Pole",{});
botAI->Whisper(text, masterName);
return false;
}
bool FishingAction::Execute(Event event)
{
WorldPosition target = WorldPosition();
WorldPosition fishingHole = FindFishingHole(botAI);
if (fishingHole.IsValid())
{
Position pos = fishingHole;
float distance = bot->GetExactDist2d(&pos);
bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ());
if (distance < MAX_DISTANCE_TO_WATER &&
distance > MIN_DISTANCE_TO_WATER && hasLOS)
target = fishingHole;
}
if (!target.IsValid())
{
target = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(),
bot->GetPositionZ(), bot->GetMap(), bot->GetPhaseMask(),
MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, true, 32);
if (!target.IsValid())
return false;
}
Position pos = target;
if (!bot->HasInArc(1.0, &pos, 1.0))
{
float angle = bot->GetAngle(pos.GetPositionX(), pos.GetPositionY());
bot->SetOrientation(angle);
if (!bot->IsRooted())
bot->SendMovementFlagUpdate();
}
EquipFishingPoleAction equipAction(botAI);
if (equipAction.isUseful())
return equipAction.Execute(event);
botAI->CastSpell(FISHING_SPELL, bot);
botAI->ChangeStrategy("+use bobber", BOT_STATE_NON_COMBAT);
return true;
}
bool FishingAction::isUseful()
{
if (!AI_VALUE(bool, "can fish"))
return false;
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
if (!pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT))
return false;
return bot->GetExactDist(&pos) < 0.1f;
}
bool UseBobberAction::isUseful()
{
return AI_VALUE(bool, "can use fishing bobber");
}
bool UseBobberAction::Execute(Event event)
{
GuidVector gos = AI_VALUE(GuidVector, "nearest game objects no los");
for (auto const& guid : gos)
{
if (GameObject* go = botAI->GetGameObject(guid))
{
if (go->GetEntry() != FISHING_BOBBER)
continue;
if (go->GetOwnerGUID() != bot->GetGUID())
continue;
if (go->getLootState() == GO_READY)
{
go->Use(bot);
botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT);
return true;
}
}
}
return false;
}
bool EndMasterFishingAction::Execute(Event event)
{
botAI->ChangeStrategy("-master fishing", BOT_STATE_NON_COMBAT);
return true;
}
bool EndMasterFishingAction::isUseful()
{
FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue<WorldPosition>("fishing spot");
WorldPosition pos = fishingSpotValueObject->Get();
if (pos.IsValid() && !fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) && pos == bot->GetPosition())
return false;
WorldPosition nearWater = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
bot->GetMap(), bot->GetPhaseMask(), MIN_DISTANCE_TO_WATER, sPlayerbotAIConfig->endFishingWithMaster, 10.0f);
return !nearWater.IsValid();
}
bool RemoveBobberStrategyAction::Execute(Event event)
{
botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT);
return true;
}

View File

@@ -0,0 +1,71 @@
/*
* 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.
*/
#ifndef _PLAYERBOT_FISHINGACTION_H
#define _PLAYERBOT_FISHINGACTION_H
#include "Action.h"
#include "MovementActions.h"
#include "Event.h"
#include "Playerbots.h"
extern const uint32 FISHING_SPELL;
extern const uint32 FISHING_POLE;
extern const uint32 FISHING_BOBBER;
WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS=false, int numDirections = 16);
class PlayerbotAI;
class FishingAction : public Action
{
public:
FishingAction(PlayerbotAI* botAI) : Action(botAI, "go fishing"){}
bool Execute(Event event) override;
bool isUseful() override;
};
class EquipFishingPoleAction : public Action
{
public:
EquipFishingPoleAction(PlayerbotAI* botAI) : Action(botAI, "equip fishing pole") {}
bool Execute(Event event) override;
bool isUseful() override;
private:
Item* _pole = nullptr;
};
class MoveNearWaterAction : public MovementAction
{
public:
MoveNearWaterAction(PlayerbotAI* botAI): MovementAction(botAI, "move near water") {}
bool Execute(Event event) override;
bool isUseful() override;
bool isPossible() override;
};
class UseBobberAction : public Action
{
public:
UseBobberAction(PlayerbotAI* botAI) : Action(botAI, "use fishing bobber") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class EndMasterFishingAction : public Action
{
public:
EndMasterFishingAction(PlayerbotAI* botAI) : Action(botAI, "end master fishing") {}
bool Execute(Event event) override;
bool isUseful() override;
};
class RemoveBobberStrategyAction : public Action
{
public:
RemoveBobberStrategyAction(PlayerbotAI* botAI) : Action(botAI, "remove bobber strategy") {}
bool Execute(Event event) override;
};
#endif

View File

@@ -70,7 +70,7 @@ bool FollowAction::isUseful()
if (!target.empty()) if (!target.empty())
fTarget = AI_VALUE(Unit*, target); fTarget = AI_VALUE(Unit*, target);
else else
fTarget = AI_VALUE(Unit*, "master target"); fTarget = AI_VALUE(Unit*, "group leader");
if (fTarget) if (fTarget)
{ {
@@ -97,6 +97,8 @@ bool FollowAction::isUseful()
distance = bot->GetDistance(loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()); distance = bot->GetDistance(loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ());
} }
if (botAI->HasStrategy("master fishing", BOT_STATE_NON_COMBAT))
return sServerFacade->IsDistanceGreaterThan(distance, sPlayerbotAIConfig->fishingDistanceFromMaster);
return sServerFacade->IsDistanceGreaterThan(distance, formation->GetMaxDistance()); return sServerFacade->IsDistanceGreaterThan(distance, formation->GetMaxDistance());
} }
@@ -114,9 +116,9 @@ bool FollowAction::CanDeadFollow(Unit* target)
return true; return true;
} }
bool FleeToMasterAction::Execute(Event event) bool FleeToGroupLeaderAction::Execute(Event event)
{ {
Unit* fTarget = AI_VALUE(Unit*, "master target"); Unit* fTarget = AI_VALUE(Unit*, "group leader");
bool canFollow = Follow(fTarget); bool canFollow = Follow(fTarget);
if (!canFollow) if (!canFollow)
{ {
@@ -146,22 +148,22 @@ bool FleeToMasterAction::Execute(Event event)
return true; return true;
} }
bool FleeToMasterAction::isUseful() bool FleeToGroupLeaderAction::isUseful()
{ {
if (!botAI->GetGroupMaster()) if (!botAI->GetGroupLeader())
return false; return false;
if (botAI->GetGroupMaster() == bot) if (botAI->GetGroupLeader() == bot)
return false; return false;
Unit* target = AI_VALUE(Unit*, "current target"); Unit* target = AI_VALUE(Unit*, "current target");
if (target && botAI->GetGroupMaster()->GetTarget() == target->GetGUID()) if (target && botAI->GetGroupLeader()->GetTarget() == target->GetGUID())
return false; return false;
if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT)) if (!botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT))
return false; return false;
Unit* fTarget = AI_VALUE(Unit*, "master target"); Unit* fTarget = AI_VALUE(Unit*, "group leader");
if (!CanDeadFollow(fTarget)) if (!CanDeadFollow(fTarget))
return false; return false;

View File

@@ -20,10 +20,10 @@ public:
bool CanDeadFollow(Unit* target); bool CanDeadFollow(Unit* target);
}; };
class FleeToMasterAction : public FollowAction class FleeToGroupLeaderAction : public FollowAction
{ {
public: public:
FleeToMasterAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to master") {} FleeToGroupLeaderAction(PlayerbotAI* botAI) : FollowAction(botAI, "flee to group leader") {}
bool Execute(Event event) override; bool Execute(Event event) override;
bool isUseful() override; bool isUseful() override;

View File

@@ -29,7 +29,6 @@ namespace ai::buff
// Returns false if the spellId is invalid. // Returns false if the spellId is invalid.
bool HasRequiredReagents(Player* bot, uint32 spellId); bool HasRequiredReagents(Player* bot, uint32 spellId);
// Applies the "switch to group buff" policy if: the bot is in a group of size x+, // Applies the "switch to group buff" policy if: the bot is in a group of size x+,
// the group variant is known/useful, and reagents are available. Otherwise, returns baseName. // the group variant is known/useful, and reagents are available. Otherwise, returns baseName.
// If announceOnMissing == true and reagents are missing, calls the 'announce' callback // If announceOnMissing == true and reagents are missing, calls the 'announce' callback

View File

@@ -190,8 +190,8 @@ CastEnchantItemAction::CastEnchantItemAction(PlayerbotAI* botAI, std::string con
bool CastEnchantItemAction::isPossible() bool CastEnchantItemAction::isPossible()
{ {
// if (!CastSpellAction::isPossible()) { // if (!CastSpellAction::isPossible())
// {
// botAI->TellMasterNoFacing("Impossible: " + spell); // botAI->TellMasterNoFacing("Impossible: " + spell);
// return false; // return false;
// } // }
@@ -265,11 +265,6 @@ CastShootAction::CastShootAction(PlayerbotAI* botAI) : CastSpellAction(botAI, "s
} }
} }
NextAction** CastSpellAction::getPrerequisites()
{
return nullptr;
}
Value<Unit*>* CastDebuffSpellOnAttackerAction::GetTargetValue() Value<Unit*>* CastDebuffSpellOnAttackerAction::GetTargetValue()
{ {
return context->GetValue<Unit*>("attacker without aura", spell); return context->GetValue<Unit*>("attacker without aura", spell);
@@ -364,7 +359,8 @@ bool UseTrinketAction::UseTrinket(Item* item)
for (int i = 0; i < MAX_SPELL_EFFECTS; i++) for (int i = 0; i < MAX_SPELL_EFFECTS; i++)
{ {
const SpellEffectInfo& effectInfo = spellInfo->Effects[i]; const SpellEffectInfo& effectInfo = spellInfo->Effects[i];
if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA) { if (effectInfo.Effect == SPELL_EFFECT_APPLY_AURA)
{
applyAura = true; applyAura = true;
break; break;
} }

View File

@@ -27,7 +27,11 @@ public:
bool isUseful() override; bool isUseful() override;
ActionThreatType getThreatType() override { return ActionThreatType::Single; } ActionThreatType getThreatType() override { return ActionThreatType::Single; }
NextAction** getPrerequisites() override; std::vector<NextAction> getPrerequisites() override
{
return {};
}
std::string const getSpell() { return spell; } std::string const getSpell() { return spell; }
protected: protected:
@@ -193,10 +197,12 @@ public:
ResurrectPartyMemberAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {} ResurrectPartyMemberAction(PlayerbotAI* botAI, std::string const spell) : CastSpellAction(botAI, spell) {}
std::string const GetTargetName() override { return "party member to resurrect"; } std::string const GetTargetName() override { return "party member to resurrect"; }
NextAction** getPrerequisites() override std::vector<NextAction> getPrerequisites() override
{ {
return NextAction::merge(NextAction::array(0, new NextAction("reach party member to resurrect"), NULL), return NextAction::merge(
Action::getPrerequisites()); { NextAction("reach party member to resurrect") },
Action::getPrerequisites()
);
} }
}; };

Some files were not shown because too many files have changed in this diff Show More