diff options
author | Treeston <treeston.mmoc@gmail.com> | 2018-01-03 20:04:19 +0100 |
---|---|---|
committer | Shauren <shauren.trinity@gmail.com> | 2021-05-16 21:56:01 +0200 |
commit | 34c7810fe507eca1b8b9389630db5d5d26d92e77 (patch) | |
tree | e05653a0adaf4c4f5c406649f1bfe6573cbb22ff /src/server/game/Combat/ThreatManager.cpp | |
parent | 5158136ee8a77046e37bafa192481b8b61d4a116 (diff) |
Core: Combat/threat system rewrite (PR #19930)
- PvE combat is now always mutual. UNIT_FLAG_IN_COMBAT is backed by actual references to the units we're in combat with.
- PvP combat is now also tracked, and almost always mutual; spells like Vanish and Feign Death can break this rule. That means we can easily determine a list of players we're fighting.
- By extension, IsInCombatWith now has sensible behavior when invoked on nonplayers.
- Threat and combat systems are no longer the same.
- They still have an enforced relationship (threat implies combat - clearing combat clears threat)...
- ...but we can have combat without threat. A creature (with threat list) isn't considered to be engaged until it has an entry on its threat list...
- ...which means we can now faithfully replicate retail engage behavior. Combat on projectile launch - engagement start on projectile impact. Yay for progress!
- AI method refactor, as already ported in 6113b9d - `JustEngagedWith`, `JustEnteredCombat` and `JustExitedCombat`.
- Vehicle threat is now properly pooled on the main vehicle body (fixes #16542).
- Various edge case bug fixes for threat redirects (Misdirection "cancelling" Vigilance and similar).
- Target re-selection is now significantly faster.
- Fixed a ton of other smaller edge case bugs, probably.
Closes #7951 and #19998.
(cherry picked from commit 532ab1c7f8653d1a2e48aa1f1f8a9ba1041d4bb7)
Diffstat (limited to 'src/server/game/Combat/ThreatManager.cpp')
-rw-r--r-- | src/server/game/Combat/ThreatManager.cpp | 1011 |
1 files changed, 558 insertions, 453 deletions
diff --git a/src/server/game/Combat/ThreatManager.cpp b/src/server/game/Combat/ThreatManager.cpp index e3d5f054abb..16511b14a3b 100644 --- a/src/server/game/Combat/ThreatManager.cpp +++ b/src/server/game/Combat/ThreatManager.cpp @@ -16,623 +16,728 @@ */ #include "ThreatManager.h" -#include "Unit.h" #include "Creature.h" -#include "Map.h" -#include "Player.h" +#include "CombatPackets.h" +#include "CreatureAI.h" +#include "MotionMaster.h" #include "ObjectAccessor.h" -#include "UnitEvents.h" -#include "SpellAuras.h" +#include "Player.h" +#include "SpellAuraEffects.h" #include "SpellMgr.h" -#include "TemporarySummon.h" - -//============================================================== -//================= ThreatCalcHelper =========================== -//============================================================== - -// The hatingUnit is not used yet -float ThreatCalcHelper::calcThreat(Unit* hatedUnit, Unit* /*hatingUnit*/, float threat, SpellSchoolMask schoolMask, SpellInfo const* threatSpell /*= nullptr*/) -{ - if (threatSpell) - { - if (SpellThreatEntry const* threatEntry = sSpellMgr->GetSpellThreatEntry(threatSpell->Id)) - if (threatEntry->pctMod != 1.0f) - threat *= threatEntry->pctMod; - // Energize is not affected by Mods - for (SpellEffectInfo const* effect : threatSpell->GetEffects()) - if (effect && (effect->Effect == SPELL_EFFECT_ENERGIZE || effect->ApplyAuraName == SPELL_AURA_PERIODIC_ENERGIZE)) - return threat; +#include "Hacks/boost_1_74_fibonacci_heap.h" +BOOST_1_74_FIBONACCI_HEAP_MSVC_COMPILE_FIX(ThreatManager::threat_list_heap::value_type) - if (Player* modOwner = hatedUnit->GetSpellModOwner()) - modOwner->ApplySpellMod(threatSpell, SpellModOp::Hate, threat); - } +const CompareThreatLessThan ThreatManager::CompareThreat; - return hatedUnit->ApplyTotalThreatModifier(threat, schoolMask); +void ThreatReference::AddThreat(float amount) +{ + if (amount == 0.0f) + return; + _baseAmount = std::max<float>(_baseAmount + amount, 0.0f); + if (amount > 0.0f) + HeapNotifyIncreased(); + else + HeapNotifyDecreased(); } -bool ThreatCalcHelper::isValidProcess(Unit* hatedUnit, Unit* hatingUnit, SpellInfo const* threatSpell /*= nullptr*/) +void ThreatReference::ScaleThreat(float factor) { - //function deals with adding threat and adding players and pets into ThreatList - //mobs, NPCs, guards have ThreatList and HateOfflineList - //players and pets have only InHateListOf - //HateOfflineList is used co contain unattackable victims (in-flight, in-water, GM etc.) - - if (!hatedUnit || !hatingUnit) - return false; - - // not to self - if (hatedUnit == hatingUnit) - return false; - - // not to GM - if (hatedUnit->GetTypeId() == TYPEID_PLAYER && hatedUnit->ToPlayer()->IsGameMaster()) - return false; - - // not to dead and not for dead - if (!hatedUnit->IsAlive() || !hatingUnit->IsAlive()) - return false; - - // not in same map or phase - if (!hatedUnit->IsInMap(hatingUnit) || !hatedUnit->IsInPhase(hatingUnit)) - return false; - - // spell not causing threat - if (threatSpell && threatSpell->HasAttribute(SPELL_ATTR1_NO_THREAT)) - return false; + if (factor == 1.0f) + return; + _baseAmount *= factor; + if (factor > 1.0f) + HeapNotifyIncreased(); + else + HeapNotifyDecreased(); +} - ASSERT(hatingUnit->GetTypeId() == TYPEID_UNIT); +void ThreatReference::UpdateOnlineState() +{ + OnlineState onlineState = SelectOnlineState(); + if (onlineState == _online) + return; + bool increase = (onlineState > _online); + _online = onlineState; + if (increase) + HeapNotifyIncreased(); + else + HeapNotifyDecreased(); - return true; + if (!IsAvailable()) + _owner->GetThreatManager().SendRemoveToClients(_victim); } -//============================================================ -//================= HostileReference ========================== -//============================================================ - -HostileReference::HostileReference(Unit* refUnit, ThreatManager* threatManager, float threat) +/*static*/ bool ThreatReference::FlagsAllowFighting(Unit const* a, Unit const* b) { - iThreat = threat; - iTempThreatModifier = 0.0f; - link(refUnit, threatManager); - iUnitGuid = refUnit->GetGUID(); - iOnline = true; - iAccessible = true; + if (a->GetTypeId() == TYPEID_UNIT && a->ToCreature()->IsTrigger()) + return false; + if (a->HasUnitFlag(UNIT_FLAG_PVP_ATTACKABLE)) + { + if (b->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_PC)) + return false; + } + else + { + if (b->HasUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC)) + return false; + } + return true; } -//============================================================ -// Tell our refTo (target) object that we have a link -void HostileReference::targetObjectBuildLink() +ThreatReference::OnlineState ThreatReference::SelectOnlineState() { - getTarget()->addHatedBy(this); + // first, check all offline conditions + if (!_owner->CanSeeOrDetect(_victim)) // not in map/phase, or stealth/invis + return ONLINE_STATE_OFFLINE; + if (_victim->HasUnitState(UNIT_STATE_DIED)) // feign death + return ONLINE_STATE_OFFLINE; + if (!FlagsAllowFighting(_owner, _victim) || !FlagsAllowFighting(_victim, _owner)) + return ONLINE_STATE_OFFLINE; + // next, check suppression (immunity to chosen melee attack school) + if (_victim->IsImmunedToDamage(_owner->GetMeleeDamageSchoolMask())) + return ONLINE_STATE_SUPPRESSED; + // or any form of CC that will break on damage - disorient, polymorph, blind etc + if (_victim->HasBreakableByDamageCrowdControlAura()) + return ONLINE_STATE_SUPPRESSED; + // no suppression - we're online + return ONLINE_STATE_ONLINE; } -//============================================================ -// Tell our refTo (taget) object, that the link is cut -void HostileReference::targetObjectDestroyLink() +void ThreatReference::UpdateTauntState(bool victimIsTaunting) { - getTarget()->removeHatedBy(this); -} + if (victimIsTaunting) + { + _taunted = TAUNT_STATE_TAUNT; + HeapNotifyIncreased(); + return; + } -//============================================================ -// Tell our refFrom (source) object, that the link is cut (Target destroyed) + // Check for SPELL_AURA_MOD_DETAUNT (applied from owner to victim) + for (AuraEffect const* eff : _victim->GetAuraEffectsByType(SPELL_AURA_MOD_DETAUNT)) + if (eff->GetCasterGUID() == _owner->GetGUID()) + { + _taunted = TAUNT_STATE_DETAUNT; + HeapNotifyDecreased(); + return; + } -void HostileReference::sourceObjectDestroyLink() -{ - setOnlineOfflineState(false); + _taunted = TAUNT_STATE_NONE; + HeapNotifyChanged(); } -//============================================================ -// Inform the source, that the status of the reference changed - -void HostileReference::fireStatusChanged(ThreatRefStatusChangeEvent& threatRefStatusChangeEvent) +void ThreatReference::ClearThreat(bool sendRemove) { - if (GetSource()) - GetSource()->processThreatEvent(&threatRefStatusChangeEvent); + _owner->GetThreatManager().PurgeThreatListRef(_victim->GetGUID(), sendRemove); + _victim->GetThreatManager().PurgeThreatenedByMeRef(_owner->GetGUID()); + delete this; } -// -- compatibility layer for combat rewrite (PR #19930) -Unit* HostileReference::GetOwner() const { return GetSource()->GetOwner(); } - -//============================================================ - -void HostileReference::addThreat(float modThreat) +/*static*/ bool ThreatManager::CanHaveThreatList(Unit const* who) { - if (!modThreat) - return; - - iThreat += modThreat; + // only creatures can have threat list + if (who->GetTypeId() != TYPEID_UNIT) + return false; - // the threat is changed. Source and target unit have to be available - // if the link was cut before relink it again - if (!isOnline()) - updateOnlineStatus(); + // pets and totems cannot have threat list + if (who->IsPet() || who->IsTotem()) + return false; - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_THREAT_CHANGE, this, modThreat); - fireStatusChanged(event); + // summons cannot have a threat list, unless they are controlled by a creature + if (who->HasUnitTypeMask(UNIT_MASK_MINION | UNIT_MASK_GUARDIAN) && !who->GetOwnerGUID().IsCreature()) + return false; - if (isValid() && modThreat > 0.0f) - { - Unit* victimOwner = getTarget()->GetCharmerOrOwner(); - if (victimOwner && victimOwner->IsAlive()) - GetSource()->addThreat(victimOwner, 0.0f); // create a threat to the owner of a pet, if the pet attacks - } + return true; } -void HostileReference::addThreatPercent(int32 percent) +ThreatManager::ThreatManager(Unit* owner) : _owner(owner), _ownerCanHaveThreatList(false), _ownerEngaged(false), _updateClientTimer(CLIENT_THREAT_UPDATE_INTERVAL), _currentVictimRef(nullptr) { - addThreat(CalculatePct(iThreat, percent)); + for (int8 i = 0; i < MAX_SPELL_SCHOOL; ++i) + _singleSchoolModifiers[i] = 1.0f; } -//============================================================ -// check, if source can reach target and set the status - -void HostileReference::updateOnlineStatus() +void ThreatManager::Initialize() { - bool online = false; - bool accessible = false; - - if (!isValid()) - if (Unit* target = ObjectAccessor::GetUnit(*GetSourceUnit(), getUnitGuid())) - link(target, GetSource()); + _ownerCanHaveThreatList = ThreatManager::CanHaveThreatList(_owner); +} - // only check for online status if - // ref is valid - // target is no player or not gamemaster - // target is not in flight - if (isValid() - && (getTarget()->GetTypeId() != TYPEID_PLAYER || !getTarget()->ToPlayer()->IsGameMaster()) - && !getTarget()->HasUnitState(UNIT_STATE_IN_FLIGHT) - && getTarget()->IsInMap(GetSourceUnit()) - && getTarget()->IsInPhase(GetSourceUnit()) - ) +void ThreatManager::Update(uint32 tdiff) +{ + if (_updateClientTimer <= tdiff) { - Creature* creature = GetSourceUnit()->ToCreature(); - online = getTarget()->isInAccessiblePlaceFor(creature); - if (!online) - { - if (creature->IsWithinCombatRange(getTarget(), creature->m_CombatDistance)) - online = true; // not accessible but stays online - } - else - accessible = true; + _updateClientTimer = CLIENT_THREAT_UPDATE_INTERVAL; + SendThreatListToClients(); } - - setAccessibleState(accessible); - setOnlineOfflineState(online); + else + _updateClientTimer -= tdiff; } -//============================================================ -// set the status and fire the event on status change - -void HostileReference::setOnlineOfflineState(bool isOnline) +Unit* ThreatManager::GetCurrentVictim() const { - if (iOnline != isOnline) - { - iOnline = isOnline; - if (!iOnline) - setAccessibleState(false); // if not online that not accessable as well - - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_ONLINE_STATUS, this); - fireStatusChanged(event); - } + if (_currentVictimRef) + return _currentVictimRef->GetVictim(); + return nullptr; } -//============================================================ +Unit* ThreatManager::GetAnyTarget() const +{ + for (ThreatReference const* ref : _sortedThreatList) + if (!ref->IsOffline()) + return ref->GetVictim(); + return nullptr; +} -void HostileReference::setAccessibleState(bool isAccessible) +Unit* ThreatManager::SelectVictim() { - if (iAccessible != isAccessible) + if (_sortedThreatList.empty()) + return nullptr; + + ThreatReference const* newVictimRef = ReselectVictim(); + if (newVictimRef != _currentVictimRef) { - iAccessible = isAccessible; + if (newVictimRef) + SendNewVictimToClients(newVictimRef); - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_ACCESSIBLE_STATUS, this); - fireStatusChanged(event); + _currentVictimRef = newVictimRef; } + return newVictimRef ? newVictimRef->GetVictim() : nullptr; } -//============================================================ -// prepare the reference for deleting -// this is called be the target - -void HostileReference::removeReference() +bool ThreatManager::IsThreatListEmpty(bool includeOffline) const { - invalidate(); - - ThreatRefStatusChangeEvent event(UEV_THREAT_REF_REMOVE_FROM_LIST, this); - fireStatusChanged(event); + if (includeOffline) + return _sortedThreatList.empty(); + for (ThreatReference const* ref : _sortedThreatList) + if (ref->IsAvailable()) + return false; + return true; } -//============================================================ - -Unit* HostileReference::GetSourceUnit() +bool ThreatManager::IsThreatenedBy(ObjectGuid const& who, bool includeOffline) const { - return (GetSource()->GetOwner()); + auto it = _myThreatListEntries.find(who); + if (it == _myThreatListEntries.end()) + return false; + return (includeOffline || it->second->IsAvailable()); } +bool ThreatManager::IsThreatenedBy(Unit const* who, bool includeOffline) const { return IsThreatenedBy(who->GetGUID(), includeOffline); } -//============================================================ -//================ ThreatContainer =========================== -//============================================================ - -void ThreatContainer::clearReferences() +float ThreatManager::GetThreat(Unit const* who, bool includeOffline) const { - for (ThreatContainer::StorageType::const_iterator i = iThreatList.begin(); i != iThreatList.end(); ++i) - { - (*i)->unlink(); - delete (*i); - } - - iThreatList.clear(); + auto it = _myThreatListEntries.find(who->GetGUID()); + if (it == _myThreatListEntries.end()) + return 0.0f; + return (includeOffline || it->second->IsAvailable()) ? it->second->GetThreat() : 0.0f; } -//============================================================ -// Return the HostileReference of NULL, if not found -HostileReference* ThreatContainer::getReferenceByTarget(Unit const* victim) const +std::vector<ThreatReference*> ThreatManager::GetModifiableThreatList() const { - if (!victim) - return nullptr; - - ObjectGuid guid = victim->GetGUID(); - for (ThreatContainer::StorageType::const_iterator i = iThreatList.begin(); i != iThreatList.end(); ++i) - { - HostileReference* ref = (*i); - if (ref && ref->getUnitGuid() == guid) - return ref; - } - - return nullptr; + std::vector<ThreatReference*> list; + list.reserve(_myThreatListEntries.size()); + for (auto it = _sortedThreatList.ordered_begin(), end = _sortedThreatList.ordered_end(); it != end; ++it) + list.push_back(const_cast<ThreatReference*>(*it)); + return list; } -//============================================================ -// Add the threat, if we find the reference - -HostileReference* ThreatContainer::addThreat(Unit* victim, float threat) +bool ThreatManager::IsThreateningAnyone(bool includeOffline) const { - HostileReference* ref = getReferenceByTarget(victim); - if (ref) - ref->addThreat(threat); - return ref; + if (includeOffline) + return !_threatenedByMe.empty(); + for (auto const& pair : _threatenedByMe) + if (pair.second->IsAvailable()) + return true; + return false; } -//============================================================ - -void ThreatContainer::ModifyThreatByPercent(Unit* victim, int32 percent) +bool ThreatManager::IsThreateningTo(ObjectGuid const& who, bool includeOffline) const { - if (HostileReference* ref = getReferenceByTarget(victim)) - ref->addThreatPercent(percent); + auto it = _threatenedByMe.find(who); + if (it == _threatenedByMe.end()) + return false; + return (includeOffline || it->second->IsAvailable()); } +bool ThreatManager::IsThreateningTo(Unit const* who, bool includeOffline) const { return IsThreateningTo(who->GetGUID(), includeOffline); } -//============================================================ -// Check if the list is dirty and sort if necessary - -void ThreatContainer::update() +void ThreatManager::UpdateOnlineStates(bool meThreateningOthers, bool othersThreateningMe) { - if (iDirty && iThreatList.size() > 1) - iThreatList.sort(Trinity::ThreatOrderPred()); - - iDirty = false; + if (othersThreateningMe) + for (auto const& pair : _myThreatListEntries) + pair.second->UpdateOnlineState(); + if (meThreateningOthers) + for (auto const& pair : _threatenedByMe) + pair.second->UpdateOnlineState(); } -//============================================================ -// return the next best victim -// could be the current victim - -HostileReference* ThreatContainer::selectNextVictim(Creature* attacker, HostileReference* currentVictim) const +static void SaveCreatureHomePositionIfNeed(Creature* c) { - HostileReference* currentRef = nullptr; - bool found = false; - bool noPriorityTargetFound = false; - - ThreatContainer::StorageType::const_iterator lastRef = iThreatList.end(); - --lastRef; + MovementGeneratorType const movetype = c->GetMotionMaster()->GetCurrentMovementGeneratorType(); + if (movetype == WAYPOINT_MOTION_TYPE || movetype == POINT_MOTION_TYPE || (c->IsAIEnabled && c->AI()->IsEscorted())) + c->SetHomePosition(c->GetPosition()); +} - for (ThreatContainer::StorageType::const_iterator iter = iThreatList.begin(); iter != iThreatList.end() && !found;) +void ThreatManager::AddThreat(Unit* target, float amount, SpellInfo const* spell, bool ignoreModifiers, bool ignoreRedirects) +{ + // step 1: we can shortcut if the spell has one of the NO_THREAT attrs set - nothing will happen + if (spell) { - currentRef = (*iter); + if (spell->HasAttribute(SPELL_ATTR1_NO_THREAT)) + return; + if (!_owner->IsEngaged() && spell->HasAttribute(SPELL_ATTR3_NO_INITIAL_AGGRO)) + return; + } - Unit* target = currentRef->getTarget(); - ASSERT(target); // if the ref has status online the target must be there ! + // while riding a vehicle, all threat goes to the vehicle, not the pilot + if (Unit* vehicle = target->GetVehicleBase()) + { + AddThreat(vehicle, amount, spell, ignoreModifiers, ignoreRedirects); + if (target->HasUnitTypeMask(UNIT_MASK_ACCESSORY)) // accessories are fully treated as components of the parent and cannot have threat + return; + amount = 0.0f; + } - // some units are prefered in comparison to others - if (!noPriorityTargetFound && (target->IsImmunedToDamage(attacker->GetMeleeDamageSchoolMask()) || target->HasNegativeAuraWithInterruptFlag(SpellAuraInterruptFlags::Damage))) + // If victim is personal spawn, redirect all aggro to summoner + if (target->IsPrivateObject() && (!GetOwner()->IsPrivateObject() || !GetOwner()->CheckPrivateObjectOwnerVisibility(target))) + { + if (Unit* privateObjectOwner = ObjectAccessor::GetUnit(*GetOwner(), target->GetPrivateObjectOwner())) { - if (iter != lastRef) - { - // current victim is a second choice target, so don't compare threat with it below - if (currentRef == currentVictim) - currentVictim = nullptr; - ++iter; - continue; - } - else - { - // if we reached to this point, everyone in the threatlist is a second choice target. In such a situation the target with the highest threat should be attacked. - noPriorityTargetFound = true; - iter = iThreatList.begin(); - continue; - } + AddThreat(privateObjectOwner, amount, spell, ignoreModifiers, ignoreRedirects); + amount = 0.0f; } + } + + // if we cannot actually have a threat list, we instead just set combat state and avoid creating threat refs altogether + if (!CanHaveThreatList()) + { + CombatManager& combatMgr = _owner->GetCombatManager(); + if (!combatMgr.SetInCombatWith(target)) + return; + // traverse redirects and put them in combat, too + for (auto const& pair : target->GetThreatManager()._redirectInfo) + if (!combatMgr.IsInCombatWith(pair.first)) + if (Unit* redirTarget = ObjectAccessor::GetUnit(*_owner, pair.first)) + combatMgr.SetInCombatWith(redirTarget); + return; + } - if (attacker->CanCreatureAttack(target)) // skip non attackable currently targets + // apply threat modifiers to the amount + if (!ignoreModifiers) + amount = CalculateModifiedThreat(amount, target, spell); + + // if we're increasing threat, send some/all of it to redirection targets instead if applicable + if (!ignoreRedirects && amount > 0.0f) + { + auto const& redirInfo = target->GetThreatManager()._redirectInfo; + if (!redirInfo.empty()) { - if (currentVictim) // select 1.3/1.1 better target in comparison current target + float const origAmount = amount; + for (auto const& pair : redirInfo) // (victim,pct) { - // list sorted and and we check current target, then this is best case - if (currentVictim == currentRef || currentRef->getThreat() <= 1.1f * currentVictim->getThreat()) + Unit* redirTarget = nullptr; + auto it = _myThreatListEntries.find(pair.first); // try to look it up in our threat list first (faster) + if (it != _myThreatListEntries.end()) + redirTarget = it->second->_victim; + else + redirTarget = ObjectAccessor::GetUnit(*_owner, pair.first); + + if (redirTarget) { - if (currentVictim != currentRef && attacker->CanCreatureAttack(currentVictim->getTarget())) - currentRef = currentVictim; // for second case, if currentvictim is attackable - - found = true; - break; - } - - if (currentRef->getThreat() > 1.3f * currentVictim->getThreat() || - (currentRef->getThreat() > 1.1f * currentVictim->getThreat() && - attacker->IsWithinMeleeRange(target))) - { //implement 110% threat rule for targets in melee range - found = true; //and 130% rule for targets in ranged distances - break; //for selecting alive targets + float amountRedirected = CalculatePct(origAmount, pair.second); + AddThreat(redirTarget, amountRedirected, spell, true, true); + amount -= amountRedirected; } } - else // select any - { - found = true; - break; - } } - ++iter; } - if (!found) - currentRef = nullptr; - return currentRef; -} + // ok, now we actually apply threat + // check if we already have an entry - if we do, just increase threat for that entry and we're done + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + { + it->second->AddThreat(amount); + return; + } -//============================================================ -//=================== ThreatManager ========================== -//============================================================ + // otherwise, ensure we're in combat (threat implies combat!) + if (!_owner->GetCombatManager().SetInCombatWith(target)) // if this returns false, we're not actually in combat, and thus cannot have threat! + return; // typical causes: bad scripts trying to add threat to GMs, dead targets etc -ThreatManager::ThreatManager(Unit* owner) : iCurrentVictim(nullptr), iOwner(owner), iUpdateTimer(THREAT_UPDATE_INTERVAL) { } + // ok, we're now in combat - create the threat list reference and push it to the respective managers + ThreatReference* ref = new ThreatReference(this, target, amount); + PutThreatListRef(target->GetGUID(), ref); + target->GetThreatManager().PutThreatenedByMeRef(_owner->GetGUID(), ref); + if (!ref->IsOffline() && !_ownerEngaged) + { + _ownerEngaged = true; + + Creature* cOwner = _owner->ToCreature(); + assert(cOwner); // if we got here the owner can have a threat list, and must be a creature! + SaveCreatureHomePositionIfNeed(cOwner); + if (cOwner->IsAIEnabled) + cOwner->AI()->JustEngagedWith(target); + } +} -// -- compatibility layer for combat rewrite (PR #19930) -void ThreatManager::ForwardThreatForAssistingMe(Unit* victim, float amount, SpellInfo const* spell, bool ignoreModifiers, bool ignoreRedirection) +void ThreatManager::ScaleThreat(Unit* target, float factor) { - (void)ignoreModifiers; (void)ignoreRedirection; - GetOwner()->getHostileRefManager().threatAssist(victim, amount, spell); + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + it->second->ScaleThreat(std::max<float>(factor,0.0f)); } -void ThreatManager::AddThreat(Unit* victim, float amount, SpellInfo const* spell, bool ignoreModifiers, bool ignoreRedirection) +void ThreatManager::MatchUnitThreatToHighestThreat(Unit* target) { - (void)ignoreModifiers; (void)ignoreRedirection; - if (!iOwner->CanHaveThreatList() || iOwner->HasUnitState(UNIT_STATE_EVADE)) + if (_sortedThreatList.empty()) return; - if (iOwner->IsControlledByPlayer() || victim->IsControlledByPlayer()) + auto it = _sortedThreatList.begin(), end = _sortedThreatList.end(); + ThreatReference const* highest = *it; + if (!highest->IsOnline()) + return; + + if (highest->_taunted) // might need to skip this - new max could be one of the preceding elements (heap property) since there is only one taunt element { - if (iOwner->IsFriendlyTo(victim) || victim->IsFriendlyTo(iOwner)) - return; + if ((++it) != end) + { + ThreatReference const* a = *it; + if (a->IsOnline() && a->GetThreat() > highest->GetThreat()) + highest = a; + + if ((++it) != end) + { + ThreatReference const* a = *it; + if (a->IsOnline() && a->GetThreat() > highest->GetThreat()) + highest = a; + } + } } - else if (!iOwner->IsHostileTo(victim) && !victim->IsHostileTo(iOwner)) - return; - iOwner->SetInCombatWith(victim); - victim->SetInCombatWith(iOwner); - addThreat(victim, amount, spell ? spell->GetSchoolMask() : victim->GetMeleeDamageSchoolMask(), spell); + AddThreat(target, highest->GetThreat() - GetThreat(target, true), nullptr, true, true); } -void ThreatManager::ClearAllThreat() +void ThreatManager::TauntUpdate() { - if (iOwner->CanHaveThreatList(true) && !isThreatListEmpty()) - iOwner->SendClearThreatListOpcode(); - clearReferences(); + std::list<AuraEffect*> const& tauntEffects = _owner->GetAuraEffectsByType(SPELL_AURA_MOD_TAUNT); + auto threatEnd = _myThreatListEntries.end(); + ThreatReference* tauntRef = nullptr; + // Only the last taunt effect applied by something still on our threat list is considered + for (auto it = tauntEffects.rbegin(), end = tauntEffects.rend(); it != end; ++it) + { + auto threatIt = _myThreatListEntries.find((*it)->GetCasterGUID()); + if (threatIt == threatEnd) + continue; + if (!threatIt->second->IsOnline()) + continue; + tauntRef = threatIt->second; + break; + } + for (auto const& pair : _myThreatListEntries) + pair.second->UpdateTauntState(pair.second == tauntRef); } -//============================================================ - -void ThreatManager::clearReferences() +void ThreatManager::ResetAllThreat() { - iThreatContainer.clearReferences(); - iThreatOfflineContainer.clearReferences(); - iCurrentVictim = nullptr; - iUpdateTimer = THREAT_UPDATE_INTERVAL; + for (auto const& pair : _myThreatListEntries) + pair.second->SetThreat(0.0f); } -//============================================================ +void ThreatManager::ClearThreat(Unit* target) +{ + auto it = _myThreatListEntries.find(target->GetGUID()); + if (it != _myThreatListEntries.end()) + it->second->ClearThreat(); +} -void ThreatManager::addThreat(Unit* victim, float threat, SpellSchoolMask schoolMask, SpellInfo const* threatSpell) +void ThreatManager::ClearAllThreat() { - if (!ThreatCalcHelper::isValidProcess(victim, GetOwner(), threatSpell)) + _ownerEngaged = false; + if (_myThreatListEntries.empty()) return; - doAddThreat(victim, ThreatCalcHelper::calcThreat(victim, iOwner, threat, schoolMask, threatSpell)); + SendClearAllThreatToClients(); + do + _myThreatListEntries.begin()->second->ClearThreat(false); + while (!_myThreatListEntries.empty()); } -void ThreatManager::doAddThreat(Unit* victim, float threat) +ThreatReference const* ThreatManager::ReselectVictim() { - uint32 redirectThreadPct = victim->GetRedirectThreatPercent(); - Unit* redirectTarget = victim->GetRedirectThreatTarget(); + ThreatReference const* oldVictimRef = _currentVictimRef; + if (oldVictimRef && !oldVictimRef->IsAvailable()) + oldVictimRef = nullptr; + // in 99% of cases - we won't need to actually look at anything beyond the first element + ThreatReference const* highest = _sortedThreatList.top(); + // if the highest reference is offline, the entire list is offline, and we indicate this + if (!highest->IsAvailable()) + return nullptr; + // if we have no old victim, or old victim is still highest, then highest is our target and we're done + if (!oldVictimRef || highest == oldVictimRef) + return highest; + // if highest threat doesn't break 110% of old victim, nothing below it is going to do so either; new victim = old victim and done + if (!ThreatManager::CompareReferencesLT(oldVictimRef, highest, 1.1f)) + return oldVictimRef; + // if highest threat breaks 130%, it's our new target regardless of range (and we're done) + if (ThreatManager::CompareReferencesLT(oldVictimRef, highest, 1.3f)) + return highest; + // if it doesn't break 130%, we need to check if it's melee - if yes, it breaks 110% (we checked earlier) and is our new target + if (_owner->IsWithinMeleeRange(highest->_victim)) + return highest; + // If we get here, highest threat is ranged, but below 130% of current - there might be a melee that breaks 110% below us somewhere, so now we need to actually look at the next highest element + // luckily, this is a heap, so getting the next highest element is O(log n), and we're just gonna do that repeatedly until we've seen enough targets (or find a target) + auto it = _sortedThreatList.ordered_begin(), end = _sortedThreatList.ordered_end(); + while (it != end) + { + ThreatReference const* next = *it; + // if we've found current victim, we're done (nothing above is higher, and nothing below can be higher) + if (next == oldVictimRef) + return next; + // if next isn't above 110% threat, then nothing below it can be either - we're done, old victim stays + if (!ThreatManager::CompareReferencesLT(oldVictimRef, next, 1.1f)) + return oldVictimRef; + // if next is melee, he's above 110% and our new victim + if (_owner->IsWithinMeleeRange(next->_victim)) + return next; + // otherwise the next highest target may still be a melee above 110% and we need to look further + ++it; + } + // we should have found the old victim at some point in the loop above, so execution should never get to this point + ASSERT(false && "Current victim not found in sorted threat list even though it has a reference - manager desync!"); + return nullptr; +} - // If victim is personal spawn, redirect all aggro to summoner - if (victim->IsPrivateObject() && (!GetOwner()->IsPrivateObject() || !GetOwner()->CheckPrivateObjectOwnerVisibility(victim))) +// returns true if a is LOWER on the threat list than b +/*static*/ bool ThreatManager::CompareReferencesLT(ThreatReference const* a, ThreatReference const* b, float aWeight) +{ + if (a->_online != b->_online) // online state precedence (ONLINE > SUPPRESSED > OFFLINE) + return a->_online < b->_online; + if (a->_taunted != b->_taunted) // taunt state precedence (TAUNT > NONE > DETAUNT) + return a->_taunted < b->_taunted; + return (a->GetThreat()*aWeight < b->GetThreat()); +} + +/*static*/ float ThreatManager::CalculateModifiedThreat(float threat, Unit const* victim, SpellInfo const* spell) +{ + // modifiers by spell + if (spell) { - redirectThreadPct = 100; - redirectTarget = ObjectAccessor::GetUnit(*GetOwner(), victim->GetPrivateObjectOwner()); + if (SpellThreatEntry const* threatEntry = sSpellMgr->GetSpellThreatEntry(spell->Id)) + if (threatEntry->pctMod != 1.0f) // flat/AP modifiers handled in Spell::HandleThreatSpells + threat *= threatEntry->pctMod; + + if (Player* modOwner = victim->GetSpellModOwner()) + modOwner->ApplySpellMod(spell, SpellModOp::Hate, threat); } - // must check > 0.0f, otherwise dead loop - if (threat > 0.0f && redirectThreadPct) + // modifiers by effect school + ThreatManager const& victimMgr = victim->GetThreatManager(); + SpellSchoolMask const mask = spell ? spell->GetSchoolMask() : SPELL_SCHOOL_MASK_NORMAL; + switch (mask) { - if (redirectTarget) + case SPELL_SCHOOL_MASK_NORMAL: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_NORMAL]; + break; + case SPELL_SCHOOL_MASK_HOLY: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_HOLY]; + break; + case SPELL_SCHOOL_MASK_FIRE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_FIRE]; + break; + case SPELL_SCHOOL_MASK_NATURE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_NATURE]; + break; + case SPELL_SCHOOL_MASK_FROST: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_FROST]; + break; + case SPELL_SCHOOL_MASK_SHADOW: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_SHADOW]; + break; + case SPELL_SCHOOL_MASK_ARCANE: + threat *= victimMgr._singleSchoolModifiers[SPELL_SCHOOL_ARCANE]; + break; + default: { - float redirectThreat = CalculatePct(threat, redirectThreadPct); - threat -= redirectThreat; - if (ThreatCalcHelper::isValidProcess(redirectTarget, GetOwner())) - _addThreat(redirectTarget, redirectThreat); + auto it = victimMgr._multiSchoolModifiers.find(mask); + if (it != victimMgr._multiSchoolModifiers.end()) + { + threat *= it->second; + break; + } + float mod = victim->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_THREAT, mask); + victimMgr._multiSchoolModifiers[mask] = mod; + threat *= mod; + break; } } - - _addThreat(victim, threat); + return threat; } -void ThreatManager::_addThreat(Unit* victim, float threat) +void ThreatManager::SendClearAllThreatToClients() const { - HostileReference* ref = iThreatContainer.addThreat(victim, threat); - // Ref is not in the online refs, search the offline refs next - if (!ref) - ref = iThreatOfflineContainer.addThreat(victim, threat); + WorldPackets::Combat::ThreatClear threatClear; + threatClear.UnitGUID = _owner->GetGUID(); + _owner->SendMessageToSet(threatClear.Write(), false); +} - if (!ref) // there was no ref => create a new one +void ThreatManager::SendThreatListToClients() const +{ + WorldPackets::Combat::ThreatUpdate threatUpdate; + threatUpdate.UnitGUID = _owner->GetGUID(); + threatUpdate.ThreatList.reserve(_sortedThreatList.size()); + for (ThreatReference const* ref : _sortedThreatList) { - bool isFirst = iThreatContainer.empty(); - // threat has to be 0 here - HostileReference* hostileRef = new HostileReference(victim, this, 0); - iThreatContainer.addReference(hostileRef); - hostileRef->addThreat(threat); // now we add the real threat - if (victim->GetTypeId() == TYPEID_PLAYER && victim->ToPlayer()->IsGameMaster()) - hostileRef->setOnlineOfflineState(false); // GM is always offline - else if (isFirst) - setCurrentVictim(hostileRef); + if (!ref->IsAvailable()) // @todo check if suppressed threat should get sent for bubble/iceblock/hop etc + continue; + + WorldPackets::Combat::ThreatInfo threatInfo; + threatInfo.UnitGUID = ref->GetVictim()->GetGUID(); + threatInfo.Threat = int64(ref->GetThreat() * 100); + threatUpdate.ThreatList.push_back(threatInfo); } + _owner->SendMessageToSet(threatUpdate.Write(), false); } -//============================================================ - -void ThreatManager::ModifyThreatByPercent(Unit* victim, int32 percent) +void ThreatManager::ForwardThreatForAssistingMe(Unit* assistant, float baseAmount, SpellInfo const* spell, bool ignoreModifiers) { - iThreatContainer.ModifyThreatByPercent(victim, percent); + if (spell && spell->HasAttribute(SPELL_ATTR1_NO_THREAT)) // shortcut, none of the calls would do anything + return; + for (auto const& pair : _threatenedByMe) + pair.second->GetOwner()->GetThreatManager().AddThreat(assistant, baseAmount, spell, ignoreModifiers); } -//============================================================ - -Unit* ThreatManager::getHostilTarget() +void ThreatManager::RemoveMeFromThreatLists() { - iThreatContainer.update(); - HostileReference* nextVictim = iThreatContainer.selectNextVictim(GetOwner()->ToCreature(), getCurrentVictim()); - setCurrentVictim(nextVictim); - return getCurrentVictim() != nullptr ? getCurrentVictim()->getTarget() : nullptr; + while (!_threatenedByMe.empty()) + _threatenedByMe.begin()->second->ClearThreat(); } -//============================================================ - -float ThreatManager::getThreat(Unit* victim, bool alsoSearchOfflineList) +void ThreatManager::UpdateMyTempModifiers() { - float threat = 0.0f; - HostileReference* ref = iThreatContainer.getReferenceByTarget(victim); - if (!ref && alsoSearchOfflineList) - ref = iThreatOfflineContainer.getReferenceByTarget(victim); - if (ref) - threat = ref->getThreat(); - return threat; + int32 mod = 0; + for (AuraEffect const* eff : _owner->GetAuraEffectsByType(SPELL_AURA_MOD_TOTAL_THREAT)) + mod += eff->GetAmount(); + + for (auto const& pair : _threatenedByMe) + { + pair.second->_tempModifier = mod; + pair.second->HeapNotifyChanged(); + } } -//============================================================ +void ThreatManager::UpdateMySpellSchoolModifiers() +{ + for (uint8 i = 0; i < MAX_SPELL_SCHOOL; ++i) + _singleSchoolModifiers[i] = _owner->GetTotalAuraMultiplierByMiscMask(SPELL_AURA_MOD_THREAT, 1 << i); + _multiSchoolModifiers.clear(); +} -void ThreatManager::tauntApply(Unit* taunter) +void ThreatManager::RegisterRedirectThreat(uint32 spellId, ObjectGuid const& victim, uint32 pct) { - HostileReference* ref = iThreatContainer.getReferenceByTarget(taunter); - if (getCurrentVictim() && ref && (ref->getThreat() < getCurrentVictim()->getThreat())) - { - if (ref->getTempThreatModifier() == 0.0f) // Ok, temp threat is unused - ref->setTempThreat(getCurrentVictim()->getThreat()); - } + _redirectRegistry[spellId][victim] = pct; + UpdateRedirectInfo(); } -//============================================================ +void ThreatManager::UnregisterRedirectThreat(uint32 spellId) +{ + auto it = _redirectRegistry.find(spellId); + if (it == _redirectRegistry.end()) + return; + _redirectRegistry.erase(it); + UpdateRedirectInfo(); +} -void ThreatManager::tauntFadeOut(Unit* taunter) +void ThreatManager::UnregisterRedirectThreat(uint32 spellId, ObjectGuid const& victim) { - HostileReference* ref = iThreatContainer.getReferenceByTarget(taunter); - if (ref) - ref->resetTempThreat(); + auto it = _redirectRegistry.find(spellId); + if (it == _redirectRegistry.end()) + return; + auto& victimMap = it->second; + auto it2 = victimMap.find(victim); + if (it2 == victimMap.end()) + return; + victimMap.erase(it2); + UpdateRedirectInfo(); } -//============================================================ +void ThreatManager::SendRemoveToClients(Unit const* victim) const +{ + WorldPackets::Combat::ThreatRemove threatRemove; + threatRemove.UnitGUID = _owner->GetGUID(); + threatRemove.AboutGUID = victim->GetGUID(); + _owner->SendMessageToSet(threatRemove.Write(), false); +} -void ThreatManager::setCurrentVictim(HostileReference* pHostileReference) +void ThreatManager::SendNewVictimToClients(ThreatReference const* victimRef) const { - if (pHostileReference && pHostileReference != iCurrentVictim) + WorldPackets::Combat::HighestThreatUpdate highestThreatUpdate; + highestThreatUpdate.UnitGUID = _owner->GetGUID(); + highestThreatUpdate.HighestThreatGUID = victimRef->_victim->GetGUID(); + highestThreatUpdate.ThreatList.reserve(_sortedThreatList.size()); + for (ThreatReference const* ref : _sortedThreatList) { - iOwner->SendChangeCurrentVictimOpcode(pHostileReference); + if (!ref->IsAvailable()) + continue; + + WorldPackets::Combat::ThreatInfo threatInfo; + threatInfo.UnitGUID = ref->GetVictim()->GetGUID(); + threatInfo.Threat = int64(ref->GetThreat() * 100); + highestThreatUpdate.ThreatList.push_back(threatInfo); } - iCurrentVictim = pHostileReference; + _owner->SendMessageToSet(highestThreatUpdate.Write(), false); } -//============================================================ -// The hated unit is gone, dead or deleted -// return true, if the event is consumed +void ThreatManager::PutThreatListRef(ObjectGuid const& guid, ThreatReference* ref) +{ + auto& inMap = _myThreatListEntries[guid]; + ASSERT(!inMap && "Duplicate threat list entry being inserted - memory leak!"); + inMap = ref; + ref->_handle = _sortedThreatList.push(ref); +} -void ThreatManager::processThreatEvent(ThreatRefStatusChangeEvent* threatRefStatusChangeEvent) +void ThreatManager::PurgeThreatListRef(ObjectGuid const& guid, bool sendRemove) { - threatRefStatusChangeEvent->setThreatManager(this); // now we can set the threat manager + auto it = _myThreatListEntries.find(guid); + if (it == _myThreatListEntries.end()) + return; + ThreatReference* ref = it->second; + _myThreatListEntries.erase(it); - HostileReference* hostilRef = threatRefStatusChangeEvent->getReference(); + if (_currentVictimRef == ref) + _currentVictimRef = nullptr; - switch (threatRefStatusChangeEvent->getType()) - { - case UEV_THREAT_REF_THREAT_CHANGE: - if ((getCurrentVictim() == hostilRef && threatRefStatusChangeEvent->getFValue()<0.0f) || - (getCurrentVictim() != hostilRef && threatRefStatusChangeEvent->getFValue()>0.0f)) - setDirty(true); // the order in the threat list might have changed - break; - case UEV_THREAT_REF_ONLINE_STATUS: - if (!hostilRef->isOnline()) - { - if (hostilRef == getCurrentVictim()) - { - setCurrentVictim(nullptr); - setDirty(true); - } - iOwner->SendRemoveFromThreatListOpcode(hostilRef); - iThreatContainer.remove(hostilRef); - iThreatOfflineContainer.addReference(hostilRef); - } - else - { - if (getCurrentVictim() && hostilRef->getThreat() > (1.1f * getCurrentVictim()->getThreat())) - setDirty(true); - iThreatContainer.addReference(hostilRef); - iThreatOfflineContainer.remove(hostilRef); - } - break; - case UEV_THREAT_REF_REMOVE_FROM_LIST: - if (hostilRef == getCurrentVictim()) - { - setCurrentVictim(nullptr); - setDirty(true); - } - iOwner->SendRemoveFromThreatListOpcode(hostilRef); - if (hostilRef->isOnline()) - iThreatContainer.remove(hostilRef); - else - iThreatOfflineContainer.remove(hostilRef); - break; - } + _sortedThreatList.erase(ref->_handle); + if (sendRemove && ref->IsOnline()) + SendRemoveToClients(ref->_victim); } -bool ThreatManager::isNeedUpdateToClient(uint32 time) +void ThreatManager::PutThreatenedByMeRef(ObjectGuid const& guid, ThreatReference* ref) { - if (isThreatListEmpty()) - return false; - - if (time >= iUpdateTimer) - { - iUpdateTimer = THREAT_UPDATE_INTERVAL; - return true; - } - iUpdateTimer -= time; - return false; + auto& inMap = _threatenedByMe[guid]; + ASSERT(!inMap && "Duplicate entry being inserted into threatened by me list - potential memory leak!"); + inMap = ref; } -// Reset all aggro without modifying the threatlist. -void ThreatManager::resetAllAggro() +void ThreatManager::PurgeThreatenedByMeRef(ObjectGuid const& guid) { - ThreatContainer::StorageType &threatList = iThreatContainer.iThreatList; - if (threatList.empty()) - return; - - for (ThreatContainer::StorageType::iterator itr = threatList.begin(); itr != threatList.end(); ++itr) - (*itr)->setThreat(0); + auto it = _threatenedByMe.find(guid); + if (it != _threatenedByMe.end()) + _threatenedByMe.erase(it); +} - setDirty(true); +void ThreatManager::UpdateRedirectInfo() +{ + _redirectInfo.clear(); + uint32 totalPct = 0; + for (auto const& pair : _redirectRegistry) // (spellid, victim -> pct) + for (auto const& victimPair : pair.second) // (victim,pct) + { + uint32 thisPct = std::min<uint32>(100 - totalPct, victimPair.second); + if (thisPct > 0) + { + _redirectInfo.push_back({ victimPair.first, thisPct }); + totalPct += thisPct; + ASSERT(totalPct <= 100); + if (totalPct == 100) + return; + } + } } |