From 25800f54e8c0b40eee1933636d6aaf1d904da57d Mon Sep 17 00:00:00 2001 From: NoxMax <50133316+NoxMax@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:31:55 -0700 Subject: [PATCH] Fix/Feat: PVP with master and PVP probablity system (thread-safe remake) (#2008) This is a remake of #1914 that had to be reverted. Original PR had a thread-safe issue where a crash happens if multiple threads access the cache at the same time. Unfortunately this problem was not caught in earlier testing. I don't know if because I was testing on a month old branch, if my settings had only ~2000, or if I needed test runs longer than an hour to find out. Regardless, this has all been addressed. Test have been run on the latest commits from today (2026/1/11), with all 7500 of my bots active, with a test run that lasted 15 hours. All stable and bots are following the probability system without issue. ~~The new edit uses mutex locking, preventing simultaneous access of the cache by multiple threads.~~ The new edit uses deterministic hashing, thereby not having issues with cache thread safety to begin with. Thank you @hermensbas for catching and reverting the original problem PR. Apologies for not catching the issue myself. --- Original PR description: 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 :grimacing: Still, they can be easily exposed in the future, and if someone is reading this then, remember to change constexpr to const. --------- Co-authored-by: bashermens <31279994+hermensbas@users.noreply.github.com> --- src/Ai/Base/Value/EnemyPlayerValue.cpp | 11 ++ src/Ai/Base/Value/PossibleTargetsValue.cpp | 117 ++++++++++++++++++++- 2 files changed, 123 insertions(+), 5 deletions(-) diff --git a/src/Ai/Base/Value/EnemyPlayerValue.cpp b/src/Ai/Base/Value/EnemyPlayerValue.cpp index 7de0cd670..c2f6e056a 100644 --- a/src/Ai/Base/Value/EnemyPlayerValue.cpp +++ b/src/Ai/Base/Value/EnemyPlayerValue.cpp @@ -11,6 +11,10 @@ bool NearestEnemyPlayersValue::AcceptUnit(Unit* unit) { + // Apply parent's filtering first (includes level difference checks) + if (!PossibleTargetsValue::AcceptUnit(unit)) + return false; + bool inCannon = botAI->IsInVehicle(false, true); Player* enemy = dynamic_cast(unit); if (enemy && botAI->IsOpposing(enemy) && enemy->IsPvP() && @@ -19,7 +23,14 @@ bool NearestEnemyPlayersValue::AcceptUnit(Unit* unit) ((inCannon || !enemy->HasFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_NOT_SELECTABLE))) && /*!enemy->HasStealthAura() && !enemy->HasInvisibilityAura()*/ enemy->CanSeeOrDetect(bot) && !(enemy->HasSpiritOfRedemptionAura())) + { + // If with master, only attack if master is PvP flagged + Player* master = botAI->GetMaster(); + if (master && !master->IsPvP() && !master->IsFFAPvP()) + return false; + return true; + } return false; } diff --git a/src/Ai/Base/Value/PossibleTargetsValue.cpp b/src/Ai/Base/Value/PossibleTargetsValue.cpp index 654abd44c..7db1dd39b 100644 --- a/src/Ai/Base/Value/PossibleTargetsValue.cpp +++ b/src/Ai/Base/Value/PossibleTargetsValue.cpp @@ -16,6 +16,20 @@ #include "SpellAuraEffects.h" #include "SpellMgr.h" #include "Unit.h" +#include "AreaDefines.h" + +// Level difference thresholds for attack probability +constexpr int32 EXTREME_LEVEL_DIFF = 5; // Don't attack if enemy is this much higher +constexpr int32 HIGH_LEVEL_DIFF = 4; // 25% chance at +/- this difference +constexpr int32 MID_LEVEL_DIFF = 3; // 50% chance at +/- this difference +constexpr int32 LOW_LEVEL_DIFF = 2; // 75% chance at +/- this difference + +// Time window for deterministic attack decisions +constexpr uint32 ATTACK_DECISION_TIME_WINDOW = 2 * MINUTE; + +// 64 bit FNV-1a hash constants +constexpr uint64_t FNV_OFFSET_BASIS = 14695981039346656037ULL; +constexpr uint64_t FNV_PRIME = 1099511628211ULL; void PossibleTargetsValue::FindUnits(std::list& targets) { @@ -24,7 +38,103 @@ void PossibleTargetsValue::FindUnits(std::list& targets) Cell::VisitObjects(bot, searcher, range); } -bool PossibleTargetsValue::AcceptUnit(Unit* unit) { return AttackersValue::IsPossibleTarget(unit, bot, range); } +bool PossibleTargetsValue::AcceptUnit(Unit* unit) +{ + if (!AttackersValue::IsPossibleTarget(unit, bot, range)) + return false; + + // Level-based PvP restrictions + if (unit->IsPlayer()) + { + // Self-defense - always allow fighting back + if (bot->IsInCombat() && bot->GetVictim() == unit) + return true; // Already fighting + + Unit* botAttacker = bot->getAttackerForHelper(); + if (botAttacker) + { + if (botAttacker == unit) + return true; // Enemy attacking + + if (botAttacker->IsPet()) + { + Unit* petOwner = botAttacker->GetOwner(); + if (petOwner && petOwner == unit) + return true; // Enemy's pet attacking + } + } + + // Skip restrictions in BG/Arena + if (bot->InBattleground() || bot->InArena()) + return true; + + // Skip restrictions if in duel with this player + if (bot->duel && bot->duel->Opponent == unit) + return true; + + // Capital cities - no restrictions + uint32 zoneId = bot->GetZoneId(); + bool inCapitalCity = (zoneId == AREA_STORMWIND_CITY || + zoneId == AREA_IRONFORGE || + zoneId == AREA_DARNASSUS || + zoneId == AREA_THE_EXODAR || + zoneId == AREA_ORGRIMMAR || + zoneId == AREA_THUNDER_BLUFF || + zoneId == AREA_UNDERCITY || + zoneId == AREA_SILVERMOON_CITY); + + if (inCapitalCity) + return true; + + // Level difference check + int32 levelDifference = unit->GetLevel() - bot->GetLevel(); + int32 absLevelDifference = std::abs(levelDifference); + + // Extreme difference - do not attack + if (levelDifference >= EXTREME_LEVEL_DIFF) + return false; + + // Calculate attack chance based on level difference + uint32 attackChance = 100; // Default 100%: Bot and target's levels are very close + + // There's a chance a bot might gank on an extremly low target + if ((absLevelDifference < EXTREME_LEVEL_DIFF && absLevelDifference >= HIGH_LEVEL_DIFF) || + levelDifference <= -EXTREME_LEVEL_DIFF) + attackChance = 25; + + else if (absLevelDifference < HIGH_LEVEL_DIFF && absLevelDifference >= MID_LEVEL_DIFF) + attackChance = 50; + + else if (absLevelDifference < MID_LEVEL_DIFF && absLevelDifference >= LOW_LEVEL_DIFF) + attackChance = 75; + + // If probability check needed, use deterministic hash-based decision + if (attackChance < 100) + { + // Decisions remain stable for ATTACK_DECISION_TIME_WINDOW. + time_t timeWindow = time(nullptr) / ATTACK_DECISION_TIME_WINDOW; + + // FNV-1a hash used to deterministically convert botGUID, targetGUID, and timeWindow + // into a consistent percentage chance without needing to cache previous decisions. + // See: http://www.isthe.com/chongo/tech/comp/fnv/ + uint64_t hash = FNV_OFFSET_BASIS; + + // Diffuse bot GUID, target GUID, and time window into the hash + hash ^= bot->GetGUID().GetRawValue(); + hash *= FNV_PRIME; + hash ^= unit->GetGUID().GetRawValue(); + hash *= FNV_PRIME; + hash ^= static_cast(timeWindow); + hash *= FNV_PRIME; + + // Convert hash to 0-99 range and compare against attack chance percentage. + // Ex: attackChance=75: hash 0-74 = attack (75%), hash 75-99 = don't attack (25%) + return (hash % 100) < attackChance; + } + } + + return true; +} void PossibleTriggersValue::FindUnits(std::list& targets) { @@ -36,9 +146,8 @@ void PossibleTriggersValue::FindUnits(std::list& targets) bool PossibleTriggersValue::AcceptUnit(Unit* unit) { if (!unit->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE)) - { return false; - } + Unit::AuraEffectList const& aurasPeriodicTriggerSpell = unit->GetAuraEffectsByType(SPELL_AURA_PERIODIC_TRIGGER_SPELL); Unit::AuraEffectList const& aurasPeriodicTriggerWithValueSpell = @@ -58,9 +167,7 @@ bool PossibleTriggersValue::AcceptUnit(Unit* unit) for (int j = 0; j < MAX_SPELL_EFFECTS; j++) { if (triggerSpellInfo->Effects[j].Effect == SPELL_EFFECT_SCHOOL_DAMAGE) - { return true; - } } } }