From 9422e1e7184248fb3889dca2c1d319c6adaec4a1 Mon Sep 17 00:00:00 2001 From: Ovahlord Date: Fri, 29 Sep 2023 21:51:14 +0200 Subject: [PATCH] Core/Pets: updated hunter pet summoning and resurrection mechanics to match sniff and retail behavior --- .../Creature/TemporarySummon/NewPet.cpp | 13 +-- .../Creature/TemporarySummon/NewPet.h | 6 +- src/server/game/Entities/Player/Player.cpp | 13 ++- src/server/game/Entities/Player/Player.h | 5 +- src/server/game/Entities/Unit/Unit.cpp | 3 + src/server/game/Handlers/PetHandler.cpp | 1 + src/server/game/Spells/SpellEffects.cpp | 96 +++++++++++++++---- 7 files changed, 98 insertions(+), 39 deletions(-) diff --git a/src/server/game/Entities/Creature/TemporarySummon/NewPet.cpp b/src/server/game/Entities/Creature/TemporarySummon/NewPet.cpp index 3435b1e6c4..0ef6d0f138 100644 --- a/src/server/game/Entities/Creature/TemporarySummon/NewPet.cpp +++ b/src/server/game/Entities/Creature/TemporarySummon/NewPet.cpp @@ -124,27 +124,18 @@ bool NewPet::CanBeDismissed() const void NewPet::Dismiss(bool permanent /*= true*/) { - printf("NewPet::Dismiss\n"); if (IsClassPet() || _playerPetDataKey.has_value()) { - Unit* summoner = GetInternalSummoner(); + Unit const* summoner = GetInternalSummoner(); if (summoner && summoner->IsPlayer()) { - Player* player = summoner->ToPlayer(); + Player const* player = summoner->ToPlayer(); // Update the pet data before unsummoning the pet if (_playerPetDataKey.has_value()) - { UpdatePlayerPetData(player->GetPlayerPetData(_playerPetDataKey->first, _playerPetDataKey->second)); - printf("set data\n"); - } if (IsClassPet()) { - if (!permanent) - player->SetActiveClassPetDataKey(_playerPetDataKey); - else - player->SetActiveClassPetDataKey(std::nullopt); - // Warlock pets do play dismiss sounds when releasing them permanently if (permanent && player->getClass() == CLASS_WARLOCK) { diff --git a/src/server/game/Entities/Creature/TemporarySummon/NewPet.h b/src/server/game/Entities/Creature/TemporarySummon/NewPet.h index 0daa14362b..1fbbfdc9ad 100644 --- a/src/server/game/Entities/Creature/TemporarySummon/NewPet.h +++ b/src/server/game/Entities/Creature/TemporarySummon/NewPet.h @@ -52,13 +52,13 @@ class TC_GAME_API NewPet final : public NewGuardian bool IsClassPet() const { return _isClassPet; } // Returns true if the pet is a hunter class pet. This is the case when the pet has player pet data and is stored under creatureId = 0 bool IsHunterPet() const { return _playerPetDataKey.has_value() && _playerPetDataKey->second == 0; } - // Returns true if the pet has a player pet data entry + // Returns true if the pet has a player pet data key to acces a player's pet data bool HasPlayerPetDataKey() { return _playerPetDataKey.has_value(); } // Returns the player pet data map key for the element that is stored in summoner's player class - Optional const& GetPlayerPetDataKey() { return _playerPetDataKey; } + Optional const& GetPlayerPetDataKey() const { return _playerPetDataKey; } // Returns true if the summoner is allowed to dismiss the pet via pet action bool CanBeDismissed() const; - // Unsummons the pet. If permanent is set to false, the pet will be re-summoned after the player has performed certain actions (dismounting etc) + // Unsummons the pet. If permanent is set to true, some pets will play a dismiss sound (such as Warlock pets) void Dismiss(bool permanent = true); // Overriden method of TemporarySummon::Unsummon to ensure that Pet::Dismiss is always called. void Unsummon(Milliseconds timeUntilDespawn = 0ms) override; diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index b053a12141..6a705e99d0 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -17744,7 +17744,7 @@ void Player::_LoadMail(PreparedQueryResult mailsResult, PreparedQueryResult mail void Player::_LoadPets(PreparedQueryResult result) { - // "SELECT PetNumber, CreatureId, TamedCreatureId, DisplayId, SavedHealth, SavedPower, CreatedBySpellId, LastSaveTime, ReactState, Slot, HasBeenRenamed, IsActive, `Name`, ActionBar FROM character_pet WHERE Guid = ? ORDER BY Slot" + // "SELECT PetNumber, CreatureId, TamedCreatureId, DisplayId, SavedHealth, SavedPower, CreatedBySpellId, LastSaveTime, ReactState, Slot, HasBeenRenamed, IsActive, `Name`, ActionBar, Talents FROM character_pet WHERE Guid = ? ORDER BY Slot" if (!result) return; @@ -17771,7 +17771,12 @@ void Player::_LoadPets(PreparedQueryResult result) petData->Talents = fields[14].GetString(); petData->Status = PlayerPetDataStatus::UpToDate; - _playerPetDataMap.emplace(std::make_pair(petData->Slot, petData->CreatureId), std::move(petData)); + std::pair key = std::make_pair(petData->Slot, petData->CreatureId); + + if (petData->IsActive) + SetActiveClassPetDataKey(key); + + _playerPetDataMap.emplace(key, std::move(petData)); } while (result->NextRow()); } @@ -17857,6 +17862,7 @@ void Player::AbandonPet() pet->Unsummon(); _deletedPlayerPetDataSet.insert(itr->second->PetNumber); _playerPetDataMap.erase(itr); + _activeClassPetDataKey.reset(); } void Player::_LoadQuestStatus(PreparedQueryResult result) @@ -19812,11 +19818,12 @@ void Player::_SavePets(CharacterDatabaseTransaction& trans) // Insert new pets and update existing pet data for (auto const& pair : _playerPetDataMap) { - bool isActive = _activeClassPetDataKey.has_value() && _activeClassPetDataKey->first == pair.first.first && _activeClassPetDataKey->second == pair.first.second; PlayerPetData* petData = pair.second.get(); if (petData->Status == PlayerPetDataStatus::UpToDate) continue; + bool isActive = _activeClassPetDataKey.has_value() && _activeClassPetDataKey->first == pair.first.first && _activeClassPetDataKey->second == pair.first.second; + if (petData->Status == PlayerPetDataStatus::New) { // INSERT INTO character_pet (Guid, PetNumber, CreatureId, TamedCreatureId, DisplayId, SavedHealth, SavedPower, CreatedBySpellId, LastSaveTime, ReactState, Slot, HasBeenRenamed, IsActive, `Name`, ActionBar, Talents) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 1d7924cd4b..8d0259f743 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -2393,10 +2393,13 @@ class TC_GAME_API Player : public Unit, public GridObject PlayerPetDataMap const& GetPlayerPetDataMap() const { return _playerPetDataMap; } // Unsummons the currently active pet and removes the player pet data from its container. The database data will be erased on the next save cycle. void AbandonPet(); + // Sets the pet data key for the class pet that will be un- and resummoned by several actions void SetActiveClassPetDataKey(Optional const& key) { _activeClassPetDataKey = key; } - + // Returns a reference to the class pet player pet data key + Optional const& GetActiveClassPetDataKey() const { return _activeClassPetDataKey; } private: Optional _activeClassPetDataKey; + protected: // Gamemaster whisper whitelist GuidList WhisperList; diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index c14aaaf5e3..7c471264be 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -5587,6 +5587,9 @@ void Unit::SetActivelyControlledSummon(NewPet* pet, bool apply) player->ApplyModByteFlag(PLAYER_FIELD_BYTES, PLAYER_FIELD_BYTES_OFFSET_FLAGS, PLAYER_FIELD_BYTE_HIDE_PET_BAR, (apply && !canControlPet)); player->SendPetSpellsMessage(pet, !apply); + + if (pet->IsClassPet() && apply) + player->SetActiveClassPetDataKey(pet->GetPlayerPetDataKey()); } } diff --git a/src/server/game/Handlers/PetHandler.cpp b/src/server/game/Handlers/PetHandler.cpp index caf54ed6f4..eeb7f95a83 100644 --- a/src/server/game/Handlers/PetHandler.cpp +++ b/src/server/game/Handlers/PetHandler.cpp @@ -109,6 +109,7 @@ void HandlePetActionHelper(NewPet* pet, Player* owner, ObjectGuid targetGuid, ui case COMMAND_ABANDON: // Despite its enum name, abandoning pets is done via extra Opcode. This here is merely unsummoning pets pet->Dismiss(); + owner->SetActiveClassPetDataKey(std::nullopt); break; case COMMAND_MOVE_TO: if (pet->IsAlive()) diff --git a/src/server/game/Spells/SpellEffects.cpp b/src/server/game/Spells/SpellEffects.cpp index 869dcd89f6..be000ffe5b 100644 --- a/src/server/game/Spells/SpellEffects.cpp +++ b/src/server/game/Spells/SpellEffects.cpp @@ -2603,21 +2603,53 @@ void Spell::EffectSummonPet(SpellEffIndex effIndex) bool isClassPet = m_spellInfo->SpellFamilyName != SPELLFAMILY_GENERIC || petCreatureId == 0; - // Dead Hunter Pets cannot be summoned and must be brought back by a resurrect pet spell effect - if (petCreatureId == 0) + // If a Hunter tries to summon a Hunter pet while having a dead pet, that pet must be resurrected first + if (petCreatureId == 0 && unitCaster->IsPlayer()) { - if (Player* player = unitCaster->ToPlayer()) + Player* player = unitCaster->ToPlayer(); + Optional const& key = player->GetActiveClassPetDataKey(); + if (key.has_value()) { - PlayerPetData* petData = player->GetPlayerPetData(petSlotIndex, petCreatureId); - if (petData && petData->SavedHealth == 0) + PlayerPetData* petData = player->GetPlayerPetData(key->first, key->second); + if (!petData) + { + WorldPackets::Pet::PetTameFailure packet; + packet.Result = PET_TAME_FAILURE_NO_PET_TO_SUMMON; + player->SendDirectMessage(packet.Write()); + return; + } + + if (petData->SavedHealth == 0) { WorldPackets::Pet::PetTameFailure packet; packet.Result = PET_TAME_FAILURE_DEAD_PET; player->SendDirectMessage(packet.Write()); + unitCaster->GetSpellHistory()->ResetCooldown(GetSpellInfo()->Id, true); return; } } + + PlayerPetData* petData = player->GetPlayerPetData(petSlotIndex, petCreatureId); + if (!petData) + { + WorldPackets::Pet::PetTameFailure packet; + packet.Result = PET_TAME_FAILURE_NO_PET_TO_SUMMON; + player->SendDirectMessage(packet.Write()); + return; + } + else if (petData->SavedHealth == 0) + { + // This is a fallback edge case to prevent Hunter from hard-locking pet summoning if database data is polluted or someone meddled with the mechanics + player->SetActiveClassPetDataKey(std::make_pair(petData->Slot, petData->CreatureId)); + WorldPackets::Pet::PetTameFailure packet; + packet.Result = PET_TAME_FAILURE_DEAD_PET; + player->SendDirectMessage(packet.Write()); + + unitCaster->GetSpellHistory()->ResetCooldown(GetSpellInfo()->Id, true); + return; + } + } if (NewPet* summon = unitCaster->SummonPet(petCreatureId, petSlotIndex, m_spellInfo->Id, isClassPet, *destTarget)) @@ -3998,6 +4030,8 @@ void Spell::EffectDismissPet(SpellEffIndex effIndex) ExecuteLogEffectUnsummonObject(effIndex, unitTarget); unitTarget->ToNewPet()->Dismiss(); + // Dismissing a class pet via dismiss spell or pet action is going to mark a pet as inactive so it needs to be re-summoned + m_caster->ToPlayer()->SetActiveClassPetDataKey(std::nullopt); } void Spell::EffectSummonObject(SpellEffIndex effIndex) @@ -4644,25 +4678,45 @@ void Spell::EffectResurrectPet(SpellEffIndex /*effIndex*/) return; NewPet* pet = player->GetActivelyControlledSummon(); - if (!pet || pet->IsAlive()) - return; - - pet->NearTeleportTo(m_targets.GetDstPos()->GetPosition()); - pet->Relocate(m_targets.GetDstPos()->GetPosition()); // This is needed so SaveStayPosition() will get the proper coords. - pet->SetUInt32Value(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_NONE); - pet->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); - pet->setDeathState(ALIVE); - pet->ClearUnitState(UNIT_STATE_ALL_ERASABLE); - pet->SetHealth(pet->CountPctFromMaxHealth(damage)); + if (pet && !pet->IsAlive()) + { + pet->NearTeleportTo(m_targets.GetDstPos()->GetPosition()); + pet->Relocate(m_targets.GetDstPos()->GetPosition()); // This is needed so SaveStayPosition() will get the proper coords. + pet->SetUInt32Value(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_NONE); + pet->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); + pet->setDeathState(ALIVE); + pet->ClearUnitState(UNIT_STATE_ALL_ERASABLE); + pet->SetHealth(pet->CountPctFromMaxHealth(damage)); + + // Reset things for when the AI to takes over + if (CharmInfo* charmInfo = pet->GetCharmInfo()) + { + if (charmInfo->HasCommandState(COMMAND_FOLLOW)) + pet->FollowTarget(player); - // Reset things for when the AI to takes over - if (CharmInfo* charmInfo = pet->GetCharmInfo()) + if (charmInfo->HasCommandState(COMMAND_STAY)) + charmInfo->SaveStayPosition(); + } + } + else if (!pet) { - if (charmInfo->HasCommandState(COMMAND_FOLLOW)) - pet->FollowTarget(player); + Optional const& key = player->GetActiveClassPetDataKey(); + if (!key.has_value()) + return; - if (charmInfo->HasCommandState(COMMAND_STAY)) - charmInfo->SaveStayPosition(); + PlayerPetData* petData = player->GetPlayerPetData(key->first, key->second); + if (petData && petData->SavedHealth == 0) + { + if (pet = player->SummonPet(key->second, key->first, m_spellInfo->Id, true, m_targets.GetDstPos()->GetPosition())) + { + pet->Relocate(m_targets.GetDstPos()->GetPosition()); // This is needed so SaveStayPosition() will get the proper coords. + pet->SetUInt32Value(UNIT_DYNAMIC_FLAGS, UNIT_DYNFLAG_NONE); + pet->RemoveFlag(UNIT_FIELD_FLAGS, UNIT_FLAG_SKINNABLE); + pet->setDeathState(ALIVE); + pet->ClearUnitState(UNIT_STATE_ALL_ERASABLE); + pet->SetHealth(pet->CountPctFromMaxHealth(damage)); + } + } } }