diff --git a/src/common/Database/Implementation/CharacterDatabase.cpp b/src/common/Database/Implementation/CharacterDatabase.cpp index 629aae3e7..2f068fcf0 100644 --- a/src/common/Database/Implementation/CharacterDatabase.cpp +++ b/src/common/Database/Implementation/CharacterDatabase.cpp @@ -520,7 +520,7 @@ void CharacterDatabaseConnection::DoPrepareStatements() // Pet PrepareStatement(CHAR_SEL_PET_SLOTS, "SELECT owner, slot FROM character_pet WHERE owner = ? AND slot >= ? AND slot <= ? ORDER BY slot", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_PET_SLOTS_DETAIL, "SELECT owner, id, entry, level, name FROM character_pet WHERE owner = ? AND slot >= ? AND slot <= ? ORDER BY slot", CONNECTION_ASYNC); - PrepareStatement(CHAR_SEL_PET_ENTRY, "SELECT entry FROM character_pet WHERE owner = ? AND id = ? AND slot >= ? AND slot <= ?", CONNECTION_ASYNC); + PrepareStatement(CHAR_SEL_PET_ENTRY, "SELECT entry, slot FROM character_pet WHERE owner = ? AND id = ? AND slot >= ? AND slot <= ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_PET_SLOT_BY_ID, "SELECT slot, entry FROM character_pet WHERE owner = ? AND id = ?", CONNECTION_ASYNC); PrepareStatement(CHAR_SEL_PET_SPELL_LIST, "SELECT DISTINCT pet_spell.spell FROM pet_spell, character_pet WHERE character_pet.owner = ? AND character_pet.id = pet_spell.guid AND character_pet.id <> ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHAR_PET, "SELECT id FROM character_pet WHERE owner = ? AND id <> ?", CONNECTION_SYNCH); diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index 56810de68..884ebfd18 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -149,6 +149,11 @@ struct CreatureTemplate return SKILL_SKINNING; // normal case } + bool IsExotic() const + { + return (type_flags & CREATURE_TYPE_FLAG_EXOTIC_PET) != 0; + } + bool IsTameable(bool exotic) const { if (type != CREATURE_TYPE_BEAST || family == 0 || (type_flags & CREATURE_TYPE_FLAG_TAMEABLE_PET) == 0) diff --git a/src/server/game/Entities/Pet/Pet.cpp b/src/server/game/Entities/Pet/Pet.cpp index 59adaf393..ccef54968 100644 --- a/src/server/game/Entities/Pet/Pet.cpp +++ b/src/server/game/Entities/Pet/Pet.cpp @@ -1,4 +1,4 @@ -/* +/* * Copyright (C) 2016+ AzerothCore , released under GNU GPL v2 license: https://github.com/azerothcore/azerothcore-wotlk/blob/master/LICENSE-GPL2 * Copyright (C) 2008-2016 TrinityCore * Copyright (C) 2005-2009 MaNGOS @@ -97,6 +97,55 @@ void Pet::RemoveFromWorld() } } +SpellCastResult Pet::TryLoadFromDB(Player* owner, bool current /*= false*/, PetType mandatoryPetType /*= MAX_PET_TYPE*/) +{ + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PET_BY_ENTRY_AND_SLOT); + stmt->setUInt32(0, owner->GetGUIDLow()); + stmt->setUInt8(1, uint8(current ? PET_SAVE_AS_CURRENT : PET_SAVE_NOT_IN_SLOT)); + + PreparedQueryResult result = CharacterDatabase.AsyncQuery(stmt); + + if (!result) + return SPELL_FAILED_NO_PET; + + Field* fields = result->Fetch(); + + uint32 petentry = fields[1].GetUInt32(); + uint32 savedHealth = fields[10].GetUInt32(); + uint32 summon_spell_id = fields[15].GetUInt32(); + auto petType = PetType(fields[16].GetUInt8()); + + // update for case of current pet "slot = 0" + if (!petentry) + return SPELL_FAILED_NO_PET; + + CreatureTemplate const* creatureInfo = sObjectMgr->GetCreatureTemplate(petentry); + if (!creatureInfo) + { + sLog->outError("Pet entry %u does not exist but used at pet load (owner: %s).", petentry, owner->GetName().c_str()); + return SPELL_FAILED_NO_PET; + } + + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(summon_spell_id); + + bool isTemporarySummoned = spellInfo && spellInfo->GetDuration() > 0; + + // check temporary summoned pets like mage water elemental + if (current && isTemporarySummoned) + return SPELL_FAILED_NO_PET; + + if (!savedHealth) + { + owner->ToPlayer()->SendTameFailure(PET_TAME_DEAD); + return SPELL_FAILED_TARGETS_DEAD; + } + + if (mandatoryPetType != MAX_PET_TYPE && petType != mandatoryPetType) + return SPELL_FAILED_BAD_TARGETS; + + return SPELL_CAST_OK; +} + bool Pet::LoadPetFromDB(Player* owner, uint8 asynchLoadType, uint32 petentry, uint32 petnumber, bool current, AsynchPetSummon* info) { // we are loading pet at that moment diff --git a/src/server/game/Entities/Pet/Pet.h b/src/server/game/Entities/Pet/Pet.h index 7802d322b..9c19212e9 100644 --- a/src/server/game/Entities/Pet/Pet.h +++ b/src/server/game/Entities/Pet/Pet.h @@ -63,6 +63,7 @@ class Pet : public Guardian bool CreateBaseAtCreature(Creature* creature); bool CreateBaseAtCreatureInfo(CreatureTemplate const* cinfo, Unit* owner); bool CreateBaseAtTamed(CreatureTemplate const* cinfo, Map* map, uint32 phaseMask); + static SpellCastResult TryLoadFromDB(Player* owner, bool current = false, PetType mandatoryPetType = MAX_PET_TYPE); static bool LoadPetFromDB(Player* owner, uint8 asynchLoadType, uint32 petentry = 0, uint32 petnumber = 0, bool current = false, AsynchPetSummon* info = NULL); bool isBeingLoaded() const { return m_loading;} void SavePetToDB(PetSaveMode mode, bool logout); diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index a0d65ae5f..9d5509777 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -20815,7 +20815,10 @@ void Player::RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent) { // xinef: dont save dead pet as current, save him not in slot if (!pet->IsAlive() && mode == PET_SAVE_AS_CURRENT && pet->getPetType() == HUNTER_PET) + { mode = PET_SAVE_NOT_IN_SLOT; + m_temporaryUnsummonedPetNumber = 0; + } #if defined(ENABLE_EXTRAS) && defined(ENABLE_EXTRA_LOGS) sLog->outDebug(LOG_FILTER_PETS, "RemovePet %u, %u, %u", pet->GetEntry(), mode, returnreagent); @@ -27579,3 +27582,19 @@ void Player::SummonPet(uint32 entry, float x, float y, float z, float ang, PetTy AsynchPetSummon* asynchPetInfo = new AsynchPetSummon(entry, pos, petType, duration, createdBySpell, casterGUID); Pet::LoadPetFromDB(this, asynchLoadType, entry, 0, false, asynchPetInfo); } + +bool Player::IsPetDismissed() +{ + /* + * Check PET_SAVE_NOT_IN_SLOT means the pet is dismissed. If someone ever + * Changes the slot flag, they will break this validation. + */ + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PET_BY_ENTRY_AND_SLOT); + stmt->setUInt32(0, GetGUIDLow()); + stmt->setUInt8(1, uint8(PET_SAVE_NOT_IN_SLOT)); + + if (PreparedQueryResult result = CharacterDatabase.AsyncQuery(stmt)) + return true; + + return false; +} diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index d63164a81..3ff7b0b4d 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1214,6 +1214,7 @@ class Player : public Unit, public GridObject uint32 GetInnTriggerId() const { return _innTriggerId; } Pet* GetPet() const; + bool IsPetDismissed(); void SummonPet(uint32 entry, float x, float y, float z, float ang, PetType petType, uint32 despwtime, uint32 createdBySpell, uint64 casterGUID, uint8 asynchLoadType); void RemovePet(Pet* pet, PetSaveMode mode, bool returnreagent = false); uint32 GetPhaseMaskForSpawn() const; // used for proper set phase for DB at GM-mode creature/GO spawn diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index 81f7e35c9..855d3b71d 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -18484,6 +18484,13 @@ void Unit::NearTeleportTo(float x, float y, float z, float orientation, bool cas } } +void Unit::SendTameFailure(uint8 result) +{ + WorldPacket data(SMSG_PET_TAME_FAILURE, 1); + data << uint8(result); + ToPlayer()->SendDirectMessage(&data); +} + void Unit::SendTeleportPacket(Position& pos) { Position oldPos = { GetPositionX(), GetPositionY(), GetPositionZ(), GetOrientation() }; diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 0d6955e50..11a446a83 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -1761,6 +1761,7 @@ class Unit : public WorldObject void SendSpellDamageImmune(Unit* target, uint32 spellId); void NearTeleportTo(float x, float y, float z, float orientation, bool casting = false, bool vehicleTeleport = false, bool withPet = false, bool removeTransport = false); + void SendTameFailure(uint8 result); void SendTeleportPacket(Position& pos); virtual bool UpdatePosition(float x, float y, float z, float ang, bool teleport = false); // returns true if unit's position really changed diff --git a/src/server/game/Handlers/NPCHandler.cpp b/src/server/game/Handlers/NPCHandler.cpp index 9e3c7c87d..6e48727c2 100644 --- a/src/server/game/Handlers/NPCHandler.cpp +++ b/src/server/game/Handlers/NPCHandler.cpp @@ -581,6 +581,24 @@ void WorldSession::SendStablePetCallback(PreparedQueryResult result, uint64 guid data << uint8(1); // 1 = current, 2/3 = in stable (any from 4, 5, ... create problems with proper show) ++num; } + else if (_player->IsPetDismissed() || _player->GetTemporaryUnsummonedPetNumber()) + { + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_CHAR_PET_BY_ENTRY_AND_SLOT); + stmt->setUInt32(0, _player->GetGUIDLow()); + stmt->setUInt8(1, uint8(_player->GetTemporaryUnsummonedPetNumber() ? PET_SAVE_AS_CURRENT : PET_SAVE_NOT_IN_SLOT)); + + if (PreparedQueryResult _result = CharacterDatabase.AsyncQuery(stmt)) + { + Field* fields = _result->Fetch(); + + data << uint32(fields[0].GetUInt32()); // id + data << uint32(fields[1].GetUInt32()); // entry + data << uint32(fields[4].GetUInt16()); // level + data << fields[8].GetString(); // petname + data << uint8(1); + ++num; + } + } if (result) { @@ -639,10 +657,22 @@ void WorldSession::HandleStablePet(WorldPacket & recvData) Pet* pet = _player->GetPet(); // can't place in stable dead pet - if (!pet || !pet->IsAlive() || pet->getPetType() != HUNTER_PET) + if (pet) { - SendStableResult(STABLE_ERR_STABLE); - return; + if (!pet->IsAlive() || pet->getPetType() != HUNTER_PET) + { + SendStableResult(STABLE_ERR_STABLE); + return; + } + } + else + { + SpellCastResult loadResult = Pet::TryLoadFromDB(_player, _player->GetTemporaryUnsummonedPetNumber() != 0, HUNTER_PET); + if (loadResult != SPELL_CAST_OK) + { + SendStableResult(STABLE_ERR_STABLE); + return; + } } PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_PET_SLOTS); @@ -681,8 +711,27 @@ void WorldSession::HandleStablePetCallback(PreparedQueryResult result) WorldPacket data(SMSG_STABLE_RESULT, 1); if (freeSlot > 0 && freeSlot <= GetPlayer()->m_stableSlots) { - _player->RemovePet(_player->GetPet(), PetSaveMode(freeSlot)); + if (_player->GetPetGUID()) + { + _player->RemovePet(_player->GetPet(), PetSaveMode(freeSlot)); + SendStableResult(STABLE_SUCCESS_STABLE); + return; + } + + // change pet slot directly in database + SQLTransaction trans = CharacterDatabase.BeginTransaction(); + + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_PET_SLOT_BY_SLOT); + stmt->setUInt8(0, freeSlot); + stmt->setUInt32(1, _player->GetGUIDLow()); + stmt->setUInt8(2, uint8(_player->GetTemporaryUnsummonedPetNumber() ? PET_SAVE_AS_CURRENT : PET_SAVE_NOT_IN_SLOT)); + trans->Append(stmt); + + CharacterDatabase.CommitTransaction(trans); + + _player->SetTemporaryUnsummonedPetNumber(0); SendStableResult(STABLE_SUCCESS_STABLE); + return; } else SendStableResult(STABLE_ERR_STABLE); @@ -725,10 +774,12 @@ void WorldSession::HandleUnstablePetCallback(PreparedQueryResult result, uint32 return; uint32 petEntry = 0; + uint32 slot = 0; if (result) { Field* fields = result->Fetch(); petEntry = fields[0].GetUInt32(); + slot = fields[1].GetUInt32(); } if (!petEntry) @@ -757,7 +808,30 @@ void WorldSession::HandleUnstablePetCallback(PreparedQueryResult result, uint32 // delete dead pet if (pet) + { _player->RemovePet(pet, PET_SAVE_AS_DELETED); + } + else if (_player->IsPetDismissed() || _player->GetTemporaryUnsummonedPetNumber()) + { + // try to find if pet is actually temporary unsummoned and alive + SpellCastResult loadResult = Pet::TryLoadFromDB(_player, _player->GetTemporaryUnsummonedPetNumber() != 0, HUNTER_PET); + if (loadResult != SPELL_CAST_OK) + { + SendStableResult(STABLE_ERR_STABLE); + return; + } + // change pet slot directly in database + SQLTransaction trans = CharacterDatabase.BeginTransaction(); + + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_PET_SLOT_BY_SLOT); + stmt->setUInt8(0, slot); + stmt->setUInt32(1, _player->GetGUIDLow()); + stmt->setUInt8(2, uint8(_player->GetTemporaryUnsummonedPetNumber() ? PET_SAVE_AS_CURRENT : PET_SAVE_NOT_IN_SLOT)); + trans->Append(stmt); + + CharacterDatabase.CommitTransaction(trans); + _player->SetTemporaryUnsummonedPetNumber(0); + } if (!Pet::LoadPetFromDB(_player, PET_LOAD_HANDLE_UNSTABLE_CALLBACK, petEntry, petId)) { @@ -830,14 +904,26 @@ void WorldSession::HandleStableSwapPet(WorldPacket & recvData) Pet* pet = _player->GetPet(); - if (!pet || pet->getPetType() != HUNTER_PET) + if (pet) { - SendStableResult(STABLE_ERR_STABLE); - return; + if (pet->getPetType() != HUNTER_PET) + { + SendStableResult(STABLE_ERR_STABLE); + return; + } + } + else if (_player->IsPetDismissed() || _player->GetTemporaryUnsummonedPetNumber()) + { + // try to find if pet is actually temporary unsummoned and alive + SpellCastResult loadResult = Pet::TryLoadFromDB(_player, _player->GetTemporaryUnsummonedPetNumber() != 0, HUNTER_PET); + if (loadResult != SPELL_CAST_OK) + { + SendStableResult(STABLE_ERR_STABLE); + return; + } } // Find swapped pet slot in stable - PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_PET_SLOT_BY_ID); stmt->setUInt32(0, _player->GetGUIDLow()); @@ -881,15 +967,24 @@ void WorldSession::HandleStableSwapPetCallback(PreparedQueryResult result, uint3 } Pet* pet = _player->GetPet(); - // The player's pet could have been removed during the delay of the DB callback - if (!pet) - { - SendStableResult(STABLE_ERR_STABLE); - return; - } - + // move alive pet to slot or delete dead pet - _player->RemovePet(pet, pet->IsAlive() ? PetSaveMode(slot) : PET_SAVE_AS_DELETED); + if (pet) + _player->RemovePet(pet, pet->IsAlive() ? PetSaveMode(slot) : PET_SAVE_AS_DELETED); + else if (_player->IsPetDismissed() || _player->GetTemporaryUnsummonedPetNumber()) + { + // change pet slot directly in database + SQLTransaction trans = CharacterDatabase.BeginTransaction(); + + PreparedStatement* stmt = CharacterDatabase.GetPreparedStatement(CHAR_UDP_CHAR_PET_SLOT_BY_SLOT); + stmt->setUInt8(0, slot); + stmt->setUInt32(1, _player->GetGUIDLow()); + stmt->setUInt8(2, uint8(_player->GetTemporaryUnsummonedPetNumber() ? PET_SAVE_AS_CURRENT : PET_SAVE_NOT_IN_SLOT)); + trans->Append(stmt); + + CharacterDatabase.CommitTransaction(trans); + _player->SetTemporaryUnsummonedPetNumber(0); + } // summon unstabled pet if (!Pet::LoadPetFromDB(_player, PET_LOAD_HANDLE_UNSTABLE_CALLBACK, petEntry, petId)) diff --git a/src/server/game/Miscellaneous/SharedDefines.h b/src/server/game/Miscellaneous/SharedDefines.h index 52b2de661..ada4396aa 100644 --- a/src/server/game/Miscellaneous/SharedDefines.h +++ b/src/server/game/Miscellaneous/SharedDefines.h @@ -3475,6 +3475,23 @@ enum PetNameInvalidReason PET_NAME_DECLENSION_DOESNT_MATCH_BASE_NAME = 16 }; +enum PetTameFailure +{ + PET_TAME_INVALID_CREATURE = 1, + PET_TAME_TOO_MANY = 2, + PET_TAME_CREATURE_ALREADY_OWNED = 3, + PET_TAME_NOT_TAMEABLE = 4, + PET_TAME_ANOTHER_SUMMON_ACTIVE = 5, + PET_TAME_UNITS_CANT_TAME = 6, + PET_TAME_NOPET_AVAILABLE = 7, + PET_TAME_INTERNAL_ERROR = 8, + PET_TAME_TOO_HIGHLEVEL = 9, + PET_TAME_DEAD = 10, + PET_TAME_NOTDEAD = 11, + PET_TAME_CANT_CONTROL_EXOTIC = 12, + PET_TAME_UNKNOWNERROR = 13 +}; + enum DungeonStatusFlag { DUNGEON_STATUSFLAG_NORMAL = 0x01, diff --git a/src/server/scripts/Spells/spell_hunter.cpp b/src/server/scripts/Spells/spell_hunter.cpp index da16f80be..695bdc277 100644 --- a/src/server/scripts/Spells/spell_hunter.cpp +++ b/src/server/scripts/Spells/spell_hunter.cpp @@ -1118,30 +1118,50 @@ class spell_hun_tame_beast : public SpellScriptLoader SpellCastResult CheckCast() { - Unit* caster = GetCaster(); + Unit* caster = GetCaster(); if (caster->GetTypeId() != TYPEID_PLAYER) return SPELL_FAILED_DONT_REPORT; + Player* player = GetCaster()->ToPlayer(); + if (!GetExplTargetUnit()) - return SPELL_FAILED_BAD_IMPLICIT_TARGETS; + { + player->SendTameFailure(PET_TAME_INVALID_CREATURE); + return SPELL_FAILED_DONT_REPORT; + } if (Creature* target = GetExplTargetUnit()->ToCreature()) { - if (target->getLevel() > caster->getLevel()) - return SPELL_FAILED_HIGHLEVEL; + if (target->getLevel() > player->getLevel()) + { + player->SendTameFailure(PET_TAME_TOO_HIGHLEVEL); + return SPELL_FAILED_DONT_REPORT; + } + + if (target->GetCreatureTemplate()->IsExotic() && !player->CanTameExoticPets()) + { + player->SendTameFailure(PET_TAME_CANT_CONTROL_EXOTIC); + return SPELL_FAILED_DONT_REPORT; + } - // use SMSG_PET_TAME_FAILURE? - if (!target->GetCreatureTemplate()->IsTameable(caster->ToPlayer()->CanTameExoticPets())) - return SPELL_FAILED_BAD_TARGETS; + if (!target->GetCreatureTemplate()->IsTameable(player->CanTameExoticPets())) + { + player->SendTameFailure(PET_TAME_NOT_TAMEABLE); + return SPELL_FAILED_DONT_REPORT; + } + + if (caster->GetPetGUID() || player->GetTemporaryUnsummonedPetNumber() || player->IsPetDismissed() || player->GetCharmGUID()) + { + player->SendTameFailure(PET_TAME_ANOTHER_SUMMON_ACTIVE); + return SPELL_FAILED_DONT_REPORT; + } - if (caster->GetPetGUID()) - return SPELL_FAILED_ALREADY_HAVE_SUMMON; - - if (caster->GetCharmGUID()) - return SPELL_FAILED_ALREADY_HAVE_CHARM; } else - return SPELL_FAILED_BAD_IMPLICIT_TARGETS; + { + player->SendTameFailure(PET_TAME_INVALID_CREATURE); + return SPELL_FAILED_DONT_REPORT; + } return SPELL_CAST_OK; }