diff --git a/src/game/Object/PetMgr.cpp b/src/game/Object/PetMgr.cpp new file mode 100644 index 000000000..d3a479665 --- /dev/null +++ b/src/game/Object/PetMgr.cpp @@ -0,0 +1,105 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +#include "PetMgr.h" +#include "Player.h" +#include "Pet.h" +#include "Transports.h" +#include "WorldPacket.h" +#include "Opcodes.h" +#include "Log.h" + +void PetMgr::LoadStableSlotsFromField(uint32 raw) +{ + m_stableSlots = raw; + if (m_stableSlots > MAX_PET_STABLES) + { + sLog.outError("Player can have not more %u stable slots, but have in DB %u", MAX_PET_STABLES, uint32(m_stableSlots)); + m_stableSlots = MAX_PET_STABLES; + } +} + +void PetMgr::Remove(PetSaveMode mode) +{ + if (Pet* pet = m_owner->GetPet()) + { + pet->Unsummon(mode, m_owner); + } +} + +void PetMgr::RemoveActionBar() +{ + WorldPacket data(SMSG_PET_SPELLS, 8); + data << ObjectGuid(); + m_owner->SendDirectMessage(&data); +} + +void PetMgr::UnsummonTemporaryIfAny() +{ + Pet* pet = m_owner->GetPet(); + if (!pet) + { + return; + } + + if (!m_temporaryUnsummonedPetNumber && pet->isControlled() && !pet->isTemporarySummoned()) + { + m_temporaryUnsummonedPetNumber = pet->GetCharmInfo()->GetPetNumber(); + } + + if (Transport* petTransport = pet->GetTransport()) + { + petTransport->RemovePassenger(pet); + pet->SetTransport(nullptr); + } + + pet->Unsummon(PET_SAVE_AS_CURRENT, m_owner); +} + +void PetMgr::ResummonTemporaryUnsummonedIfAny() +{ + if (!m_temporaryUnsummonedPetNumber) + { + return; + } + + // not resummon in not appropriate state + if (m_owner->IsPetNeedBeTemporaryUnsummoned()) + { + return; + } + + if (m_owner->GetPetGuid()) + { + return; + } + + Pet* NewPet = new Pet; + if (!NewPet->LoadPetFromDB(m_owner, 0, m_temporaryUnsummonedPetNumber, true)) + { + delete NewPet; + } + + m_temporaryUnsummonedPetNumber = 0; +} diff --git a/src/game/Object/PetMgr.h b/src/game/Object/PetMgr.h new file mode 100644 index 000000000..098a81631 --- /dev/null +++ b/src/game/Object/PetMgr.h @@ -0,0 +1,96 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +#ifndef MANGOS_H_PETMGR +#define MANGOS_H_PETMGR + +#include "Common.h" +#include "Pet.h" // PetSaveMode enum + MAX_PET_STABLES constant + +class Player; + +/** + * PetMgr — owns a Player's pet-ownership metadata: the stable-slot + * count persisted with the character row and the temporary-unsummon + * tracking number used by transports / mounts to bring the same pet + * back after a short interruption. + * + * What this DOES NOT own: + * - The Pet creature itself (a Creature subclass living in the world, + * managed by Object/Pet.{cpp,h}). Its lifecycle, AI, spells, and + * persistence to the `character_pet` table all stay in Pet.cpp. + * - The pet stable opcode handlers (CMSG_STABLE_PET and friends in + * WorldHandlers/NPCHandler.cpp). They keep reading/updating the slot + * count through Player's public accessors, so the stable-purchase + * economics (gold cost via StableSlotPricesStore, MAX_PET_STABLES + * cap) are untouched. + */ +class PetMgr +{ + public: + explicit PetMgr(Player* owner) + : m_owner(owner), m_stableSlots(0), m_temporaryUnsummonedPetNumber(0) + { + } + + /// Number of paid stable slots the character has unlocked. + /// Persisted to `characters`.stable_slots; clamped to + /// MAX_PET_STABLES on load. + uint32 GetStableSlots() const { return m_stableSlots; } + void SetStableSlots(uint32 slots) { m_stableSlots = slots; } + + /// Called from Player::LoadFromDB with the raw column value. + /// Clamps to MAX_PET_STABLES and logs a server error on + /// out-of-range data so an operator can detect a tampered row. + void LoadStableSlotsFromField(uint32 raw); + + /// Pet number that was active before a temporary unsummon (e.g. + /// transport zone-in). Zero means no pending resummon. + uint32 GetTemporaryUnsummonedPetNumber() const { return m_temporaryUnsummonedPetNumber; } + void SetTemporaryUnsummonedPetNumber(uint32 petnumber) { m_temporaryUnsummonedPetNumber = petnumber; } + + /// If the owner currently has a controlled pet, asks it to + /// unsummon with the given mode (see PetSaveMode). + void Remove(PetSaveMode mode); + + /// SMSG_PET_SPELLS with an empty guid — clears the pet action + /// bar UI on the client. + void RemoveActionBar(); + + /// Stash the current non-temporary pet's number so we can bring + /// it back later, then unsummon with PET_SAVE_AS_CURRENT. + void UnsummonTemporaryIfAny(); + + /// Counterpart to UnsummonTemporaryIfAny: if we stashed a pet + /// number and it's now appropriate to resummon, load it from DB. + /// Clears the stash either way. + void ResummonTemporaryUnsummonedIfAny(); + + private: + Player* m_owner; + uint32 m_stableSlots; + uint32 m_temporaryUnsummonedPetNumber; +}; + +#endif // MANGOS_H_PETMGR diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index c252a717c..6493d6976 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -526,7 +526,7 @@ UpdateMask Player::updateVisualBits; * * @param session The owning world session. */ -Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_reputationMgr(this) +Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_reputationMgr(this), m_spellCooldownMgr(this), m_petMgr(this) { #ifdef ENABLE_PLAYERBOTS m_playerbotAI = 0; @@ -675,8 +675,7 @@ Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_ m_ammoDPSMin = 0.0f; m_ammoDPSMax = 0.0f; - // Initialize temporary unsummoned pet number to 0 - m_temporaryUnsummonedPetNumber = 0; + // Temporary-unsummoned pet number now initialized by m_petMgr's constructor. //////////////////// Rest System///////////////////// // Initialize time of entering inn to 0 @@ -709,8 +708,7 @@ Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_ m_forced_speed_changes[i] = 0; } - // Initialize stable slots to 0 - m_stableSlots = 0; + // Stable-slot count now initialized by m_petMgr's constructor. /////////////////// Instance System ///////////////////// // Initialize homebind timer to 0 @@ -3525,7 +3523,7 @@ void Player::SendInitialSpells() * * * * * * * * * * * * * * * * */ uint16 spellCount = 0; - WorldPacket data(SMSG_INITIAL_SPELLS, (1 + 2 + 4 * m_spells.size() + 2 + m_spellCooldowns.size() * (2 + 2 + 2 + 4 + 4))); + WorldPacket data(SMSG_INITIAL_SPELLS, (1 + 2 + 4 * m_spells.size() + 2 + GetSpellCooldownMap().size() * (2 + 2 + 2 + 4 + 4))); data << uint8(0); /** * * * * * * * * * * * * * * * * @@ -3561,9 +3559,9 @@ void Player::SendInitialSpells() data.put(countPos, spellCount); // write real count value /* For each spell the player has on cooldown */ - uint16 spellCooldowns = m_spellCooldowns.size(); + uint16 spellCooldowns = GetSpellCooldownMap().size(); data << uint16(spellCooldowns); - for (SpellCooldowns::const_iterator itr = m_spellCooldowns.begin(); itr != m_spellCooldowns.end(); ++itr) + for (SpellCooldowns::const_iterator itr = GetSpellCooldownMap().begin(); itr != GetSpellCooldownMap().end(); ++itr) { /* If the spell doesn't exist in the spellbook, just ignore it */ SpellEntry const* sEntry = sSpellStore.LookupEntry(itr->first); @@ -4400,145 +4398,6 @@ void Player::removeSpell(uint32 spell_id, bool disabled, bool learn_low_rank) } } -/** - * @brief Removes a cooldown entry for a specific spell. - * - * @param spell_id The spell identifier whose cooldown should be removed. - * @param update True to notify the client that the cooldown was cleared. - */ -void Player::RemoveSpellCooldown(uint32 spell_id, bool update /* = false */) -{ - m_spellCooldowns.erase(spell_id); - - if (update) - { - SendClearCooldown(spell_id, this); - } -} - -/** - * @brief Removes cooldowns for all spells in a cooldown category. - * - * @param cat The spell category identifier. - * @param update True to notify the client for cleared cooldowns. - */ -void Player::RemoveSpellCategoryCooldown(uint32 cat, bool update /* = false */) -{ - SpellCategoryStore::const_iterator ct = sSpellCategoryStore.find(cat); - if (ct == sSpellCategoryStore.end()) - { - return; - } - - const SpellCategorySet& ct_set = ct->second; - for (SpellCooldowns::const_iterator i = m_spellCooldowns.begin(); i != m_spellCooldowns.end();) - { - if (ct_set.find(i->first) != ct_set.end()) - { - RemoveSpellCooldown((i++)->first, update); - } - else - { - ++i; - } - } -} - -/** - * @brief Clears all tracked spell cooldowns for the player. - */ -void Player::RemoveAllSpellCooldown() -{ - if (!m_spellCooldowns.empty()) - { - for (SpellCooldowns::const_iterator itr = m_spellCooldowns.begin(); itr != m_spellCooldowns.end(); ++itr) - { - SendClearCooldown(itr->first, this); - } - - m_spellCooldowns.clear(); - } -} - -/** - * @brief Loads persisted spell cooldowns from the database query result. - * - * @param result The query result containing cooldown rows. - */ -void Player::_LoadSpellCooldowns(QueryResult* result) -{ - // some cooldowns can be already set at aura loading... - - // QueryResult *result = CharacterDatabase.PQuery("SELECT `spell`,`item`,`time` FROM `character_spell_cooldown` WHERE `guid` = '%u'",GetGUIDLow()); - - if (result) - { - time_t curTime = time(NULL); - - do - { - Field* fields = result->Fetch(); - - uint32 spell_id = fields[0].GetUInt32(); - uint32 item_id = fields[1].GetUInt32(); - time_t db_time = (time_t)fields[2].GetUInt64(); - - if (!sSpellStore.LookupEntry(spell_id)) - { - sLog.outError("Player %u has unknown spell %u in `character_spell_cooldown`, skipping.", GetGUIDLow(), spell_id); - continue; - } - - // skip outdated cooldown - if (db_time <= curTime) - { - continue; - } - - AddSpellCooldown(spell_id, item_id, db_time); - - DEBUG_LOG("Player (GUID: %u) spell %u, item %u cooldown loaded (%u secs).", GetGUIDLow(), spell_id, item_id, uint32(db_time - curTime)); - } - while (result->NextRow()); - - delete result; - } -} - -/** - * @brief Saves active spell cooldowns to the database. - */ -void Player::_SaveSpellCooldowns() -{ - static SqlStatementID deleteSpellCooldown ; - static SqlStatementID insertSpellCooldown ; - - SqlStatement stmt = CharacterDatabase.CreateStatement(deleteSpellCooldown, "DELETE FROM `character_spell_cooldown` WHERE `guid` = ?"); - stmt.PExecute(GetGUIDLow()); - - time_t curTime = time(NULL); - time_t infTime = curTime + infinityCooldownDelayCheck; - - // remove outdated and save active - for (SpellCooldowns::iterator itr = m_spellCooldowns.begin(); itr != m_spellCooldowns.end();) - { - if (itr->second.end <= curTime) - { - m_spellCooldowns.erase(itr++); - } - else if (itr->second.end <= infTime) // not save locked cooldowns, it will be reset or set at reload - { - stmt = CharacterDatabase.CreateStatement(insertSpellCooldown, "INSERT INTO `character_spell_cooldown` (`guid`,`spell`,`item`,`time`) VALUES( ?, ?, ?, ?)"); - stmt.PExecute(GetGUIDLow(), itr->first, itr->second.itemid, uint64(itr->second.end)); - ++itr; - } - else - { - ++itr; - } - } -} - /** * @brief Calculates the current cost to reset the player's talents. * @@ -18951,12 +18810,7 @@ bool Player::LoadFromDB(ObjectGuid guid, SqlQueryHolder* holder) uint32 extraflags = fields[31].GetUInt32(); - m_stableSlots = fields[32].GetUInt32(); - if (m_stableSlots > MAX_PET_STABLES) - { - sLog.outError("Player can have not more %u stable slots, but have in DB %u", MAX_PET_STABLES, uint32(m_stableSlots)); - m_stableSlots = MAX_PET_STABLES; - } + m_petMgr.LoadStableSlotsFromField(fields[32].GetUInt32()); m_atLoginFlags = fields[33].GetUInt32(); @@ -20668,7 +20522,7 @@ void Player::SaveToDB() uberInsert.addUInt32(m_ExtraFlags); - uberInsert.addUInt32(uint32(m_stableSlots)); // to prevent save uint8 as char + uberInsert.addUInt32(uint32(GetStableSlots())); // to prevent save uint8 as char uberInsert.addUInt32(uint32(m_atLoginFlags)); @@ -21720,19 +21574,6 @@ void Player::UpdateDuelFlag(time_t currTime) duel->opponent->duel->startTime = currTime; } -/** - * @brief Unsummons the player's current pet using the requested save mode. - * - * @param mode The persistence mode to use when removing the pet. - */ -void Player::RemovePet(PetSaveMode mode) -{ - if (Pet* pet = GetPet()) - { - pet->Unsummon(mode, this); - } -} - /** * @brief Unsummons the player's active mini-pet. */ @@ -22080,16 +21921,6 @@ void Player::CharmSpellInitialize() GetSession()->SendPacket(&data); } -/** - * @brief Clears the pet action bar on the client. - */ -void Player::RemovePetActionBar() -{ - WorldPacket data(SMSG_PET_SPELLS, 8); - data << ObjectGuid(); - SendDirectMessage(&data); -} - /** * @brief Checks whether a spell modifier currently applies to a spell. * @@ -23176,162 +23007,6 @@ void Player::UpdatePvP(bool state, bool ovrride) } } -/** - * @brief Applies personal and category cooldowns for a spell cast. - * - * @param spellInfo The spell entry that triggered the cooldown. - * @param itemId The casting item entry, if any. - * @param spell The active spell instance. - * @param infinityCooldown True to apply a long-lived cooldown marker. - */ -void Player::AddSpellAndCategoryCooldowns(SpellEntry const* spellInfo, uint32 itemId, Spell* spell, bool infinityCooldown) -{ - // init cooldown values - uint32 cat = 0; - int32 rec = -1; - int32 catrec = -1; - - // some special item spells without correct cooldown in SpellInfo - // cooldown information stored in item prototype - // This used in same way in WorldSession::HandleItemQuerySingleOpcode data sending to client. - - if (itemId) - { - if (ItemPrototype const* proto = ObjectMgr::GetItemPrototype(itemId)) - { - for (int idx = 0; idx < MAX_ITEM_PROTO_SPELLS; ++idx) - { - if (proto->Spells[idx].SpellId == spellInfo->Id) - { - cat = proto->Spells[idx].SpellCategory; - rec = proto->Spells[idx].SpellCooldown; - catrec = proto->Spells[idx].SpellCategoryCooldown; - break; - } - } - } - } - - // if no cooldown found above then base at DBC data - if (rec < 0 && catrec < 0) - { - cat = spellInfo->Category; - rec = spellInfo->RecoveryTime; - catrec = spellInfo->CategoryRecoveryTime; - } - - time_t curTime = time(NULL); - - time_t catrecTime; - time_t recTime; - - // overwrite time for selected category - if (infinityCooldown) - { - // use +MONTH as infinity mark for spell cooldown (will checked as MONTH/2 at save ans skipped) - // but not allow ignore until reset or re-login - catrecTime = catrec > 0 ? curTime + infinityCooldownDelay : 0; - recTime = rec > 0 ? curTime + infinityCooldownDelay : catrecTime; - } - else - { - // shoot spells used equipped item cooldown values already assigned in GetAttackTime(RANGED_ATTACK) - // prevent 0 cooldowns set by another way - if (rec <= 0 && catrec <= 0 && (cat == 76 || cat == 351)) - { - rec = GetAttackTime(RANGED_ATTACK); - } - - // Now we have cooldown data (if found any), time to apply mods - if (rec > 0) - { - ApplySpellMod(spellInfo->Id, SPELLMOD_COOLDOWN, rec, spell); - } - - if (catrec > 0) - { - ApplySpellMod(spellInfo->Id, SPELLMOD_COOLDOWN, catrec, spell); - } - - // replace negative cooldowns by 0 - if (rec < 0) - { - rec = 0; - } - if (catrec < 0) - { - catrec = 0; - } - - // no cooldown after applying spell mods - if (rec == 0 && catrec == 0) - { - return; - } - - catrecTime = catrec ? curTime + catrec / IN_MILLISECONDS : 0; - recTime = rec ? curTime + rec / IN_MILLISECONDS : catrecTime; - } - - // self spell cooldown - if (recTime > 0) - { - AddSpellCooldown(spellInfo->Id, itemId, recTime); - } - - // category spells - if (cat && catrec > 0) - { - SpellCategoryStore::const_iterator i_scstore = sSpellCategoryStore.find(cat); - if (i_scstore != sSpellCategoryStore.end()) - { - for (SpellCategorySet::const_iterator i_scset = i_scstore->second.begin(); i_scset != i_scstore->second.end(); ++i_scset) - { - if (*i_scset == spellInfo->Id) // skip main spell, already handled above - { - continue; - } - - AddSpellCooldown(*i_scset, itemId, catrecTime); - } - } - } -} - -/** - * @brief Stores a cooldown entry for a spell. - * - * @param spellid The spell identifier. - * @param itemid The associated item identifier, if any. - * @param end_time The server time when the cooldown ends. - */ -void Player::AddSpellCooldown(uint32 spellid, uint32 itemid, time_t end_time) -{ - SpellCooldown sc; - sc.end = end_time; - sc.itemid = itemid; - m_spellCooldowns[spellid] = sc; -} - -/** - * @brief Applies cooldowns and notifies the client about a spell cooldown event. - * - * @param spellInfo The spell entry that triggered the cooldown. - * @param itemId The associated item entry, if any. - * @param spell The active spell instance. - */ -void Player::SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId, Spell* spell) -{ - // start cooldowns at server side, if any - AddSpellAndCategoryCooldowns(spellInfo, itemId, spell); - - // Send activate cooldown timer (possible 0) at client side - WorldPacket data(SMSG_COOLDOWN_EVENT, (4 + 8)); - data << uint32(spellInfo->Id); - data << GetObjectGuid(); - SendDirectMessage(&data); -} - /** * @brief Stores the location used to return the player after leaving a battleground. * @@ -24018,8 +23693,8 @@ void Player::ApplyEquipCooldown(Item* pItem) } //! Don't replace longer cooldowns by equip cooldown if we have any. - SpellCooldowns::iterator itr = m_spellCooldowns.find(spellData.SpellId); - if (itr != m_spellCooldowns.end() && itr->second.itemid == pItem->GetEntry() && itr->second.end > time(NULL) + 30) + SpellCooldowns::const_iterator itr = GetSpellCooldownMap().find(spellData.SpellId); + if (itr != GetSpellCooldownMap().end() && itr->second.itemid == pItem->GetEntry() && itr->second.end > time(NULL) + 30) { break; } @@ -26077,61 +25752,6 @@ void Player::UpdateFallInformationIfNeed(MovementInfo const& minfo, uint16 opcod } } -/** - * @brief Temporarily unsummons the current pet when the player's state requires it. - */ -void Player::UnsummonPetTemporaryIfAny() -{ - Pet* pet = GetPet(); - if (!pet) - { - return; - } - - if (!m_temporaryUnsummonedPetNumber && pet->isControlled() && !pet->isTemporarySummoned()) - { - m_temporaryUnsummonedPetNumber = pet->GetCharmInfo()->GetPetNumber(); - } - - if (Transport* petTransport = pet->GetTransport()) - { - petTransport->RemovePassenger(pet); - pet->SetTransport(nullptr); - } - - pet->Unsummon(PET_SAVE_AS_CURRENT, this); -} - -/** - * @brief Resummons a pet that was temporarily unsummoned earlier. - */ -void Player::ResummonPetTemporaryUnSummonedIfAny() -{ - if (!m_temporaryUnsummonedPetNumber) - { - return; - } - - // not resummon in not appropriate state - if (IsPetNeedBeTemporaryUnsummoned()) - { - return; - } - - if (GetPetGuid()) - { - return; - } - - Pet* NewPet = new Pet; - if (!NewPet->LoadPetFromDB(this, 0, m_temporaryUnsummonedPetNumber, true)) - { - delete NewPet; - } - - m_temporaryUnsummonedPetNumber = 0; -} - /** * @brief Checks whether the player can currently see spell-click interaction on a creature. * diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index c0eda2ee1..55dc8b44b 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -66,6 +66,8 @@ #include "MapReference.h" #include "Util.h" // for Tokens typedef #include "ReputationMgr.h" +#include "SpellCooldownMgr.h" // held by value on Player; brings in SpellCooldown struct + owns the cooldown map +#include "PetMgr.h" // held by value on Player; owns stable-slot count + temp-unsummon pet number #include "BattleGround.h" #include "DBCStores.h" #include "SharedDefines.h" @@ -241,16 +243,7 @@ struct SpellModifier typedef std::list SpellModList; -/** - * @brief Structure to hold spell cooldown information - */ -struct SpellCooldown -{ - time_t end; ///< End time of the cooldown - uint16 itemid; ///< Item ID associated with the cooldown -}; - -typedef std::map SpellCooldowns; +// SpellCooldown struct and SpellCooldowns typedef moved to SpellCooldownMgr.h. /** * @brief Trainer spell state enumeration @@ -1374,7 +1367,7 @@ class Player : public Unit } // Remove the player's pet - void RemovePet(PetSaveMode mode); + void RemovePet(PetSaveMode mode) { m_petMgr.Remove(mode); } // Remove the player's mini pet void RemoveMiniPet(); @@ -1764,7 +1757,7 @@ class Player : public Unit // Load the player's pet void LoadPet(); - uint32 m_stableSlots; // Number of stable slots + // Stable-slot count now owned by m_petMgr (see GetStableSlots/SetStableSlots). /*********************************************************/ /*** GOSSIP SYSTEM ***/ @@ -2242,7 +2235,7 @@ class Player : public Unit void PossessSpellInitialize(); // Remove the pet action bar - void RemovePetActionBar(); + void RemovePetActionBar() { m_petMgr.RemoveActionBar(); } // Check if the player has a specific spell bool HasSpell(uint32 spell) const override; @@ -2346,10 +2339,7 @@ class Player : public Unit } // Get the player's spell cooldown map - SpellCooldowns const& GetSpellCooldownMap() const - { - return m_spellCooldowns; - } + SpellCooldowns const& GetSpellCooldownMap() const { return m_spellCooldownMgr.GetSpellCooldownMap(); } // Add a spell modifier to the player void AddSpellMod(SpellModifier* mod, bool apply); @@ -2372,38 +2362,23 @@ class Player : public Unit static uint32 const infinityCooldownDelay = MONTH; // used for set "infinity cooldowns" for spells and check static uint32 const infinityCooldownDelayCheck = MONTH / 2; - // Check if the player has a spell cooldown - bool HasSpellCooldown(uint32 spell_id) const - { - SpellCooldowns::const_iterator itr = m_spellCooldowns.find(spell_id); - return itr != m_spellCooldowns.end() && itr->second.end > time(NULL); - } + // Spell-cooldown API — thin delegating wrappers around m_spellCooldownMgr. + bool HasSpellCooldown(uint32 spell_id) const { return m_spellCooldownMgr.HasSpellCooldown(spell_id); } - // Get the delay for a spell cooldown - time_t GetSpellCooldownDelay(uint32 spell_id) const - { - SpellCooldowns::const_iterator itr = m_spellCooldowns.find(spell_id); - time_t t = time(NULL); - return itr != m_spellCooldowns.end() && itr->second.end > t ? itr->second.end - t : 0; - } + time_t GetSpellCooldownDelay(uint32 spell_id) const { return m_spellCooldownMgr.GetSpellCooldownDelay(spell_id); } - // Add spell and category cooldowns - void AddSpellAndCategoryCooldowns(SpellEntry const* spellInfo, uint32 itemId, Spell* spell = NULL, bool infinityCooldown = false); + void AddSpellAndCategoryCooldowns(SpellEntry const* spellInfo, uint32 itemId, Spell* spell = NULL, bool infinityCooldown = false) { m_spellCooldownMgr.AddSpellAndCategoryCooldowns(spellInfo, itemId, spell, infinityCooldown); } - // Add a spell cooldown - void AddSpellCooldown(uint32 spell_id, uint32 itemid, time_t end_time); + void AddSpellCooldown(uint32 spell_id, uint32 itemid, time_t end_time) { m_spellCooldownMgr.AddSpellCooldown(spell_id, itemid, end_time); } - // Send a cooldown event to the client - void SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId = 0, Spell* spell = NULL); + void SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId = 0, Spell* spell = NULL) { m_spellCooldownMgr.SendCooldownEvent(spellInfo, itemId, spell); } // Prohibit a spell school for a specific duration void ProhibitSpellSchool(SpellSchoolMask idSchoolMask, uint32 unTimeMs) override; - // Remove a spell cooldown - void RemoveSpellCooldown(uint32 spell_id, bool update = false); + void RemoveSpellCooldown(uint32 spell_id, bool update = false) { m_spellCooldownMgr.RemoveSpellCooldown(spell_id, update); } - // Remove a spell category cooldown - void RemoveSpellCategoryCooldown(uint32 cat, bool update = false); + void RemoveSpellCategoryCooldown(uint32 cat, bool update = false) { m_spellCooldownMgr.RemoveSpellCategoryCooldown(cat, update); } // Send a clear cooldown message to the client void SendClearCooldown(uint32 spell_id, Unit* target); @@ -2414,13 +2389,11 @@ class Player : public Unit return m_GlobalCooldownMgr; } - void RemoveAllSpellCooldown(); + void RemoveAllSpellCooldown() { m_spellCooldownMgr.RemoveAllSpellCooldown(); } - // Load spell cooldowns from the database - void _LoadSpellCooldowns(QueryResult* result); + void _LoadSpellCooldowns(QueryResult* result) { m_spellCooldownMgr.LoadFromDB(result); } - // Save spell cooldowns to the database - void _SaveSpellCooldowns(); + void _SaveSpellCooldowns() { m_spellCooldownMgr.SaveToDB(); } // Set resurrect request data void setResurrectRequestData(ObjectGuid guid, uint32 mapId, float X, float Y, float Z, uint32 health, uint32 mana) @@ -3552,11 +3525,13 @@ class Player : public Unit // Remove an at-login flag for the player void RemoveAtLoginFlag(AtLoginFlags f, bool in_db_also = false); - // Temporarily removed pet cache - uint32 GetTemporaryUnsummonedPetNumber() const { return m_temporaryUnsummonedPetNumber; } - void SetTemporaryUnsummonedPetNumber(uint32 petnumber) { m_temporaryUnsummonedPetNumber = petnumber; } - void UnsummonPetTemporaryIfAny(); - void ResummonPetTemporaryUnSummonedIfAny(); + // Pet-metadata API — thin delegating wrappers around m_petMgr. + uint32 GetStableSlots() const { return m_petMgr.GetStableSlots(); } + void SetStableSlots(uint32 slots) { m_petMgr.SetStableSlots(slots); } + uint32 GetTemporaryUnsummonedPetNumber() const { return m_petMgr.GetTemporaryUnsummonedPetNumber(); } + void SetTemporaryUnsummonedPetNumber(uint32 petnumber) { m_petMgr.SetTemporaryUnsummonedPetNumber(petnumber); } + void UnsummonPetTemporaryIfAny() { m_petMgr.UnsummonTemporaryIfAny(); } + void ResummonPetTemporaryUnSummonedIfAny() { m_petMgr.ResummonTemporaryUnsummonedIfAny(); } bool IsPetNeedBeTemporaryUnsummoned() const { return !IsInWorld() || !IsAlive() || IsMounted() /*+in flight*/; } // Send cinematic start to the client @@ -3898,7 +3873,7 @@ class Player : public Unit PlayerMails m_mail; // Player mails PlayerSpellMap m_spells; // Player spells - SpellCooldowns m_spellCooldowns; // Spell cooldowns + // Spell-cooldown map now owned by m_spellCooldownMgr. GlobalCooldownMgr m_GlobalCooldownMgr; // Global cooldown manager @@ -4099,11 +4074,16 @@ class Player : public Unit // Detect invisibility timer uint32 m_DetectInvTimer; - // Temporary removed pet cache - uint32 m_temporaryUnsummonedPetNumber; + // Temporary-removed pet cache and stable-slot count now owned by m_petMgr. // Reputation manager for the player ReputationMgr m_reputationMgr; + + // Owns the spell-cooldown map + load/save/apply lifecycle + SpellCooldownMgr m_spellCooldownMgr; + + // Owns stable-slot count + temp-unsummon pet number + PetMgr m_petMgr; }; /** diff --git a/src/game/Object/SpellCooldownMgr.cpp b/src/game/Object/SpellCooldownMgr.cpp new file mode 100644 index 000000000..0a57814c9 --- /dev/null +++ b/src/game/Object/SpellCooldownMgr.cpp @@ -0,0 +1,286 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +#include "SpellCooldownMgr.h" +#include "Player.h" +#include "Log.h" +#include "Opcodes.h" +#include "SpellMgr.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "ObjectMgr.h" +#include "Spell.h" +#include "DBCStores.h" +#include "Database/DatabaseEnv.h" +#include "Database/DatabaseImpl.h" + +void SpellCooldownMgr::AddSpellAndCategoryCooldowns(SpellEntry const* spellInfo, uint32 itemId, Spell* spell, bool infinityCooldown) +{ + // init cooldown values + uint32 cat = 0; + int32 rec = -1; + int32 catrec = -1; + + // some special item spells without correct cooldown in SpellInfo + // cooldown information stored in item prototype + // This used in same way in WorldSession::HandleItemQuerySingleOpcode data sending to client. + + if (itemId) + { + if (ItemPrototype const* proto = ObjectMgr::GetItemPrototype(itemId)) + { + for (int idx = 0; idx < MAX_ITEM_PROTO_SPELLS; ++idx) + { + if (proto->Spells[idx].SpellId == spellInfo->Id) + { + cat = proto->Spells[idx].SpellCategory; + rec = proto->Spells[idx].SpellCooldown; + catrec = proto->Spells[idx].SpellCategoryCooldown; + break; + } + } + } + } + + // if no cooldown found above then base at DBC data + if (rec < 0 && catrec < 0) + { + cat = spellInfo->Category; + rec = spellInfo->RecoveryTime; + catrec = spellInfo->CategoryRecoveryTime; + } + + time_t curTime = time(NULL); + + time_t catrecTime; + time_t recTime; + + // overwrite time for selected category + if (infinityCooldown) + { + // use +MONTH as infinity mark for spell cooldown (will checked as MONTH/2 at save ans skipped) + // but not allow ignore until reset or re-login + catrecTime = catrec > 0 ? curTime + Player::infinityCooldownDelay : 0; + recTime = rec > 0 ? curTime + Player::infinityCooldownDelay : catrecTime; + } + else + { + // shoot spells used equipped item cooldown values already assigned in GetAttackTime(RANGED_ATTACK) + // prevent 0 cooldowns set by another way + if (rec <= 0 && catrec <= 0 && (cat == 76 || cat == 351)) + { + rec = m_owner->GetAttackTime(RANGED_ATTACK); + } + + // Now we have cooldown data (if found any), time to apply mods + if (rec > 0) + { + m_owner->ApplySpellMod(spellInfo->Id, SPELLMOD_COOLDOWN, rec, spell); + } + + if (catrec > 0) + { + m_owner->ApplySpellMod(spellInfo->Id, SPELLMOD_COOLDOWN, catrec, spell); + } + + // replace negative cooldowns by 0 + if (rec < 0) + { + rec = 0; + } + if (catrec < 0) + { + catrec = 0; + } + + // no cooldown after applying spell mods + if (rec == 0 && catrec == 0) + { + return; + } + + catrecTime = catrec ? curTime + catrec / IN_MILLISECONDS : 0; + recTime = rec ? curTime + rec / IN_MILLISECONDS : catrecTime; + } + + // self spell cooldown + if (recTime > 0) + { + AddSpellCooldown(spellInfo->Id, itemId, recTime); + } + + // category spells + if (cat && catrec > 0) + { + SpellCategoryStore::const_iterator i_scstore = sSpellCategoryStore.find(cat); + if (i_scstore != sSpellCategoryStore.end()) + { + for (SpellCategorySet::const_iterator i_scset = i_scstore->second.begin(); i_scset != i_scstore->second.end(); ++i_scset) + { + if (*i_scset == spellInfo->Id) // skip main spell, already handled above + { + continue; + } + + AddSpellCooldown(*i_scset, itemId, catrecTime); + } + } + } +} + +void SpellCooldownMgr::AddSpellCooldown(uint32 spellid, uint32 itemid, time_t end_time) +{ + SpellCooldown sc; + sc.end = end_time; + sc.itemid = itemid; + m_cooldowns[spellid] = sc; +} + +void SpellCooldownMgr::SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId, Spell* spell) +{ + // start cooldowns at server side, if any + AddSpellAndCategoryCooldowns(spellInfo, itemId, spell); + + // Send activate cooldown timer (possible 0) at client side + WorldPacket data(SMSG_COOLDOWN_EVENT, (4 + 8)); + data << uint32(spellInfo->Id); + data << m_owner->GetObjectGuid(); + m_owner->SendDirectMessage(&data); +} + +void SpellCooldownMgr::RemoveSpellCooldown(uint32 spell_id, bool update /* = false */) +{ + m_cooldowns.erase(spell_id); + + if (update) + { + m_owner->SendClearCooldown(spell_id, m_owner); + } +} + +void SpellCooldownMgr::RemoveSpellCategoryCooldown(uint32 cat, bool update /* = false */) +{ + SpellCategoryStore::const_iterator ct = sSpellCategoryStore.find(cat); + if (ct == sSpellCategoryStore.end()) + { + return; + } + + const SpellCategorySet& ct_set = ct->second; + for (SpellCooldowns::const_iterator i = m_cooldowns.begin(); i != m_cooldowns.end();) + { + if (ct_set.find(i->first) != ct_set.end()) + { + RemoveSpellCooldown((i++)->first, update); + } + else + { + ++i; + } + } +} + +void SpellCooldownMgr::RemoveAllSpellCooldown() +{ + if (!m_cooldowns.empty()) + { + for (SpellCooldowns::const_iterator itr = m_cooldowns.begin(); itr != m_cooldowns.end(); ++itr) + { + m_owner->SendClearCooldown(itr->first, m_owner); + } + + m_cooldowns.clear(); + } +} + +void SpellCooldownMgr::LoadFromDB(QueryResult* result) +{ + // some cooldowns can be already set at aura loading... + + // QueryResult *result = CharacterDatabase.PQuery("SELECT `spell`,`item`,`time` FROM `character_spell_cooldown` WHERE `guid` = '%u'",GetGUIDLow()); + + if (result) + { + time_t curTime = time(NULL); + + do + { + Field* fields = result->Fetch(); + + uint32 spell_id = fields[0].GetUInt32(); + uint32 item_id = fields[1].GetUInt32(); + time_t db_time = (time_t)fields[2].GetUInt64(); + + if (!sSpellStore.LookupEntry(spell_id)) + { + sLog.outError("Player %u has unknown spell %u in `character_spell_cooldown`, skipping.", m_owner->GetGUIDLow(), spell_id); + continue; + } + + // skip outdated cooldown + if (db_time <= curTime) + { + continue; + } + + AddSpellCooldown(spell_id, item_id, db_time); + + DEBUG_LOG("Player (GUID: %u) spell %u, item %u cooldown loaded (%u secs).", m_owner->GetGUIDLow(), spell_id, item_id, uint32(db_time - curTime)); + } + while (result->NextRow()); + + delete result; + } +} + +void SpellCooldownMgr::SaveToDB() +{ + static SqlStatementID deleteSpellCooldown ; + static SqlStatementID insertSpellCooldown ; + + SqlStatement stmt = CharacterDatabase.CreateStatement(deleteSpellCooldown, "DELETE FROM `character_spell_cooldown` WHERE `guid` = ?"); + stmt.PExecute(m_owner->GetGUIDLow()); + + time_t curTime = time(NULL); + time_t infTime = curTime + Player::infinityCooldownDelayCheck; + + // remove outdated and save active + for (SpellCooldowns::iterator itr = m_cooldowns.begin(); itr != m_cooldowns.end();) + { + if (itr->second.end <= curTime) + { + m_cooldowns.erase(itr++); + } + else if (itr->second.end <= infTime) // not save locked cooldowns, it will be reset or set at reload + { + stmt = CharacterDatabase.CreateStatement(insertSpellCooldown, "INSERT INTO `character_spell_cooldown` (`guid`,`spell`,`item`,`time`) VALUES( ?, ?, ?, ?)"); + stmt.PExecute(m_owner->GetGUIDLow(), itr->first, itr->second.itemid, uint64(itr->second.end)); + ++itr; + } + else + { + ++itr; + } + } +} diff --git a/src/game/Object/SpellCooldownMgr.h b/src/game/Object/SpellCooldownMgr.h new file mode 100644 index 000000000..f11799fb1 --- /dev/null +++ b/src/game/Object/SpellCooldownMgr.h @@ -0,0 +1,87 @@ +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +#ifndef MANGOS_H_SPELLCOOLDOWNMGR +#define MANGOS_H_SPELLCOOLDOWNMGR + +#include "Common.h" +#include + +class Player; +class Spell; +class QueryResult; +struct SpellEntry; + +/** + * @brief Structure to hold spell cooldown information + */ +struct SpellCooldown +{ + time_t end; ///< End time of the cooldown + uint16 itemid; ///< Item ID associated with the cooldown +}; + +typedef std::map SpellCooldowns; + +/** + * @brief Owns a player's active spell-cooldown map and the operations on it. + * + * Held by value on Player as m_spellCooldownMgr with a Player* back-pointer. + * Persisted to character_spell_cooldown via LoadFromDB()/SaveToDB(). + */ +class SpellCooldownMgr +{ + public: + explicit SpellCooldownMgr(Player* owner) : m_owner(owner) {} + + SpellCooldowns const& GetSpellCooldownMap() const { return m_cooldowns; } + + bool HasSpellCooldown(uint32 spell_id) const + { + SpellCooldowns::const_iterator itr = m_cooldowns.find(spell_id); + return itr != m_cooldowns.end() && itr->second.end > time(NULL); + } + + time_t GetSpellCooldownDelay(uint32 spell_id) const + { + SpellCooldowns::const_iterator itr = m_cooldowns.find(spell_id); + time_t t = time(NULL); + return itr != m_cooldowns.end() && itr->second.end > t ? itr->second.end - t : 0; + } + + void AddSpellAndCategoryCooldowns(SpellEntry const* spellInfo, uint32 itemId, Spell* spell = NULL, bool infinityCooldown = false); + void AddSpellCooldown(uint32 spell_id, uint32 itemid, time_t end_time); + void SendCooldownEvent(SpellEntry const* spellInfo, uint32 itemId = 0, Spell* spell = NULL); + void RemoveSpellCooldown(uint32 spell_id, bool update = false); + void RemoveSpellCategoryCooldown(uint32 cat, bool update = false); + void RemoveAllSpellCooldown(); + void LoadFromDB(QueryResult* result); + void SaveToDB(); + + private: + Player* m_owner; ///< Non-owning pointer to the owning Player. + SpellCooldowns m_cooldowns; ///< Active spell cooldowns keyed by spell id. +}; + +#endif // MANGOS_H_SPELLCOOLDOWNMGR diff --git a/src/game/WorldHandlers/NPCHandler.cpp b/src/game/WorldHandlers/NPCHandler.cpp index 749e9afd4..2e1d56b80 100644 --- a/src/game/WorldHandlers/NPCHandler.cpp +++ b/src/game/WorldHandlers/NPCHandler.cpp @@ -779,7 +779,7 @@ void WorldSession::SendStablePet(ObjectGuid guid) size_t wpos = data.wpos(); data << uint8(0); // place holder for slot show number - data << uint8(GetPlayer()->m_stableSlots); + data << uint8(GetPlayer()->GetStableSlots()); uint8 num = 0; // counter for place holder @@ -931,7 +931,7 @@ void WorldSession::HandleStablePet(WorldPacket& recv_data) delete result; } - if (free_slot > 0 && free_slot <= GetPlayer()->m_stableSlots) + if (free_slot > 0 && free_slot <= GetPlayer()->GetStableSlots()) { pet->Unsummon(PetSaveMode(free_slot), _player); SendStableResult(STABLE_SUCCESS_STABLE); @@ -1042,12 +1042,12 @@ void WorldSession::HandleBuyStableSlot(WorldPacket& recv_data) GetPlayer()->RemoveSpellsCausingAura(SPELL_AURA_FEIGN_DEATH); } - if (GetPlayer()->m_stableSlots < MAX_PET_STABLES) + if (GetPlayer()->GetStableSlots() < MAX_PET_STABLES) { - StableSlotPricesEntry const* SlotPrice = sStableSlotPricesStore.LookupEntry(GetPlayer()->m_stableSlots + 1); + StableSlotPricesEntry const* SlotPrice = sStableSlotPricesStore.LookupEntry(GetPlayer()->GetStableSlots() + 1); if (_player->GetMoney() >= SlotPrice->Price) { - ++GetPlayer()->m_stableSlots; + GetPlayer()->SetStableSlots(GetPlayer()->GetStableSlots() + 1); _player->ModifyMoney(-int32(SlotPrice->Price)); SendStableResult(STABLE_SUCCESS_BUY_SLOT); }