/* * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "AchievementMgr.h" #include "BattlefieldMgr.h" #include "CellImpl.h" #include "Channel.h" #include "ChannelMgr.h" #include "Formulas.h" #include "GameTime.h" #include "GridNotifiers.h" #include "Group.h" #include "Guild.h" #include "InstanceScript.h" #include "Language.h" #include "OutdoorPvPMgr.h" #include "Pet.h" #include "Player.h" #include "ScriptMgr.h" #include "SkillDiscovery.h" #include "SpellAuraEffects.h" #include "SpellMgr.h" #include "UpdateFieldFlags.h" #include "Vehicle.h" #include "Weather.h" #include "WeatherMgr.h" #include "WorldState.h" #include "WorldStatePackets.h" /// @todo: this import is not necessary for compilation and marked as unused by the IDE // however, for some reasons removing it would cause a damn linking issue // there is probably some underlying problem with imports which should properly addressed // see: https://github.com/azerothcore/azerothcore-wotlk/issues/9766 #include "GridNotifiersImpl.h" // Zone Interval should be 1 second constexpr auto ZONE_UPDATE_INTERVAL = 1000; void Player::Update(uint32 p_time) { if (!IsInWorld()) return; sScriptMgr->OnPlayerBeforeUpdate(this, p_time); // undelivered mail if (m_nextMailDelivereTime && m_nextMailDelivereTime <= GameTime::GetGameTime().count()) { SendNewMail(); ++unReadMails; // It will be recalculate at mailbox open (for unReadMails important // non-0 until mailbox open, it also will be recalculated) m_nextMailDelivereTime = time_t(0); } // Update cinematic location, if 500ms have passed and we're doing a // cinematic now. _cinematicMgr->m_cinematicDiff += p_time; if (_cinematicMgr->m_cinematicCamera && _cinematicMgr->m_activeCinematicCameraId && GetMSTimeDiffToNow(_cinematicMgr->m_lastCinematicCheck) > CINEMATIC_UPDATEDIFF) { _cinematicMgr->m_lastCinematicCheck = getMSTime(); _cinematicMgr->UpdateCinematicLocation(p_time); } // used to implement delayed far teleports SetMustDelayTeleport(true); ProcessSpellQueue(); Unit::Update(p_time); SetMustDelayTeleport(false); time_t now = GameTime::GetGameTime().count(); UpdatePvPFlag(now); UpdateFFAPvPFlag(now); UpdateContestedPvP(p_time); UpdateDuelFlag(now); CheckDuelDistance(now); UpdateAfkReport(now); // Xinef: update charm AI only if we are controlled by creature or // non-posses player charm if (IsCharmed() && !HasUnitFlag(UNIT_FLAG_POSSESSED)) { m_charmUpdateTimer += p_time; if (m_charmUpdateTimer >= 1000) { m_charmUpdateTimer = 0; if (Unit* charmer = GetCharmer()) if (charmer->IsAlive()) UpdateCharmedAI(); } } time_t lastTick = m_Last_tick; if (now > m_Last_tick) { // Update items that have just a limited lifetime UpdateItemDuration(uint32(now - m_Last_tick)); // check every minute, less chance to crash and wont break anything. UpdateSoulboundTradeItems(); // Played time uint32 elapsed = uint32(now - m_Last_tick); m_Played_time[PLAYED_TIME_TOTAL] += elapsed; // Total played time m_Played_time[PLAYED_TIME_LEVEL] += elapsed; // Level played time GetSession()->SetTotalTime(GetSession()->GetTotalTime() + elapsed); m_Last_tick = now; } // If mute expired, remove it from the DB if (GetSession()->m_muteTime && GetSession()->m_muteTime < now) { GetSession()->m_muteTime = 0; LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_MUTE_TIME); stmt->SetData(0, 0); // Set the mute time to 0 stmt->SetData(1, ""); stmt->SetData(2, ""); stmt->SetData(3, GetSession()->GetAccountId()); LoginDatabase.Execute(stmt); } if (!m_timedquests.empty()) { QuestSet::iterator iter = m_timedquests.begin(); while (iter != m_timedquests.end()) { QuestStatusData& q_status = m_QuestStatus[*iter]; if (q_status.Timer <= p_time) { uint32 quest_id = *iter; ++iter; // current iter will be removed in FailQuest FailQuest(quest_id); } else { q_status.Timer -= p_time; m_QuestStatusSave[*iter] = true; ++iter; } } } m_achievementMgr->Update(p_time); if (HasUnitState(UNIT_STATE_MELEE_ATTACKING) && !HasUnitState(UNIT_STATE_CASTING) && !HasUnitState(UNIT_STATE_CHARGING)) { if (Unit* victim = GetVictim()) { // default combat reach 10 /// @todo add weapon, skill check if (isAttackReady(BASE_ATTACK)) { if (!IsWithinMeleeRange(victim)) { setAttackTimer(BASE_ATTACK, 100); if (m_swingErrorMsg != 1) // send single time (client auto repeat) { SendAttackSwingNotInRange(); m_swingErrorMsg = 1; } } // 120 degrees of radiant range, if player is not in boundary radius else if (!IsWithinBoundaryRadius(victim) && !HasInArc(2 * float(M_PI) / 3, victim)) { setAttackTimer(BASE_ATTACK, 100); if (m_swingErrorMsg != 2) // send single time (client auto repeat) { SendAttackSwingBadFacingAttack(); m_swingErrorMsg = 2; } } else { m_swingErrorMsg = 0; // reset swing error state // prevent base and off attack in same time, delay attack at // 0.2 sec if (HasOffhandWeaponForAttack()) if (getAttackTimer(OFF_ATTACK) < ATTACK_DISPLAY_DELAY) setAttackTimer(OFF_ATTACK, ATTACK_DISPLAY_DELAY); // do attack AttackerStateUpdate(victim, BASE_ATTACK); resetAttackTimer(BASE_ATTACK); // Blizzlike: Reset ranged swing timer when performing melee attack resetAttackTimer(RANGED_ATTACK); } } if (HasOffhandWeaponForAttack() && isAttackReady(OFF_ATTACK)) { if (!IsWithinMeleeRange(victim)) setAttackTimer(OFF_ATTACK, 100); else if (!IsWithinBoundaryRadius(victim) && !HasInArc(2 * float(M_PI) / 3, victim)) setAttackTimer(BASE_ATTACK, 100); else { // prevent base and off attack in same time, delay attack at // 0.2 sec if (getAttackTimer(BASE_ATTACK) < ATTACK_DISPLAY_DELAY) setAttackTimer(BASE_ATTACK, ATTACK_DISPLAY_DELAY); // do attack AttackerStateUpdate(victim, OFF_ATTACK); resetAttackTimer(OFF_ATTACK); // Blizzlike: Reset ranged swing timer when performing melee attack resetAttackTimer(RANGED_ATTACK); } } /*Unit* owner = victim->GetOwner(); Unit* u = owner ? owner : victim; if (u->IsPvP() && (!duel || duel->opponent != u)) { UpdatePvP(true); RemoveAurasWithInterruptFlags(AURA_INTERRUPT_FLAG_ENTER_PVP_COMBAT); }*/ } } if (HasPlayerFlag(PLAYER_FLAGS_RESTING)) { if (now > lastTick && _restTime > 0) // freeze update { time_t currTime = GameTime::GetGameTime().count(); time_t timeDiff = currTime - _restTime; if (timeDiff >= 10) // freeze update { _restTime = currTime; float bubble = 0.125f * sWorld->getRate(RATE_REST_INGAME); float extraPerSec = ((float) GetUInt32Value(PLAYER_NEXT_LEVEL_XP) / 72000.0f) * bubble; // speed collect rest bonus (section/in hour) SetRestBonus(GetRestBonus() + timeDiff * extraPerSec); } } } if (m_weaponChangeTimer > 0) { if (p_time >= m_weaponChangeTimer) m_weaponChangeTimer = 0; else m_weaponChangeTimer -= p_time; } if (!IsPositionValid()) // pussywizard: will crash below at eg. GetZoneAndAreaId { LOG_INFO("misc", "Player::Update - invalid position ({:0.1f}, {:0.1f}, {:0.1f})! Map: {}, MapId: {}, {}", GetPositionX(), GetPositionY(), GetPositionZ(), (FindMap() ? FindMap()->GetId() : 0), GetMapId(), GetGUID().ToString()); GetSession()->KickPlayer("Invalid position"); return; } if (m_zoneUpdateTimer > 0) { if (p_time >= m_zoneUpdateTimer) { // On zone update tick check if we are still in an inn if we are // supposed to be in one if (HasRestFlag(REST_FLAG_IN_TAVERN)) { AreaTrigger const* atEntry = sObjectMgr->GetAreaTrigger(GetInnTriggerId()); if (!atEntry || !IsInAreaTriggerRadius(atEntry, 5.f)) { RemoveRestFlag(REST_FLAG_IN_TAVERN); } } uint32 newzone, newarea; GetZoneAndAreaId(newzone, newarea); if (m_zoneUpdateId != newzone) UpdateZone(newzone, newarea); // also update area else { // use area updates as well // needed for free far all arenas for example if (m_areaUpdateId != newarea) UpdateArea(newarea); } m_zoneUpdateTimer = ZONE_UPDATE_INTERVAL; } else m_zoneUpdateTimer -= p_time; } sScriptMgr->OnPlayerUpdate(this, p_time); if (IsAlive()) { m_regenTimer += p_time; RegenerateAll(); // Apply buffs from items with Apply on Equip trigger if they are not present. for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) { if (!m_items[i]) continue; std::vector spellIDs; m_items[i]->GetOnEquipSpellIDs(spellIDs); bool apply = false; for (uint32 spellID : spellIDs) if (!apply && !HasAura(spellID)) apply = true; if (apply) ApplyItemEquipSpell(m_items[i], true, false); } } if (m_deathState == DeathState::JustDied) KillPlayer(); if (m_nextSave) { if (p_time >= m_nextSave) { // m_nextSave reset in SaveToDB call SaveToDB(false, false); LOG_DEBUG("entities.player", "Player::Update: Player '{}' ({}) saved", GetName(), GetGUID().ToString()); } else { m_nextSave -= p_time; } } // Handle Water/drowning HandleDrowning(p_time); if (GetDrunkValue()) { m_drunkTimer += p_time; if (m_drunkTimer > 9 * IN_MILLISECONDS) HandleSobering(); } if (HasPendingBind()) { if (_pendingBindTimer <= p_time) { // Player left the instance if (_pendingBindId == GetInstanceId()) BindToInstance(); SetPendingBind(0, 0); } else _pendingBindTimer -= p_time; } // not auto-free ghost from body in instances if (m_deathTimer > 0 && !GetMap()->Instanceable() && !HasPreventResurectionAura()) { if (p_time >= m_deathTimer) { m_deathTimer = 0; BuildPlayerRepop(); RepopAtGraveyard(); } else m_deathTimer -= p_time; } UpdateEnchantTime(p_time); UpdateHomebindTime(p_time); if (!_instanceResetTimes.empty()) { for (InstanceTimeMap::iterator itr = _instanceResetTimes.begin(); itr != _instanceResetTimes.end();) { if (itr->second < now) _instanceResetTimes.erase(itr++); else ++itr; } } // group update SendUpdateToOutOfRangeGroupMembers(); Pet* pet = GetPet(); if (pet && !pet->IsWithinDistInMap(this, GetMap()->GetVisibilityRange()) && !pet->isPossessed()) // if (pet && !pet->IsWithinDistInMap(this, // GetMap()->GetVisibilityDistance()) && (GetCharmGUID() && // (pet->GetGUID() // != GetCharmGUID()))) RemovePet(pet, PET_SAVE_NOT_IN_SLOT, true); // pussywizard: if (m_hostileReferenceCheckTimer <= p_time) { m_hostileReferenceCheckTimer = 15000; if (!GetMap()->IsDungeon()) getHostileRefMgr().deleteReferencesOutOfRange( GetVisibilityRange()); } else m_hostileReferenceCheckTimer -= p_time; // we should execute delayed teleports only for alive(!) players // because we don't want player's ghost teleported from graveyard // xinef: so we store it to the end of the world and teleport out of the ass // after resurrection? if (HasDelayedTeleport() /* && IsAlive()*/) { SetHasDelayedTeleport(false); TeleportTo(teleportStore_dest, teleportStore_options); } if (!IsBeingTeleported() && bRequestForcedVisibilityUpdate) { bRequestForcedVisibilityUpdate = false; UpdateObjectVisibility(true, true); m_delayed_unit_relocation_timer = 0; RemoveFromNotify(NOTIFY_VISIBILITY_CHANGED); } } void Player::UpdateMirrorTimers() { // Desync flags for update on next HandleDrowning if (m_MirrorTimerFlags) m_MirrorTimerFlagsLast = ~m_MirrorTimerFlags; } void Player::UpdateNextMailTimeAndUnreads() { // Update the next delivery time and unread mails time_t cTime = GameTime::GetGameTime().count(); m_nextMailDelivereTime = 0; unReadMails = 0; for (Mail const* mail : GetMails()) { if (mail->deliver_time > cTime) { if (!m_nextMailDelivereTime || m_nextMailDelivereTime > mail->deliver_time) m_nextMailDelivereTime = mail->deliver_time; } // must be not checked yet if (mail->checked & MAIL_CHECK_MASK_READ) continue; // and already delivered or expired if (cTime < mail->deliver_time || cTime > mail->expire_time) continue; unReadMails++; } } void Player::UpdateLFGChannel() { if (!sWorld->getBoolConfig(CONFIG_RESTRICTED_LFG_CHANNEL)) return; ChannelMgr* cMgr = ChannelMgr::forTeam(GetTeamId()); if (!cMgr) return; ChatChannelsEntry const* cce = sChatChannelsStore.LookupEntry(26); /*LookingForGroup*/ Channel* cLFG = cMgr->GetJoinChannel(cce->pattern[m_session->GetSessionDbcLocale()], cce->ChannelID); if (!cLFG) return; Channel* cUsed = nullptr; for (Channel* channel : m_channels) if (channel && channel->GetChannelId() == cce->ChannelID) { cUsed = cLFG; break; } if (IsUsingLfg()) { if (cUsed == cLFG) return; cLFG->JoinChannel(this, ""); } else { if (cLFG != cUsed) return; cLFG->LeaveChannel(this, true); } } void Player::UpdateLocalChannels(uint32 newZone) { // pussywizard: mutex needed (tc changed opcode to THREAD UNSAFE) static std::mutex channelsLock; std::lock_guard guard(channelsLock); if (GetSession()->PlayerLoading() && !IsBeingTeleportedFar()) return; // The client handles it automatically after loading, but not // after teleporting AreaTableEntry const* current_zone = sAreaTableStore.LookupEntry(newZone); if (!current_zone) return; ChannelMgr* cMgr = ChannelMgr::forTeam(GetTeamId()); if (!cMgr) return; std::string current_zone_name = current_zone->area_name[GetSession()->GetSessionDbcLocale()]; for (uint32 i = 0; i < sChatChannelsStore.GetNumRows(); ++i) { if (ChatChannelsEntry const* channel = sChatChannelsStore.LookupEntry(i)) { Channel* usedChannel = nullptr; for (Channel* channel : m_channels) { if (channel && channel->GetChannelId() == i) { usedChannel = channel; break; } } Channel* removeChannel = nullptr; Channel* joinChannel = nullptr; bool sendRemove = true; if (CanJoinConstantChannelInZone(channel, current_zone)) { if (!(channel->flags & CHANNEL_DBC_FLAG_GLOBAL)) { if (channel->flags & CHANNEL_DBC_FLAG_CITY_ONLY && usedChannel) continue; // Already on the channel, as city channel // names are not changing char new_channel_name_buf[100]; std::string currentNameExt; if (channel->flags & CHANNEL_DBC_FLAG_CITY_ONLY) currentNameExt = sObjectMgr->GetAcoreStringForDBCLocale(LANG_CHANNEL_CITY); else currentNameExt = current_zone_name; snprintf(new_channel_name_buf, 100, channel->pattern[m_session->GetSessionDbcLocale()], currentNameExt.c_str()); joinChannel = cMgr->GetJoinChannel(new_channel_name_buf, channel->ChannelID); if (usedChannel) { if (joinChannel != usedChannel) { removeChannel = usedChannel; sendRemove = false; // Do not send leave channel, it // already replaced at client } else joinChannel = nullptr; } } else joinChannel = cMgr->GetJoinChannel( channel->pattern[m_session->GetSessionDbcLocale()], channel->ChannelID); } else removeChannel = usedChannel; if (joinChannel) joinChannel->JoinChannel( this, ""); // Changed Channel: ... or Joined Channel: ... if (removeChannel) { removeChannel->LeaveChannel(this, sendRemove); // Leave old channel std::string name = removeChannel ->GetName(); // Store name, (*i)erase in LeftChannel LeftChannel(removeChannel); // Remove from player's channel list } } } } void Player::UpdateDefense() { if (UpdateSkill(SKILL_DEFENSE, sWorld->getIntConfig(CONFIG_SKILL_GAIN_DEFENSE))) UpdateDefenseBonusesMod(); // update dependent from defense skill part } void Player::UpdateRating(CombatRating cr) { int32 amount = m_baseRatingValue[cr]; // Apply bonus from SPELL_AURA_MOD_RATING_FROM_STAT // stat used stored in miscValueB for this aura AuraEffectList const& modRatingFromStat = GetAuraEffectsByType(SPELL_AURA_MOD_RATING_FROM_STAT); for (AuraEffectList::const_iterator i = modRatingFromStat.begin(); i != modRatingFromStat.end(); ++i) if ((*i)->GetMiscValue() & (1 << cr)) amount += int32(CalculatePct(GetStat(Stats((*i)->GetMiscValueB())), (*i)->GetAmount())); if (amount < 0) amount = 0; SetUInt32Value(static_cast(PLAYER_FIELD_COMBAT_RATING_1) + static_cast(cr), uint32(amount)); bool affectStats = CanModifyStats(); switch (cr) { case CR_WEAPON_SKILL: // Implemented in Unit::RollMeleeOutcomeAgainst case CR_DEFENSE_SKILL: UpdateDefenseBonusesMod(); break; case CR_DODGE: UpdateDodgePercentage(); break; case CR_PARRY: UpdateParryPercentage(); break; case CR_BLOCK: UpdateBlockPercentage(); break; case CR_HIT_MELEE: UpdateMeleeHitChances(); break; case CR_HIT_RANGED: UpdateRangedHitChances(); break; case CR_HIT_SPELL: UpdateSpellHitChances(); break; case CR_CRIT_MELEE: if (affectStats) { UpdateCritPercentage(BASE_ATTACK); UpdateCritPercentage(OFF_ATTACK); } break; case CR_CRIT_RANGED: if (affectStats) UpdateCritPercentage(RANGED_ATTACK); break; case CR_CRIT_SPELL: if (affectStats) UpdateAllSpellCritChances(); break; case CR_HIT_TAKEN_MELEE: // Implemented in Unit::MeleeMissChanceCalc case CR_HIT_TAKEN_RANGED: break; case CR_HIT_TAKEN_SPELL: // Implemented in Unit::MagicSpellHitResult break; case CR_CRIT_TAKEN_MELEE: // Implemented in Unit::RollMeleeOutcomeAgainst // (only for chance to crit) case CR_CRIT_TAKEN_RANGED: break; case CR_CRIT_TAKEN_SPELL: // Implemented in Unit::SpellCriticalBonus (only // for chance to crit) break; case CR_HASTE_MELEE: // Implemented in Player::ApplyRatingMod case CR_HASTE_RANGED: case CR_HASTE_SPELL: break; case CR_WEAPON_SKILL_MAINHAND: // Implemented in // Unit::RollMeleeOutcomeAgainst case CR_WEAPON_SKILL_OFFHAND: case CR_WEAPON_SKILL_RANGED: break; case CR_EXPERTISE: if (affectStats) { UpdateExpertise(BASE_ATTACK); UpdateExpertise(OFF_ATTACK); } break; case CR_ARMOR_PENETRATION: if (affectStats) UpdateArmorPenetration(amount); break; } } void Player::UpdateAllRatings() { for (uint8 cr = 0; cr < MAX_COMBAT_RATING; ++cr) UpdateRating(CombatRating(cr)); } // skill+step, checking for max value bool Player::UpdateSkill(uint32 skill_id, uint32 step) { if (!skill_id || !sScriptMgr->OnPlayerCanUpdateSkill(this, skill_id)) return false; SkillStatusMap::iterator itr = mSkillStatus.find(skill_id); if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED) return false; uint32 valueIndex = PLAYER_SKILL_VALUE_INDEX(itr->second.pos); uint32 data = GetUInt32Value(valueIndex); uint32 value = SKILL_VALUE(data); uint32 max = SKILL_MAX(data); sScriptMgr->OnPlayerBeforeUpdateSkill(this, skill_id, value, max, step); if ((!max) || (!value) || (value >= max)) return false; if (value < max) { uint32 new_value = value + step; if (new_value > max) new_value = max; SetUInt32Value(valueIndex, MAKE_SKILL_VALUE(new_value, max)); if (itr->second.uState != SKILL_NEW) itr->second.uState = SKILL_CHANGED; UpdateSkillEnchantments(skill_id, value, new_value); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, skill_id); sScriptMgr->OnPlayerUpdateSkill(this, skill_id, value, max, step, new_value); return true; } return false; } // iraizo: turn this into a switch statement inline int SkillGainChance(uint32 SkillValue, uint32 GrayLevel, uint32 GreenLevel, uint32 YellowLevel) { if (SkillValue >= GrayLevel) return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREY) * 10; if (SkillValue >= GreenLevel) return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_GREEN) * 10; if (SkillValue >= YellowLevel) return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_YELLOW) * 10; return sWorld->getIntConfig(CONFIG_SKILL_CHANCE_ORANGE) * 10; } bool Player::UpdateGatherSkill(uint32 SkillId, uint32 SkillValue, uint32 RedLevel, uint32 Multiplicator) { LOG_DEBUG("entities.player.skills", "UpdateGatherSkill(SkillId {} SkillLevel {} RedLevel {})", SkillId, SkillValue, RedLevel); uint32 gathering_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_GATHERING); sScriptMgr->OnPlayerUpdateGatheringSkill(this, SkillId, SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25, gathering_skill_gain); // For skinning and Mining chance decrease with level. 1-74 - no decrease, // 75-149 - 2 times, 225-299 - 8 times switch (SkillId) { case SKILL_HERBALISM: case SKILL_LOCKPICKING: case SKILL_JEWELCRAFTING: case SKILL_INSCRIPTION: return UpdateSkillPro(SkillId, SkillGainChance(SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25) * Multiplicator, gathering_skill_gain); case SKILL_SKINNING: if (sWorld->getIntConfig(CONFIG_SKILL_CHANCE_SKINNING_STEPS) == 0) return UpdateSkillPro(SkillId, SkillGainChance(SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25) * Multiplicator, gathering_skill_gain); else return UpdateSkillPro( SkillId, (SkillGainChance(SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25) * Multiplicator) >> (SkillValue / sWorld->getIntConfig(CONFIG_SKILL_CHANCE_SKINNING_STEPS)), gathering_skill_gain); case SKILL_MINING: if (sWorld->getIntConfig(CONFIG_SKILL_CHANCE_MINING_STEPS) == 0) return UpdateSkillPro(SkillId, SkillGainChance(SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25) * Multiplicator, gathering_skill_gain); else return UpdateSkillPro( SkillId, (SkillGainChance(SkillValue, RedLevel + 100, RedLevel + 50, RedLevel + 25) * Multiplicator) >> (SkillValue / sWorld->getIntConfig(CONFIG_SKILL_CHANCE_MINING_STEPS)), gathering_skill_gain); } return false; } bool Player::UpdateCraftSkill(uint32 spellid) { LOG_DEBUG("entities.player.skills", "UpdateCraftSkill spellid {}", spellid); SkillLineAbilityMapBounds bounds = sSpellMgr->GetSkillLineAbilityMapBounds(spellid); for (SkillLineAbilityMap::const_iterator _spell_idx = bounds.first; _spell_idx != bounds.second; ++_spell_idx) { if (_spell_idx->second->SkillLine) { uint32 SkillValue = GetPureSkillValue(_spell_idx->second->SkillLine); // Alchemy Discoveries here SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellid); if (spellInfo && spellInfo->Mechanic == MECHANIC_DISCOVERY) { if (uint32 discoveredSpell = GetSkillDiscoverySpell( _spell_idx->second->SkillLine, spellid, this)) learnSpell(discoveredSpell); } uint32 craft_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_CRAFTING); sScriptMgr->OnPlayerUpdateCraftingSkill(this, _spell_idx->second, SkillValue, craft_skill_gain); return UpdateSkillPro( _spell_idx->second->SkillLine, SkillGainChance(SkillValue, _spell_idx->second->TrivialSkillLineRankHigh, (_spell_idx->second->TrivialSkillLineRankHigh + _spell_idx->second->TrivialSkillLineRankLow) / 2, _spell_idx->second->TrivialSkillLineRankLow), craft_skill_gain); } } return false; } float getProbabilityOfLevelUp(uint32 SkillValue) { /* According to El's Extreme Angling page, from 1 to 115 the probability of * a skill level up is 100% since 100/1 = 100. From 115 - 135 should average * 2 catches per skill up so that means 100/2 = 50%. This returns the * probability depending on the player's SkillValue. */ if (!SkillValue) { return 0.0f; } std::array bounds {115, 135, 160, 190, 215, 295, 315, 355, 425, 450}; std::array dens {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 9.0f, 10.0f, 11.0f, 12.0f, 1.0f}; auto it = std::lower_bound(std::begin(bounds), std::end(bounds), SkillValue); return 100 / dens[std::distance(std::begin(bounds), it)]; } bool Player::UpdateFishingSkill() { LOG_DEBUG("entities.player.skills", "UpdateFishingSkill"); uint32 SkillValue = GetPureSkillValue(SKILL_FISHING); if (SkillValue >= GetMaxSkillValue(SKILL_FISHING)) { return false; } /* Whenever the player clicks on the fishing gameobject the * core will decide based on a probability if the skill raises or not. */ return UpdateSkillPro( SKILL_FISHING, static_cast(getProbabilityOfLevelUp(SkillValue)) * 10, sWorld->getIntConfig(CONFIG_SKILL_GAIN_GATHERING)); } // levels sync. with spell requirement for skill levels to learn // bonus abilities in sSkillLineAbilityStore // Used only to avoid scan DBC at each skill grow static uint32 bonusSkillLevels[] = {75, 150, 225, 300, 375, 450}; static const std::size_t bonusSkillLevelsSize = sizeof(bonusSkillLevels) / sizeof(uint32); bool Player::UpdateSkillPro(uint16 SkillId, int32 Chance, uint32 step) { LOG_DEBUG("entities.player.skills", "UpdateSkillPro(SkillId {}, Chance {:3.1f}%)", SkillId, Chance / 10.0f); if (!SkillId || !sScriptMgr->OnPlayerCanUpdateSkill(this, SkillId)) return false; if (Chance <= 0) // speedup in 0 chance case { LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance={:3.1f}% missed", Chance / 10.0f); return false; } SkillStatusMap::iterator itr = mSkillStatus.find(SkillId); if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED) return false; uint32 valueIndex = PLAYER_SKILL_VALUE_INDEX(itr->second.pos); uint32 data = GetUInt32Value(valueIndex); uint32 SkillValue = SKILL_VALUE(data); uint32 MaxValue = SKILL_MAX(data); sScriptMgr->OnPlayerBeforeUpdateSkill(this, SkillId, SkillValue, MaxValue, step); if (!MaxValue || !SkillValue || SkillValue >= MaxValue) return false; int32 Roll = irand(1, 1000); if (Roll <= Chance) { uint32 new_value = SkillValue + step; if (new_value > MaxValue) new_value = MaxValue; SetUInt32Value(valueIndex, MAKE_SKILL_VALUE(new_value, MaxValue)); if (itr->second.uState != SKILL_NEW) itr->second.uState = SKILL_CHANGED; for (std::size_t i = 0; i < bonusSkillLevelsSize; ++i) { uint32 bsl = bonusSkillLevels[i]; if (SkillValue < bsl && new_value >= bsl) { learnSkillRewardedSpells(SkillId, new_value); break; } } UpdateSkillEnchantments(SkillId, SkillValue, new_value); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_REACH_SKILL_LEVEL, SkillId); LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance={:3.1f}% taken", Chance / 10.0f); sScriptMgr->OnPlayerUpdateSkill(this, SkillId, SkillValue, MaxValue, step, new_value); return true; } LOG_DEBUG("entities.player.skills", "Player::UpdateSkillPro Chance={:3.1f}% missed", Chance / 10.0f); return false; } void Player::UpdateWeaponSkill(Unit* victim, WeaponAttackType attType, Item* item /*= nullptr*/) { if (IsInFeralForm()) return; // always maximized SKILL_FERAL_COMBAT in fact if (GetShapeshiftForm() == FORM_TREE) return; // use weapon but not skill up if (victim->IsCreature() && victim->ToCreature()->HasFlagsExtra(CREATURE_FLAG_EXTRA_NO_SKILL_GAINS)) return; uint32 weapon_skill_gain = sWorld->getIntConfig(CONFIG_SKILL_GAIN_WEAPON); Item* tmpitem = GetWeaponForAttack(attType, true); if (item && item != tmpitem && !item->IsBroken()) { tmpitem = item; } if (!tmpitem && attType == BASE_ATTACK) { // Keep unarmed & fist weapon skills in sync UpdateSkill(SKILL_UNARMED, weapon_skill_gain); UpdateSkill(SKILL_FIST_WEAPONS, weapon_skill_gain); } else if (tmpitem && tmpitem->GetTemplate()->SubClass != ITEM_SUBCLASS_WEAPON_FISHING_POLE) { switch (tmpitem->GetTemplate()->SubClass) { case ITEM_SUBCLASS_WEAPON_FISHING_POLE: break; case ITEM_SUBCLASS_WEAPON_FIST: UpdateSkill(SKILL_UNARMED, weapon_skill_gain); [[fallthrough]]; default: UpdateSkill(tmpitem->GetSkill(), weapon_skill_gain); break; } } UpdateAllCritPercentages(); } void Player::UpdateCombatSkills(Unit* victim, WeaponAttackType attType, bool defence, Item* item /*= nullptr*/) { uint8 playerLevel = GetLevel(); uint16 currentSkillValue = defence ? GetBaseDefenseSkillValue() : GetBaseWeaponSkillValue(attType); uint16 currentSkillMax = 5 * playerLevel; int32 skillDiff = currentSkillMax - currentSkillValue; // Max skill reached for level. // Can in some cases be less than 0: having max skill and then .level -1 as example. if (skillDiff <= 0) { return; } uint8 greylevel = Acore::XP::GetGrayLevel(playerLevel); uint8 moblevel = defence ? victim->getLevelForTarget(this) : victim->GetLevel(); // if defense than victim == attacker /*if (moblevel < greylevel) return;*/ // Patch 3.0.8 (2009-01-20): You can no longer skill up weapons on mobs that are immune to damage. if (moblevel > playerLevel + 5) { moblevel = playerLevel + 5; } int16 lvldif = moblevel - greylevel; if (lvldif < 3) { lvldif = 3; } float chance = float(3 * lvldif * skillDiff) / playerLevel; if (!defence) { chance += chance * 0.02f * GetStat(STAT_INTELLECT); } chance = chance < 1.0f ? 1.0f : chance; // minimum chance to increase skill is 1% LOG_DEBUG("entities.player", "Player::UpdateCombatSkills(defence:{}, playerLevel:{}, moblevel:{}) -> ({}/{}) chance to increase skill is {}%", defence, playerLevel, moblevel, currentSkillValue, currentSkillMax, chance); if (roll_chance_f(chance)) { if (defence) { UpdateDefense(); } else { UpdateWeaponSkill(victim, attType, item); } } } void Player::UpdateSkillsForLevel() { uint16 maxconfskill = sWorld->GetConfigMaxSkillValue(); uint32 maxSkill = GetMaxSkillValueForLevel(); bool alwaysMaxSkill = sWorld->getBoolConfig(CONFIG_ALWAYS_MAX_SKILL_FOR_LEVEL); for (SkillStatusMap::iterator itr = mSkillStatus.begin(); itr != mSkillStatus.end(); ++itr) { if (itr->second.uState == SKILL_DELETED) continue; uint32 pskill = itr->first; SkillRaceClassInfoEntry const* rcEntry = GetSkillRaceClassInfo(pskill, getRace(), getClass()); if (!rcEntry) continue; if (GetSkillRangeType(rcEntry) != SKILL_RANGE_LEVEL) continue; uint32 valueIndex = PLAYER_SKILL_VALUE_INDEX(itr->second.pos); uint32 data = GetUInt32Value(valueIndex); uint32 max = SKILL_MAX(data); uint32 val = SKILL_VALUE(data); /// update only level dependent max skill values if (max != 1) { /// maximize skill always if (alwaysMaxSkill || (rcEntry->Flags & SKILL_FLAG_ALWAYS_MAX_VALUE)) { SetUInt32Value(valueIndex, MAKE_SKILL_VALUE(maxSkill, maxSkill)); if (itr->second.uState != SKILL_NEW) itr->second.uState = SKILL_CHANGED; } else if (max != maxconfskill) /// update max skill value if current /// max skill not maximized { SetUInt32Value(valueIndex, MAKE_SKILL_VALUE(val, maxSkill)); if (itr->second.uState != SKILL_NEW) itr->second.uState = SKILL_CHANGED; } } } } void Player::UpdateSkillsToMaxSkillsForLevel() { for (SkillStatusMap::iterator itr = mSkillStatus.begin(); itr != mSkillStatus.end(); ++itr) { if (itr->second.uState == SKILL_DELETED) continue; uint32 pskill = itr->first; if (IsProfessionOrRidingSkill(pskill)) continue; uint32 valueIndex = PLAYER_SKILL_VALUE_INDEX(itr->second.pos); uint32 data = GetUInt32Value(valueIndex); uint32 max = SKILL_MAX(data); if (max > 1) { SetUInt32Value(valueIndex, MAKE_SKILL_VALUE(max, max)); if (itr->second.uState != SKILL_NEW) itr->second.uState = SKILL_CHANGED; } if (pskill == SKILL_DEFENSE) UpdateDefenseBonusesMod(); } } bool Player::UpdatePosition(float x, float y, float z, float orientation, bool teleport) { if (!Unit::UpdatePosition(x, y, z, orientation, teleport)) return false; // Update player zone if needed if (m_needZoneUpdate) { uint32 newZone, newArea; GetZoneAndAreaId(newZone, newArea); UpdateZone(newZone, newArea); m_needZoneUpdate = false; } if (GetGroup()) SetGroupUpdateFlag(GROUP_UPDATE_FLAG_POSITION); if (GetTrader() && !IsWithinDistInMap(GetTrader(), INTERACTION_DISTANCE)) GetSession()->SendCancelTrade(TRADE_STATUS_TRADE_CANCELED); CheckAreaExploreAndOutdoor(); return true; } void Player::UpdateHonorFields() { /// called when rewarding honor and at each save time_t now = time_t(GameTime::GetGameTime().count()); time_t today = time_t(GameTime::GetGameTime().count() / DAY) * DAY; if (m_lastHonorUpdateTime < today) { time_t yesterday = today - DAY; uint16 kills_today = PAIR32_LOPART(GetUInt32Value(PLAYER_FIELD_KILLS)); // update yesterday's contribution if (m_lastHonorUpdateTime >= yesterday) { SetUInt32Value(PLAYER_FIELD_YESTERDAY_CONTRIBUTION, GetUInt32Value(PLAYER_FIELD_TODAY_CONTRIBUTION)); // this is the first update today, reset today's contribution SetUInt32Value(PLAYER_FIELD_TODAY_CONTRIBUTION, 0); SetUInt32Value(PLAYER_FIELD_KILLS, MAKE_PAIR32(0, kills_today)); } else { // no honor/kills yesterday or today, reset SetUInt32Value(PLAYER_FIELD_YESTERDAY_CONTRIBUTION, 0); SetUInt32Value(PLAYER_FIELD_KILLS, 0); } } m_lastHonorUpdateTime = now; } void Player::UpdateArea(uint32 newArea) { // pussywizard: inform instance, needed for Icecrown Citadel if (InstanceScript* instance = GetInstanceScript()) instance->OnPlayerAreaUpdate(this, m_areaUpdateId, newArea); sScriptMgr->OnPlayerUpdateArea(this, m_areaUpdateId, newArea); // FFA_PVP flags are area and not zone id dependent // so apply them accordingly m_areaUpdateId = newArea; AreaTableEntry const* area = sAreaTableStore.LookupEntry(newArea); pvpInfo.IsInFFAPvPArea = area && (area->flags & AREA_FLAG_ARENA); UpdateFFAPvPState(false); UpdateAreaDependentAuras(newArea); pvpInfo.IsInNoPvPArea = false; if (area && area->IsSanctuary()) { SetByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY); pvpInfo.IsInNoPvPArea = true; CombatStopWithPets(); } else RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_SANCTUARY); uint32 const areaRestFlag = (GetTeamId(true) == TEAM_ALLIANCE) ? AREA_FLAG_REST_ZONE_ALLIANCE : AREA_FLAG_REST_ZONE_HORDE; if (area && area->flags & areaRestFlag) SetRestFlag(REST_FLAG_IN_FACTION_AREA); else RemoveRestFlag(REST_FLAG_IN_FACTION_AREA); } void Player::UpdateZone(uint32 newZone, uint32 newArea, bool force) { if (!newZone) return; if (m_zoneUpdateId != newZone || force) { sOutdoorPvPMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); sOutdoorPvPMgr->HandlePlayerEnterZone(this, newZone); sWorldState->HandlePlayerLeaveZone(this, static_cast(m_zoneUpdateId)); sWorldState->HandlePlayerEnterZone(this, static_cast(newZone)); } if (m_zoneUpdateId != newZone) { sBattlefieldMgr->HandlePlayerLeaveZone(this, m_zoneUpdateId); sBattlefieldMgr->HandlePlayerEnterZone(this, newZone); SendInitWorldStates(newZone, newArea); // only if really enters to new zone, not // just area change, works strange... if (Guild* guild = GetGuild()) guild->UpdateMemberData(this, GUILD_MEMBER_DATA_ZONEID, newZone); } GetMap()->UpdatePlayerZoneStats(m_zoneUpdateId, newZone); // group update if (GetGroup()) SetGroupUpdateFlag(GROUP_UPDATE_FULL); m_zoneUpdateId = newZone; m_zoneUpdateTimer = ZONE_UPDATE_INTERVAL; // zone changed, so area changed as well, update it UpdateArea(newArea); AreaTableEntry const* zone = sAreaTableStore.LookupEntry(newZone); if (!zone) return; if (sWorld->getBoolConfig(CONFIG_WEATHER)) GetMap()->GetOrGenerateZoneDefaultWeather(newZone); GetMap()->SendZoneDynamicInfo(newZone, this); sScriptMgr->OnPlayerUpdateZone(this, newZone, newArea); // in PvP, any not controlled zone (except zone->team == 6, default case) // in PvE, only opposition team capital switch (zone->team) { case AREATEAM_ALLY: pvpInfo.IsInHostileArea = GetTeamId(true) != TEAM_ALLIANCE && (sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL); break; case AREATEAM_HORDE: pvpInfo.IsInHostileArea = GetTeamId(true) != TEAM_HORDE && (sWorld->IsPvPRealm() || zone->flags & AREA_FLAG_CAPITAL); break; case AREATEAM_NONE: // overwrite for battlegrounds, maybe batter some zone flags but current // known not 100% fit to this pvpInfo.IsInHostileArea = sWorld->IsPvPRealm() || InBattleground() || zone->flags & AREA_FLAG_WINTERGRASP; break; default: // 6 in fact pvpInfo.IsInHostileArea = false; break; } // Treat players having a quest flagging for PvP as always in hostile area pvpInfo.IsHostile = pvpInfo.IsInHostileArea || HasPvPForcingQuest(); if (zone->flags & AREA_FLAG_CAPITAL) // Is in a capital city { if (!pvpInfo.IsHostile || zone->IsSanctuary()) SetRestFlag(REST_FLAG_IN_CITY); pvpInfo.IsInNoPvPArea = true; } else RemoveRestFlag(REST_FLAG_IN_CITY); // Recently left a capital city UpdatePvPState(); // remove items with area/map limitations (delete only for alive player to // allow back in ghost mode) if player resurrected at teleport this will be // applied in resurrect code if (IsAlive()) DestroyZoneLimitedItem(true, newZone); // check some item equip limitations (in result lost CanTitanGrip at talent // reset, for example) AutoUnequipOffhandIfNeed(); // recent client version not send leave/join channel packets for built-in // local channels UpdateLocalChannels(newZone); UpdateZoneDependentAuras(newZone); } void Player::UpdateEquipSpellsAtFormChange() { for (uint8 i = 0; i < INVENTORY_SLOT_BAG_END; ++i) { if (m_items[i] && !m_items[i]->IsBroken() && CanUseAttackType(GetAttackBySlot(i))) { ApplyItemEquipSpell(m_items[i], false, true); // remove spells that not fit to form ApplyItemEquipSpell( m_items[i], true, true); // add spells that fit form but not active } } // item set bonuses not dependent from item broken state for (std::size_t setindex = 0; setindex < ItemSetEff.size(); ++setindex) { ItemSetEffect* eff = ItemSetEff[setindex]; if (!eff) continue; for (uint32 y = 0; y < MAX_ITEM_SET_SPELLS; ++y) { SpellInfo const* spellInfo = eff->spells[y]; if (!spellInfo) continue; ApplyEquipSpell(spellInfo, nullptr, false, true); // remove spells that not fit to form if (!sScriptMgr->OnPlayerCanApplyEquipSpellsItemSet(this, eff)) break; ApplyEquipSpell(spellInfo, nullptr, true, true); // add spells that fit form but not active } } } void Player::UpdateHomebindTime(uint32 time) { // GMs never get homebind timer online if (m_InstanceValid || IsGameMaster()) { if (m_HomebindTimer) // instance valid, but timer not reset { // hide reminder WorldPacket data(SMSG_RAID_GROUP_ONLY, 4 + 4); data << uint32(0); data << uint32(0); SendDirectMessage(&data); } // instance is valid, reset homebind timer m_HomebindTimer = 0; } else if (m_HomebindTimer > 0) { if (time >= m_HomebindTimer) { // teleport to nearest graveyard RepopAtGraveyard(); } else m_HomebindTimer -= time; } else { // instance is invalid, start homebind timer m_HomebindTimer = 60000; // send message to player WorldPacket data(SMSG_RAID_GROUP_ONLY, 4 + 4); data << uint32(m_HomebindTimer); data << uint32(1); SendDirectMessage(&data); LOG_DEBUG( "maps", "PLAYER: Player '{}' ({}) will be teleported to homebind in 60 " "seconds", GetName(), GetGUID().ToString()); } } void Player::UpdatePvPState() { UpdateFFAPvPState(); if (pvpInfo.IsHostile) // in hostile area { if (IsInFlight()) // on taxi return; if (!IsPvP() || pvpInfo.EndTimer != 0) UpdatePvP(true, true); } else // in friendly area { if (IsPvP() && !HasPlayerFlag(PLAYER_FLAGS_IN_PVP) && pvpInfo.EndTimer == 0) pvpInfo.EndTimer = GameTime::GetGameTime().count(); // start toggle-off } } void Player::UpdateFFAPvPState(bool reset /*= true*/) { /// @todo: should we always synchronize UNIT_FIELD_BYTES_2, 1 of controller // and controlled? no, we shouldn't, those are checked for affecting player // by client if (!pvpInfo.IsInNoPvPArea && !IsGameMaster() && (pvpInfo.IsInFFAPvPArea || sWorld->IsFFAPvPRealm())) { if (!IsFFAPvP()) { sScriptMgr->OnPlayerFfaPvpStateUpdate(this, true); SetByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP); for (ControlSet::iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) (*itr)->SetByteValue(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP); } if (pvpInfo.IsInFFAPvPArea) { pvpInfo.FFAPvPEndTimer = time_t(0); } } else if (IsFFAPvP()) { if ((pvpInfo.IsInNoPvPArea || IsGameMaster()) || reset || !pvpInfo.EndTimer) { pvpInfo.FFAPvPEndTimer = time_t(0); if (HasByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP)) { RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP); sScriptMgr->OnPlayerFfaPvpStateUpdate(this, false); } for (ControlSet::iterator itr = m_Controlled.begin(); itr != m_Controlled.end(); ++itr) (*itr)->RemoveByteFlag(UNIT_FIELD_BYTES_2, 1, UNIT_BYTE2_FLAG_FFA_PVP); // xinef: iterate attackers AttackerSet toRemove; AttackerSet const& attackers = getAttackers(); for (AttackerSet::const_iterator itr = attackers.begin(); itr != attackers.end(); ++itr) if (!(*itr)->IsValidAttackTarget(this)) toRemove.insert(*itr); for (AttackerSet::const_iterator itr = toRemove.begin(); itr != toRemove.end(); ++itr) (*itr)->AttackStop(); // xinef: remove our own victim if (Unit* victim = GetVictim()) if (!IsValidAttackTarget(victim)) AttackStop(); } else { // Not in FFA PvP Area // Not FFA PvP realm // Not FFA PvP timer already set // Being recently in PvP combat if (!pvpInfo.IsInFFAPvPArea && !sWorld->IsFFAPvPRealm() && !pvpInfo.FFAPvPEndTimer) { pvpInfo.FFAPvPEndTimer = GameTime::GetGameTime().count() + sWorld->getIntConfig(CONFIG_FFA_PVP_TIMER); } } } } void Player::UpdatePvP(bool state, bool _override) { if (!state || _override) { SetPvP(state); pvpInfo.EndTimer = 0; } else { pvpInfo.EndTimer = GameTime::GetGameTime().count(); SetPvP(state); } RemovePlayerFlag(PLAYER_FLAGS_PVP_TIMER); sScriptMgr->OnPlayerPVPFlagChange(this, state); } void Player::UpdatePotionCooldown(Spell* spell) { // no potion used i combat or still in combat if (!GetLastPotionId() || IsInCombat()) return; // Call not from spell cast, send cooldown event for item spells if no in // combat if (!spell) { // spell/item pair let set proper cooldown (except not existed charged // spell cooldown spellmods for potions) if (ItemTemplate const* proto = sObjectMgr->GetItemTemplate(GetLastPotionId())) for (uint8 idx = 0; idx < MAX_ITEM_SPELLS; ++idx) if (proto->Spells[idx].SpellId && proto->Spells[idx].SpellTrigger == ITEM_SPELLTRIGGER_ON_USE) if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(proto->Spells[idx].SpellId)) SendCooldownEvent(spellInfo, GetLastPotionId()); } // from spell cases (m_lastPotionId set in Spell::SendSpellCooldown) else { if (spell->IsIgnoringCooldowns()) return; SendCooldownEvent(spell->m_spellInfo, m_lastPotionId, spell); } SetLastPotionId(0); } template void Player::UpdateVisibilityOf(Player* target, UpdateData& data, std::vector& visibleNow); template void Player::UpdateVisibilityOf(Creature* target, UpdateData& data, std::vector& visibleNow); template void Player::UpdateVisibilityOf(Corpse* target, UpdateData& data, std::vector& visibleNow); template void Player::UpdateVisibilityOf(GameObject* target, UpdateData& data, std::vector& visibleNow); template void Player::UpdateVisibilityOf(DynamicObject* target, UpdateData& data, std::vector& visibleNow); void Player::UpdateVisibilityForPlayer(bool mapChange) { // After added to map seer must be a player - there is no possibility to // still have different seer (all charm auras must be already removed) if (mapChange && m_seer != this) m_seer = this; Acore::VisibleNotifier notifier(*this, mapChange); Cell::VisitObjects(m_seer, notifier, GetSightRange()); Cell::VisitFarVisibleObjects(m_seer, notifier, VISIBILITY_DISTANCE_GIGANTIC); notifier.SendToSelf(); if (mapChange) m_last_notify_position.Relocate(-5000.0f, -5000.0f, -5000.0f, 0.0f); } void Player::UpdateObjectVisibility(bool forced, bool fromUpdate) { // Prevent updating visibility if player is not in world (example: LoadFromDB sets drunkstate which updates invisibility while player is not in map) if (!IsInWorld()) return; if (!forced) AddToNotify(NOTIFY_VISIBILITY_CHANGED); else if (!isBeingLoaded()) { if (!fromUpdate) // pussywizard: { bRequestForcedVisibilityUpdate = true; return; } Unit::UpdateObjectVisibility(true); UpdateVisibilityForPlayer(); } } template inline void UpdateVisibilityOf_helper(Player* player, T* target, std::vector& /*v*/) { player->GetObjectVisibilityContainer().LinkWorldObjectVisibility(target); } template <> inline void UpdateVisibilityOf_helper(Player* player, GameObject* target, std::vector& /*v*/) { player->GetObjectVisibilityContainer().LinkWorldObjectVisibility(target); } template <> inline void UpdateVisibilityOf_helper(Player* player, Creature* target, std::vector& v) { player->GetObjectVisibilityContainer().LinkWorldObjectVisibility(target); v.push_back(target); } template <> inline void UpdateVisibilityOf_helper(Player* player, Player* target, std::vector& v) { player->GetObjectVisibilityContainer().LinkWorldObjectVisibility(target); v.push_back(target); } template inline void BeforeVisibilityDestroy(T* /*t*/, Player* /*p*/) { } template <> inline void BeforeVisibilityDestroy(Creature* t, Player* p) { if (p->GetPetGUID() == t->GetGUID() && t->IsPet()) ((Pet*) t)->Remove(PET_SAVE_NOT_IN_SLOT, true); } template void Player::UpdateVisibilityOf(T* target, UpdateData& data, std::vector& visibleNow) { GetMap()->AddObjectToPendingUpdateList(target); if (HaveAtClient(target)) { if (!CanSeeOrDetect(target, false, true)) { BeforeVisibilityDestroy(target, this); target->BuildOutOfRangeUpdateBlock(&data); GetObjectVisibilityContainer().UnlinkWorldObjectVisibility(target); } } else { if (CanSeeOrDetect(target, false, true)) { target->BuildCreateUpdateBlockForPlayer(&data, this); UpdateVisibilityOf_helper(this, target, visibleNow); } } } void Player::GetInitialVisiblePackets(Unit* target) { GetAurasForTarget(target); if (target->IsAlive()) { if (target->HasUnitState(UNIT_STATE_MELEE_ATTACKING) && target->GetVictim()) target->SendMeleeAttackStart(target->GetVictim(), this); } } void Player::UpdateVisibilityOf(WorldObject* target) { if (HaveAtClient(target)) { if (!CanSeeOrDetect(target, false, true)) { if (target->IsCreature()) BeforeVisibilityDestroy(target->ToCreature(), this); target->DestroyForPlayer(this); GetObjectVisibilityContainer().UnlinkWorldObjectVisibility(target); } } else { if (CanSeeOrDetect(target, false, true)) { target->SendUpdateToPlayer(this); GetObjectVisibilityContainer().LinkWorldObjectVisibility(target); // target aura duration for caster show only if target exist at // caster client send data at target visibility change (adding to // client) if (target->IsUnit()) GetInitialVisiblePackets((Unit*) target); } } } void Player::UpdateTriggerVisibility() { if (!IsInWorld()) return; if (GetObjectVisibilityContainer().GetVisibleWorldObjectsMap()->empty()) return; UpdateData udata; DoForAllVisibleWorldObjects([this, &udata](WorldObject* worldObject) { if (worldObject->IsCreature()) { Creature* creature = worldObject->ToCreature(); // Update fields of triggers, transformed units or unselectable // units (values dependent on GM state) if (!creature || (!creature->IsTrigger() && !creature->HasTransformAura() && !creature->HasUnitFlag(UNIT_FLAG_NOT_SELECTABLE))) return; creature->SetFieldNotifyFlag(UF_FLAG_PUBLIC); creature->BuildValuesUpdateBlockForPlayer(&udata, this); creature->RemoveFieldNotifyFlag(UF_FLAG_PUBLIC); } else if (worldObject->IsGameObject()) { GameObject* go = worldObject->ToGameObject(); if (!go) return; go->SetFieldNotifyFlag(UF_FLAG_PUBLIC); go->BuildValuesUpdateBlockForPlayer(&udata, this); go->RemoveFieldNotifyFlag(UF_FLAG_PUBLIC); } }); if (!udata.HasData()) return; WorldPacket packet; udata.BuildPacket(packet); SendDirectMessage(&packet); } void Player::UpdateForQuestWorldObjects() { if (GetObjectVisibilityContainer().GetVisibleWorldObjectsMap()->empty()) return; UpdateData udata; DoForAllVisibleWorldObjects([this, &udata](WorldObject* worldObject) { if (worldObject->IsGameObject()) { if (GameObject* obj = worldObject->ToGameObject()) obj->BuildValuesUpdateBlockForPlayer(&udata, this); } else if (worldObject->IsCreature()) { Creature* obj = worldObject->ToCreature(); if (!obj) return; // check if this unit requires quest specific flags if (obj->HasNpcFlag(UNIT_NPC_FLAG_SPELLCLICK)) { SpellClickInfoMapBounds clickPair = sObjectMgr->GetSpellClickInfoMapBounds(obj->GetEntry()); for (SpellClickInfoContainer::const_iterator _itr = clickPair.first; _itr != clickPair.second; ++_itr) { //! This code doesn't look right, but it was logically converted to condition system to do the exact //! same thing it did before. It definitely needs to be overlooked for intended functionality. ConditionList conds = sConditionMgr->GetConditionsForSpellClickEvent(obj->GetEntry(), _itr->second.spellId); bool buildUpdateBlock = false; for (ConditionList::const_iterator jtr = conds.begin(); jtr != conds.end() && !buildUpdateBlock; ++jtr) if ((*jtr)->ConditionType == CONDITION_QUESTREWARDED || (*jtr)->ConditionType == CONDITION_QUESTTAKEN) buildUpdateBlock = true; if (buildUpdateBlock) { obj->BuildValuesUpdateBlockForPlayer(&udata, this); break; } } } else if (obj->HasNpcFlag(UNIT_NPC_FLAG_VENDOR_MASK | UNIT_NPC_FLAG_TRAINER)) obj->BuildValuesUpdateBlockForPlayer(&udata, this); } }); if (!udata.HasData()) return; WorldPacket packet; udata.BuildPacket(packet); SendDirectMessage(&packet); } void Player::UpdateTitansGrip() { // 10% damage reduce if 2x2h weapons are used if (!CanTitanGrip()) RemoveAurasDueToSpell(49152); else if (Aura* aur = GetAura(49152)) aur->RecalculateAmountOfEffects(); } void Player::UpdateZoneDependentAuras(uint32 newZone) { // Some spells applied at enter into zone (with subzones), aura removed in // UpdateAreaDependentAuras that called always at zone->area update SpellAreaForAreaMapBounds saBounds = sSpellMgr->GetSpellAreaForAreaMapBounds(newZone); for (SpellAreaForAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) if (itr->second->autocast && itr->second->IsFitToRequirements(this, newZone, 0)) if (!HasAura(itr->second->spellId)) CastSpell(this, itr->second->spellId, true); } void Player::UpdateAreaDependentAuras(uint32 newArea) { // remove auras from spells with area limitations for (AuraMap::iterator iter = m_ownedAuras.begin(); iter != m_ownedAuras.end();) { // use m_zoneUpdateId for speed: UpdateArea called from UpdateZone or // instead UpdateZone in both cases m_zoneUpdateId up-to-date if (iter->second->GetSpellInfo()->CheckLocation( GetMapId(), m_zoneUpdateId, newArea, this, false) != SPELL_CAST_OK) RemoveOwnedAura(iter); else ++iter; } // Xinef: check controlled auras if (!m_Controlled.empty()) for (ControlSet::iterator itr = m_Controlled.begin(); itr != m_Controlled.end();) { Unit* controlled = *itr; ++itr; if (controlled && !controlled->IsPet()) { Unit::AuraMap& tAuras = controlled->GetOwnedAuras(); for (Unit::AuraMap::iterator auraIter = tAuras.begin(); auraIter != tAuras.end();) { if (auraIter->second->GetSpellInfo()->CheckLocation( GetMapId(), m_zoneUpdateId, newArea, nullptr) != SPELL_CAST_OK) controlled->RemoveOwnedAura(auraIter); else ++auraIter; } } } // some auras applied at subzone enter SpellAreaForAreaMapBounds saBounds = sSpellMgr->GetSpellAreaForAreaMapBounds(newArea); for (SpellAreaForAreaMap::const_iterator itr = saBounds.first; itr != saBounds.second; ++itr) if (itr->second->autocast && itr->second->IsFitToRequirements(this, m_zoneUpdateId, newArea)) if (!HasAura(itr->second->spellId)) CastSpell(this, itr->second->spellId, true); if (newArea == 4273 && GetVehicle() && GetPositionX() > 400) // Ulduar { switch (GetVehicleBase()->GetEntry()) { case 33062: case 33109: case 33060: GetVehicle()->Dismiss(); break; } } } void Player::UpdateCorpseReclaimDelay() { bool pvp = m_ExtraFlags & PLAYER_EXTRA_PVP_DEATH; if ((pvp && !sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVP)) || (!pvp && !sWorld->getBoolConfig(CONFIG_DEATH_CORPSE_RECLAIM_DELAY_PVE))) return; time_t now = GameTime::GetGameTime().count(); if (now < m_deathExpireTime) { // full and partly periods 1..3 uint64 count = (m_deathExpireTime - now) / DEATH_EXPIRE_STEP + 1; if (count < MAX_DEATH_COUNT) m_deathExpireTime = now + (count + 1) * DEATH_EXPIRE_STEP; else m_deathExpireTime = now + MAX_DEATH_COUNT * DEATH_EXPIRE_STEP; } else m_deathExpireTime = now + DEATH_EXPIRE_STEP; } void Player::UpdateCharmedAI() { // Xinef: maybe passed as argument? Unit* charmer = GetCharmer(); CharmInfo* charmInfo = GetCharmInfo(); // Xinef: needs more thinking, maybe kill player? if (!charmer || !charmInfo) return; // Xinef: we should be killed if caster enters evade mode and charm is // infinite if (charmer->IsCreature() && charmer->ToCreature()->IsInEvadeMode()) { AuraEffectList const& auras = GetAuraEffectsByType(SPELL_AURA_MOD_CHARM); for (AuraEffectList::const_iterator iter = auras.begin(); iter != auras.end(); ++iter) if ((*iter)->GetCasterGUID() == charmer->GetGUID() && (*iter)->GetBase()->IsPermanent()) { Unit::DealDamage(charmer, this, GetHealth(), nullptr, DIRECT_DAMAGE, SPELL_SCHOOL_MASK_NORMAL, nullptr, false); return; } } Unit* target = GetVictim(); if (target) SetInFront(target); if (HasUnitState(UNIT_STATE_CASTING)) return; bool Mages = getClassMask() & (1 << (CLASS_MAGE - 1) | 1 << (CLASS_WARLOCK - 1) | 1 << (CLASS_DRUID - 1) | 1 << (CLASS_HUNTER - 1) | 1 << (CLASS_PRIEST - 1)); // Xinef: charmer type specific actions if (charmer->IsPlayer()) { bool follow = false; if (!target) { if (charmInfo->GetPlayerReactState() == REACT_PASSIVE) follow = true; else if (charmInfo->GetPlayerReactState() == REACT_DEFENSIVE) { if (charmer->GetVictim()) target = charmer->GetVictim(); else follow = true; } if (follow) { if (!HasUnitState(UNIT_STATE_FOLLOW)) GetMotionMaster()->MoveFollow(charmer, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); return; } } else if (target && GetMotionMaster()->GetCurrentMovementGeneratorType() != CHASE_MOTION_TYPE) GetMotionMaster()->MoveChase(target, Mages ? 15 : 4); } if (!target || !IsValidAttackTarget(target)) { target = SelectNearbyTarget(nullptr, GetMap()->IsDungeon() ? 100.f : 30.f); if (!target) { if (!HasUnitState(UNIT_STATE_FOLLOW)) GetMotionMaster()->MoveFollow(charmer, PET_FOLLOW_DIST, PET_FOLLOW_ANGLE); return; } GetMotionMaster()->MoveChase(target, Mages ? 15 : 4); Attack(target, true); } else { float Distance = GetDistance(target); uint8 rnd = urand(0, 1); if (Mages) { if ((GetPower(POWER_MANA) * 100 / GetMaxPower(POWER_MANA)) < 10) { GetMotionMaster()->MoveChase(target, 4); return; } if (Distance <= 3) { if (urand(0, 1)) { if (m_charmAISpells[SPELL_T_STUN] && !HasSpellCooldown(m_charmAISpells[SPELL_T_STUN])) CastSpell(target, m_charmAISpells[SPELL_T_STUN], false); else if (m_charmAISpells[SPELL_ROOT_OR_FEAR] && !HasSpellCooldown( m_charmAISpells[SPELL_ROOT_OR_FEAR])) CastSpell(target, m_charmAISpells[SPELL_ROOT_OR_FEAR], false); else if (m_charmAISpells[SPELL_IMMUNITY] && !HasSpellCooldown(m_charmAISpells[SPELL_IMMUNITY])) CastSpell(this, m_charmAISpells[SPELL_IMMUNITY], true); } else { switch (urand(0, 1)) { case 0: if (m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd] && !HasSpellCooldown( m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd])) CastSpell( target, m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd], false); break; case 1: if (m_charmAISpells[SPELL_DOT_DAMAGE] && !HasSpellCooldown( m_charmAISpells[SPELL_DOT_DAMAGE])) CastSpell(target, m_charmAISpells[SPELL_DOT_DAMAGE], false); break; } } } else { switch (urand(0, 2)) { case 0: if (m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd] && !HasSpellCooldown( m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd])) CastSpell(target, m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd], false); break; case 1: if (m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd] && !HasSpellCooldown( m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd])) CastSpell(target, m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd], false); break; case 2: if (m_charmAISpells[SPELL_DOT_DAMAGE] && !HasSpellCooldown(m_charmAISpells[SPELL_DOT_DAMAGE])) CastSpell(target, m_charmAISpells[SPELL_DOT_DAMAGE], false); break; } } } else { if (Distance > 10) { GetMotionMaster()->MoveChase(target, 2.0f); if (m_charmAISpells[SPELL_T_CHARGE] && !HasSpellCooldown(m_charmAISpells[SPELL_T_CHARGE])) CastSpell(target, m_charmAISpells[SPELL_T_CHARGE], false); else if (m_charmAISpells[SPELL_FAST_RUN] && !HasSpellCooldown(m_charmAISpells[SPELL_FAST_RUN])) CastSpell(this, m_charmAISpells[SPELL_FAST_RUN], false); } if (HasUnitState(UNIT_STATE_CASTING)) return; switch (urand(0, 2)) { case 0: if (m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd] && !HasSpellCooldown( m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd])) CastSpell(target, m_charmAISpells[SPELL_INSTANT_DAMAGE + rnd], false); break; case 1: if (m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd] && !HasSpellCooldown( m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd])) CastSpell(target, m_charmAISpells[SPELL_HIGH_DAMAGE1 + rnd], false); break; case 2: if (m_charmAISpells[SPELL_DOT_DAMAGE] && !HasSpellCooldown(m_charmAISpells[SPELL_DOT_DAMAGE])) CastSpell(target, m_charmAISpells[SPELL_DOT_DAMAGE], false); break; } } } } void Player::UpdateLootAchievements(LootItem* item, Loot* loot) { UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_ITEM, item->itemid, item->count); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_TYPE, loot->loot_type, item->count); UpdateAchievementCriteria(ACHIEVEMENT_CRITERIA_TYPE_LOOT_EPIC_ITEM, item->itemid, item->count); } void Player::UpdateAchievementCriteria(AchievementCriteriaTypes type, uint32 miscValue1 /*= 0*/, uint32 miscValue2 /*= 0*/, Unit* unit /*= nullptr*/) { m_achievementMgr->UpdateAchievementCriteria(type, miscValue1, miscValue2, unit); } void Player::UpdateFallInformationIfNeed(MovementInfo const& minfo, uint16 opcode) { if (m_lastFallTime >= minfo.fallTime || m_lastFallZ <= minfo.pos.GetPositionZ() || opcode == MSG_MOVE_FALL_LAND) SetFallInformation(minfo.fallTime, minfo.pos.GetPositionZ()); } void Player::UpdateSpecCount(uint8 count) { uint32 curCount = GetSpecsCount(); if (curCount == count) return; if (m_activeSpec >= count) ActivateSpec(0); CharacterDatabaseTransaction trans = CharacterDatabase.BeginTransaction(); CharacterDatabasePreparedStatement* stmt = nullptr; // Copy spec data if (count > curCount) { _SaveActions(trans); // make sure the button list is cleaned up for (ActionButtonList::iterator itr = m_actionButtons.begin(); itr != m_actionButtons.end(); ++itr) { stmt = CharacterDatabase.GetPreparedStatement(CHAR_INS_CHAR_ACTION); stmt->SetData(0, GetGUID().GetCounter()); stmt->SetData(1, 1); stmt->SetData(2, itr->first); stmt->SetData(3, itr->second.GetAction()); stmt->SetData(4, uint8(itr->second.GetType())); trans->Append(stmt); } } // Delete spec data for removed spec. else if (count < curCount) { _SaveActions(trans); stmt = CharacterDatabase.GetPreparedStatement( CHAR_DEL_CHAR_ACTION_EXCEPT_SPEC); stmt->SetData(0, m_activeSpec); stmt->SetData(1, GetGUID().GetCounter()); trans->Append(stmt); m_activeSpec = 0; } CharacterDatabase.CommitTransaction(trans); SetSpecsCount(count); SendTalentsInfoData(false); } void Player::SendUpdateWorldState(uint32 variable, uint32 value) const { WorldPackets::WorldState::UpdateWorldState worldstate; worldstate.VariableID = variable; worldstate.Value = value; SendDirectMessage(worldstate.Write()); } void Player::ProcessTerrainStatusUpdate() { // process liquid auras using generic unit code Unit::ProcessTerrainStatusUpdate(); LiquidData const& liquidData = GetLiquidData(); // player specific logic for mirror timers if (liquidData.Status != LIQUID_MAP_NO_WATER) { // Breath bar state (under water in any liquid type) if ((liquidData.Flags & MAP_ALL_LIQUIDS) != 0) { if ((liquidData.Status & LIQUID_MAP_UNDER_WATER) != 0) m_MirrorTimerFlags |= UNDERWATER_INWATER; else m_MirrorTimerFlags &= ~UNDERWATER_INWATER; } // Fatigue bar state (if not on flight path or transport) if ((liquidData.Flags & MAP_LIQUID_TYPE_DARK_WATER) && !IsInFlight() && !GetTransport()) { // Exclude also uncontrollable vehicles Vehicle* vehicle = GetVehicle(); VehicleSeatEntry const* vehicleSeat = vehicle ? vehicle->GetSeatForPassenger(this) : nullptr; if (!vehicleSeat || vehicleSeat->CanControl()) m_MirrorTimerFlags |= UNDERWATER_INDARKWATER; else m_MirrorTimerFlags &= ~UNDERWATER_INDARKWATER; } else m_MirrorTimerFlags &= ~UNDERWATER_INDARKWATER; // Lava state (any contact) if (liquidData.Flags & MAP_LIQUID_TYPE_MAGMA) { if (liquidData.Status & MAP_LIQUID_STATUS_IN_CONTACT) m_MirrorTimerFlags |= UNDERWATER_INLAVA; else m_MirrorTimerFlags &= ~UNDERWATER_INLAVA; } // Slime state (any contact) if (liquidData.Flags & MAP_LIQUID_TYPE_SLIME) { if (liquidData.Status & MAP_LIQUID_STATUS_IN_CONTACT) m_MirrorTimerFlags |= UNDERWATER_INSLIME; else m_MirrorTimerFlags &= ~UNDERWATER_INSLIME; } } else m_MirrorTimerFlags &= ~(UNDERWATER_INWATER | UNDERWATER_INLAVA | UNDERWATER_INSLIME | UNDERWATER_INDARKWATER); } uint32 Player::GetSpellQueueWindow() const { return sWorld->getIntConfig(CONFIG_SPELL_QUEUE_WINDOW); } bool Player::CanExecutePendingSpellCastRequest(SpellInfo const* spellInfo) { if (GetGlobalCooldownMgr().HasGlobalCooldown(spellInfo)) return false; if (GetSpellCooldownDelay(spellInfo->Id) > GetSpellQueueWindow()) return false; for (CurrentSpellTypes spellSlot : {CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL}) if (Spell* spell = GetCurrentSpell(spellSlot)) { bool autoshot = spell->m_spellInfo->IsAutoRepeatRangedSpell(); if (IsNonMeleeSpellCast(false, true, true, autoshot)) return false; } return true; } const PendingSpellCastRequest* Player::GetCastRequest(uint32 category) const { for (const PendingSpellCastRequest& request : SpellQueue) if (request.category == category) return &request; return nullptr; } bool Player::CanRequestSpellCast(SpellInfo const* spellInfo) { if (!sWorld->getBoolConfig(CONFIG_SPELL_QUEUE_ENABLED)) return false; // Check for existing cast request with the same category if (GetCastRequest(spellInfo->GetCategory())) return false; if (GetGlobalCooldownMgr().GetGlobalCooldown(spellInfo) > GetSpellQueueWindow()) return false; if (GetSpellCooldownDelay(spellInfo->Id) > GetSpellQueueWindow()) return false; // If there is an existing cast that will last longer than the allowable // spell queue window, then we can't request a new spell cast for (CurrentSpellTypes spellSlot : { CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL }) if (Spell* spell = GetCurrentSpell(spellSlot)) if (spell->GetCastTimeRemaining() > static_cast(GetSpellQueueWindow())) return false; return true; } void Player::ExecuteOrCancelSpellCastRequest(PendingSpellCastRequest* request, bool isCancel /* = false*/) { if (isCancel) request->cancelInProgress = true; if (WorldSession* session = GetSession()) { if (request->isItem) session->HandleUseItemOpcode(request->requestPacket); else session->HandleCastSpellOpcode(request->requestPacket); } } void Player::ProcessSpellQueue() { while (!SpellQueue.empty()) { PendingSpellCastRequest& request = SpellQueue.front(); // Peek at the first spell SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(request.spellId); if (!spellInfo) { LOG_ERROR("entities.player", "Player::ProcessSpellQueue: Invalid spell {}", request.spellId); SpellQueue.clear(); break; } if (CanExecutePendingSpellCastRequest(spellInfo)) { ExecuteOrCancelSpellCastRequest(&request); // ExecuteOrCancelSpellCastRequest() can lead to clearing the SpellQueue. // Example scenario: // Handling a spell → Dealing damage to yourself (e.g., spell_pri_vampiric_touch) → // Killing yourself → Player::setDeathState() → SpellQueue.clear(). // Calling std::deque::pop_front() on an empty deque results in undefined behavior, // so an additional check is added. if (!SpellQueue.empty()) SpellQueue.pop_front(); } else // If the first spell can't execute, stop processing break; } }