From 718302fa185c5c07d1eb424b2df85e0f5fe366b2 Mon Sep 17 00:00:00 2001 From: hondacrx Date: Thu, 10 Mar 2022 09:39:55 -0500 Subject: [PATCH] Core/Pets: Pet management refactoring Port From (https://github.com/TrinityCore/TrinityCore/commit/ca92686b44cc8c53a8991bf02d6e8534797fc115) --- .../Database/Databases/CharacterDatabase.cs | 28 +- Source/Game/Chat/Commands/PetCommands.cs | 34 +- Source/Game/Entities/Pet.cs | 596 ++++++++++-------- Source/Game/Entities/Player/Player.DB.cs | 60 +- Source/Game/Entities/Player/Player.Fields.cs | 2 +- Source/Game/Entities/Player/Player.cs | 49 +- Source/Game/Entities/Unit/Unit.Pets.cs | 14 +- Source/Game/Handlers/CharacterHandler.cs | 5 + Source/Game/Handlers/NPCHandler.cs | 77 ++- Source/Game/Handlers/PetHandler.cs | 7 +- Source/Game/Server/WorldSession.cs | 2 +- Source/Game/Spells/Spell.cs | 29 + Source/Game/Spells/SpellEffects.cs | 4 +- Source/Scripts/Spells/Hunter.cs | 20 +- 14 files changed, 549 insertions(+), 378 deletions(-) diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index 4a76d2cf7..697eae6fe 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -693,13 +693,7 @@ namespace Framework.Database PrepareStatement(CharStatements.DEL_CALENDAR_INVITE, "DELETE FROM calendar_invites WHERE InviteID = ?"); // Pet - PrepareStatement(CharStatements.SEL_PET_SLOTS, "SELECT owner, slot FROM character_pet WHERE owner = ? AND slot >= ? AND slot <= ? ORDER BY slot"); - PrepareStatement(CharStatements.SEL_PET_SLOTS_DETAIL, "SELECT owner, id, entry, level, name, modelid FROM character_pet WHERE owner = ? AND slot >= ? AND slot <= ? ORDER BY slot"); - PrepareStatement(CharStatements.SEL_PET_ENTRY, "SELECT entry FROM character_pet WHERE owner = ? AND id = ? AND slot >= ? AND slot <= ?"); - PrepareStatement(CharStatements.SEL_PET_SLOT_BY_ID, "SELECT slot, entry FROM character_pet WHERE owner = ? AND id = ?"); - PrepareStatement(CharStatements.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 <> ?"); - PrepareStatement(CharStatements.SEL_CHAR_PET, "SELECT id FROM character_pet WHERE owner = ? AND id <> ?"); - PrepareStatement(CharStatements.SEL_CHAR_PETS, "SELECT id FROM character_pet WHERE owner = ?"); + PrepareStatement(CharStatements.SEL_CHAR_PET_IDS, "SELECT id FROM character_pet WHERE owner = ?"); PrepareStatement(CharStatements.DEL_CHAR_PET_DECLINEDNAME_BY_OWNER, "DELETE FROM character_pet_declinedname WHERE owner = ?"); PrepareStatement(CharStatements.DEL_CHAR_PET_DECLINEDNAME, "DELETE FROM character_pet_declinedname WHERE id = ?"); PrepareStatement(CharStatements.INS_CHAR_PET_DECLINEDNAME, "INSERT INTO character_pet_declinedname (id, owner, genitive, dative, accusative, instrumental, prepositional) VALUES (?, ?, ?, ?, ?, ?, ?)"); @@ -722,14 +716,9 @@ namespace Framework.Database "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); PrepareStatement(CharStatements.INS_PET_AURA_EFFECT, "INSERT INTO pet_aura_effect (guid, casterGuid, spell, effectMask, effectIndex, amount, baseAmount) " + "VALUES (?, ?, ?, ?, ?, ?, ?)"); - PrepareStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY, "SELECT id, entry, owner, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ? AND id = ?"); - PrepareStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY_AND_SLOT_2, "SELECT id, entry, owner, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ? AND entry = ? AND (slot = ? OR slot > ?)"); - PrepareStatement(CharStatements.SEL_CHAR_PET_BY_SLOT, "SELECT id, entry, owner, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ? AND (slot = ? OR slot > ?) "); - PrepareStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY_AND_SLOT, "SELECT id, entry, owner, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ? AND slot = ?"); + PrepareStatement(CharStatements.SEL_CHAR_PETS, "SELECT id, entry, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ?"); PrepareStatement(CharStatements.DEL_CHAR_PET_BY_OWNER, "DELETE FROM character_pet WHERE owner = ?"); PrepareStatement(CharStatements.UPD_CHAR_PET_NAME, "UPDATE character_pet SET name = ?, renamed = 1 WHERE owner = ? AND id = ?"); - PrepareStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_SLOT_EXCLUDE_ID, "UPDATE character_pet SET slot = ? WHERE owner = ? AND slot = ? AND id <> ?"); - PrepareStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_SLOT, "UPDATE character_pet SET slot = ? WHERE owner = ? AND slot = ?"); PrepareStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_ID, "UPDATE character_pet SET slot = ? WHERE owner = ? AND id = ?"); PrepareStatement(CharStatements.DEL_CHAR_PET_BY_ID, "DELETE FROM character_pet WHERE id = ?"); PrepareStatement(CharStatements.DEL_CHAR_PET_BY_SLOT, "DELETE FROM character_pet WHERE owner = ? AND (slot = ? OR slot > ?)"); @@ -1356,23 +1345,12 @@ namespace Framework.Database DEL_PET_SPELLS, DEL_CHAR_PET_BY_OWNER, DEL_CHAR_PET_DECLINEDNAME_BY_OWNER, - SEL_CHAR_PET_BY_ENTRY_AND_SLOT, - SEL_PET_SLOTS, - SEL_PET_SLOTS_DETAIL, - SEL_PET_ENTRY, - SEL_PET_SLOT_BY_ID, - SEL_PET_SPELL_LIST, - SEL_CHAR_PET, + SEL_CHAR_PET_IDS, SEL_CHAR_PETS, INS_CHAR_PET, - SEL_CHAR_PET_BY_ENTRY, - SEL_CHAR_PET_BY_ENTRY_AND_SLOT_2, - SEL_CHAR_PET_BY_SLOT, DEL_CHAR_PET_DECLINEDNAME, INS_CHAR_PET_DECLINEDNAME, UPD_CHAR_PET_NAME, - UPD_CHAR_PET_SLOT_BY_SLOT_EXCLUDE_ID, - UPD_CHAR_PET_SLOT_BY_SLOT, UPD_CHAR_PET_SLOT_BY_ID, DEL_CHAR_PET_BY_ID, DEL_CHAR_PET_BY_SLOT, diff --git a/Source/Game/Chat/Commands/PetCommands.cs b/Source/Game/Chat/Commands/PetCommands.cs index 098c1d739..de6504b8c 100644 --- a/Source/Game/Chat/Commands/PetCommands.cs +++ b/Source/Game/Chat/Commands/PetCommands.cs @@ -52,41 +52,23 @@ namespace Game.Chat } // Everything looks OK, create new pet - Pet pet = new(player, PetType.Hunter); - if (!pet.CreateBaseAtCreature(creatureTarget)) - { - handler.SendSysMessage("Error 1"); - return false; - } + Pet pet = player.CreateTamedPetFrom(creatureTarget); - creatureTarget.SetDeathState(DeathState.JustDied); - creatureTarget.RemoveCorpse(); - creatureTarget.SetHealth(0); // just for nice GM-mode view - - pet.SetCreatorGUID(player.GetGUID()); - pet.SetFaction(player.GetFaction()); - - if (!pet.InitStatsForLevel(creatureTarget.GetLevel())) - { - Log.outError(LogFilter.ChatSystem, "InitStatsForLevel() in EffectTameCreature failed! Pet deleted."); - handler.SendSysMessage("Error 2"); - return false; - } + // "kill" original creature + creatureTarget.DespawnOrUnsummon(); // prepare visual effect for levelup - pet.SetLevel(creatureTarget.GetLevel() - 1); - - pet.GetCharmInfo().SetPetNumber(Global.ObjectMgr.GeneratePetNumber(), true); - // this enables pet details window (Shift+P) - pet.InitPetCreateSpells(); - pet.SetFullHealth(); + pet.SetLevel(player.GetLevel() - 1); + // add to world pet.GetMap().AddToMap(pet.ToCreature()); // visual effect for levelup - pet.SetLevel(creatureTarget.GetLevel()); + pet.SetLevel(player.GetLevel()); + // caster have pet now player.SetMinion(pet, true); + pet.SavePetToDB(PetSaveMode.AsCurrent); player.PetSpellInitialize(); diff --git a/Source/Game/Entities/Pet.cs b/Source/Game/Entities/Pet.cs index 2e1d437b8..998afa4e7 100644 --- a/Source/Game/Entities/Pet.cs +++ b/Source/Game/Entities/Pet.cs @@ -96,89 +96,95 @@ namespace Game.Entities } } - public bool LoadPetFromDB(Player owner, uint petEntry = 0, uint petnumber = 0, bool current = false) + public static Tuple GetLoadPetInfo(PetStable stable, uint petEntry, uint petnumber, bool current) { - m_loading = true; - - ulong ownerid = owner.GetGUID().GetCounter(); - - PreparedStatement stmt; - if (petnumber != 0) { // Known petnumber entry - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY); - stmt.AddValue(0, ownerid); - stmt.AddValue(1, petnumber); + if (stable.CurrentPet != null && stable.CurrentPet.PetNumber == petnumber) + return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + + for (var stableSlot = 0; stableSlot < stable.StabledPets.Length; ++stableSlot) + if (stable.StabledPets[stableSlot] != null && stable.StabledPets[stableSlot].PetNumber == petnumber) + return Tuple.Create(stable.StabledPets[stableSlot], PetSaveMode.FirstStableSlot + stableSlot); + + foreach (var pet in stable.UnslottedPets) + if (pet.CreatureId == petEntry) + return Tuple.Create(pet, PetSaveMode.NotInSlot); } else if (current) { // Current pet (slot 0) - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY_AND_SLOT); - stmt.AddValue(0, ownerid); - stmt.AddValue(1, (byte)PetSaveMode.AsCurrent); + if (stable.CurrentPet != null) + return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); } else if (petEntry != 0) { // known petEntry entry (unique for summoned pet, but non unique for hunter pet (only from current or not stabled pets) - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PET_BY_ENTRY_AND_SLOT_2); - stmt.AddValue(0, ownerid); - stmt.AddValue(1, petEntry); - stmt.AddValue(2, (byte)PetSaveMode.AsCurrent); - stmt.AddValue(3, (byte)PetSaveMode.LastStableSlot); + if (stable.CurrentPet != null && stable.CurrentPet.CreatureId == petEntry) + return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + + foreach (var pet in stable.UnslottedPets) + if (pet.CreatureId == petEntry) + return Tuple.Create(pet, PetSaveMode.NotInSlot); } else { // Any current or other non-stabled pet (for hunter "call pet") - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PET_BY_SLOT); - stmt.AddValue(0, ownerid); - stmt.AddValue(1, (byte)PetSaveMode.AsCurrent); - stmt.AddValue(2, (byte)PetSaveMode.LastStableSlot); + if (stable.CurrentPet != null) + return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + + if (!stable.UnslottedPets.Empty()) + return Tuple.Create(stable.UnslottedPets.First(), PetSaveMode.NotInSlot); } - SQLResult result = DB.Characters.Query(stmt); - if (result.IsEmpty()) + return Tuple.Create(null, PetSaveMode.AsDeleted); + } + + public bool LoadPetFromDB(Player owner, uint petEntry = 0, uint petnumber = 0, bool current = false) + { + m_loading = true; + + PetStable petStable = owner.GetPetStable(); + + ulong ownerid = owner.GetGUID().GetCounter(); + (PetStable.PetInfo petInfo, PetSaveMode slot) = GetLoadPetInfo(petStable, petEntry, petnumber, current); + if (petInfo == null) { m_loading = false; return false; } - // update for case of current pet "slot = 0" - petEntry = result.Read(1); - if (petEntry == 0) - return false; - - uint summonSpellId = result.Read(14); - SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(summonSpellId, owner.GetMap().GetDifficultyID()); + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(petInfo.CreatedBySpellId, owner.GetMap().GetDifficultyID()); bool isTemporarySummon = spellInfo != null && spellInfo.GetDuration() > 0; if (current && isTemporarySummon) return false; - PetType petType = (PetType)result.Read(15); - if (petType == PetType.Hunter) + if (petInfo.Type == PetType.Hunter) { - CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(petEntry); + CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(petInfo.CreatureId); if (creatureInfo == null || !creatureInfo.IsTameable(owner.CanTameExoticPets())) return false; } - uint petId = result.Read(0); if (current && owner.IsPetNeedBeTemporaryUnsummoned()) { - owner.SetTemporaryUnsummonedPetNumber(petId); + owner.SetTemporaryUnsummonedPetNumber(petInfo.PetNumber); return false; } Map map = owner.GetMap(); - if (!Create(map.GenerateLowGuid(HighGuid.Pet), map, petEntry)) + ulong guid = map.GenerateLowGuid(HighGuid.Pet); + + if (!Create(guid, map, petInfo.CreatureId, petInfo.PetNumber)) return false; PhasingHandler.InheritPhaseShift(this, owner); - SetPetType(petType); + SetPetType(petInfo.Type); SetFaction(owner.GetFaction()); - SetCreatedBySpell(summonSpellId); + SetCreatedBySpell(petInfo.CreatedBySpellId); float px, py, pz; if (IsCritter()) @@ -197,14 +203,14 @@ namespace Game.Entities return true; } - GetCharmInfo().SetPetNumber(petId, IsPermanentPetFor(owner)); + GetCharmInfo().SetPetNumber(petInfo.PetNumber, IsPermanentPetFor(owner)); - SetDisplayId(result.Read(3)); - SetNativeDisplayId(result.Read(3)); - uint petlevel = result.Read(4); + SetDisplayId(petInfo.DisplayId); + SetNativeDisplayId(petInfo.DisplayId); + uint petlevel = petInfo.Level; SetNpcFlags(NPCFlags.None); SetNpcFlags2(NPCFlags2.None); - SetName(result.Read(8)); + SetName(petInfo.Name); switch (GetPetType()) { @@ -218,7 +224,7 @@ namespace Game.Entities SetClass(Class.Warrior); SetGender(Gender.None); SetSheath(SheathState.Melee); - SetPetFlags(result.Read(9) ? UnitPetFlags.CanBeAbandoned : UnitPetFlags.CanBeRenamed | UnitPetFlags.CanBeAbandoned); + SetPetFlags(petInfo.WasRenamed ? UnitPetFlags.CanBeAbandoned : UnitPetFlags.CanBeRenamed | UnitPetFlags.CanBeAbandoned); SetUnitFlags(UnitFlags.PlayerControlled); // this enables popup window (pet abandon, cancel) break; default: @@ -231,7 +237,7 @@ namespace Game.Entities SetCreatorGUID(owner.GetGUID()); InitStatsForLevel(petlevel); - SetPetExperience(result.Read(5)); + SetPetExperience(petInfo.Experience); SynchronizeLevelWithOwner(); @@ -244,15 +250,15 @@ namespace Game.Entities return false; } - SetReactState((ReactStates)result.Read(6)); + SetReactState(petInfo.ReactState); SetCanModifyStats(true); if (GetPetType() == PetType.Summon && !current) //all (?) summon pets come with full health when called, but not when they are current SetFullPower(PowerType.Mana); else { - uint savedhealth = result.Read(10); - uint savedmana = result.Read(11); + uint savedhealth = petInfo.Health; + uint savedmana = petInfo.Mana; if (savedhealth == 0 && GetPetType() == PetType.Hunter) SetDeathState(DeathState.JustDied); else @@ -266,102 +272,108 @@ namespace Game.Entities // 0=current // 1..MAX_PET_STABLES in stable slot // PET_SAVE_NOT_IN_SLOT(100) = not stable slot (summoning)) - if (result.Read(7) != 0) + if (slot == PetSaveMode.NotInSlot) { - stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_SLOT_EXCLUDE_ID); - stmt.AddValue(0, (byte)PetSaveMode.NotInSlot); - stmt.AddValue(1, ownerid); - stmt.AddValue(2, (byte)PetSaveMode.AsCurrent); - stmt.AddValue(3, GetCharmInfo().GetPetNumber()); - DB.Characters.Execute(stmt); + var unslottedPetInfo = petStable.UnslottedPets.Find(unslottedPet => unslottedPet.PetNumber == petInfo.PetNumber); + Cypher.Assert(petStable.CurrentPet == null); + Cypher.Assert(unslottedPetInfo != null); - stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_ID); - stmt.AddValue(0, (byte)PetSaveMode.AsCurrent); - stmt.AddValue(1, ownerid); - stmt.AddValue(2, GetCharmInfo().GetPetNumber()); - DB.Characters.Execute(stmt); + petStable.CurrentPet = unslottedPetInfo; + petStable.UnslottedPets.Remove(unslottedPetInfo); } // Send fake summon spell cast - this is needed for correct cooldown application for spells // Example: 46584 - without this cooldown (which should be set always when pet is loaded) isn't set clientside // @todo pets should be summoned from real cast instead of just faking it? - if (summonSpellId != 0) + if (petInfo.CreatedBySpellId != 0) { SpellGo spellGo = new(); SpellCastData castData = spellGo.Cast; castData.CasterGUID = owner.GetGUID(); castData.CasterUnit = owner.GetGUID(); - castData.CastID = ObjectGuid.Create(HighGuid.Cast, SpellCastSource.Normal, owner.GetMapId(), summonSpellId, map.GenerateLowGuid(HighGuid.Cast)); - castData.SpellID = (int)summonSpellId; + castData.CastID = ObjectGuid.Create(HighGuid.Cast, SpellCastSource.Normal, owner.GetMapId(), petInfo.CreatedBySpellId, map.GenerateLowGuid(HighGuid.Cast)); + castData.SpellID = (int)petInfo.CreatedBySpellId; castData.CastFlags = SpellCastFlags.Unk9; castData.CastTime = Time.GetMSTime(); owner.SendMessageToSet(spellGo, true); } owner.SetMinion(this, true); + + if (!isTemporarySummon) + GetCharmInfo().LoadPetActionBar(petInfo.ActionBar); + map.AddToMap(ToCreature()); - uint timediff = (uint)(GameTime.GetGameTime() - result.Read(13)); - _LoadAuras(timediff); - - // load action bar, if data broken will fill later by default spells. - if (!isTemporarySummon) - { - GetCharmInfo().LoadPetActionBar(result.Read(12)); - - _LoadSpells(); - _LoadSpellCooldowns(); - LearnPetPassives(); - InitLevelupSpellsForLevel(); - if (map.IsBattleArena()) - RemoveArenaAuras(); - - CastPetAuras(current); - } - - Log.outDebug(LogFilter.Pet, "New Pet has guid {0}", GetGUID().ToString()); - - ushort specId = result.Read(16); - ChrSpecializationRecord petSpec = CliDB.ChrSpecializationStorage.LookupByKey(specId); - if (petSpec != null) - specId = (ushort)Global.DB2Mgr.GetChrSpecializationByIndex(owner.HasAuraType(AuraType.OverridePetSpecs) ? Class.Max : 0, petSpec.OrderIndex).Id; - - SetSpecialization(specId); - - // The SetSpecialization function will run these functions if the pet's spec is not 0 - if (GetSpecialization() == 0) - { - CleanupActionBar(); // remove unknown spells from action bar after load - owner.PetSpellInitialize(); - } - - SetGroupUpdateFlag(GroupUpdatePetFlags.Full); - - if (GetPetType() == PetType.Hunter) - { - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_DECLINED_NAME); - stmt.AddValue(0, owner.GetGUID().GetCounter()); - stmt.AddValue(1, GetCharmInfo().GetPetNumber()); - result = DB.Characters.Query(stmt); - - if (!result.IsEmpty()) - { - _declinedname = new DeclinedName(); - for (byte i = 0; i < SharedConst.MaxDeclinedNameCases; ++i) - { - _declinedname.name[i] = result.Read(i); - } - } - } - //set last used pet number (for use in BG's) - if (owner.IsTypeId(TypeId.Player) && IsControlled() && !IsTemporarySummoned() && (GetPetType() == PetType.Summon || GetPetType() == PetType.Hunter)) - owner.ToPlayer().SetLastPetNumber(petId); + if (owner.IsPlayer() && IsControlled() && !IsTemporarySummoned() && (GetPetType() == PetType.Summon || GetPetType() == PetType.Hunter)) + owner.ToPlayer().SetLastPetNumber(petInfo.PetNumber); - // must be after SetMinion (owner guid check) - LoadTemplateImmunities(); - m_loading = false; + owner.GetSession().AddQueryHolderCallback(DB.Characters.DelayQueryHolder(new PetLoadQueryHolder(ownerid, petInfo.PetNumber))) + .AfterComplete(holder => + { + var session = owner.GetSession(); + var lastSaveTime = petInfo.LastSaveTime; + var specializationId = petInfo.SpecializationId; + if (session.GetPlayer() != owner || owner.GetPet() != this) + return; + + // passing previous checks ensure that 'this' is still valid + if (m_removed) + return; + + uint timediff = (uint)(GameTime.GetGameTime() - lastSaveTime); + _LoadAuras(holder.GetResult(PetLoginQueryLoad.Auras), holder.GetResult(PetLoginQueryLoad.AuraEffects), timediff); + + // load action bar, if data broken will fill later by default spells. + if (!isTemporarySummon) + { + _LoadSpells(holder.GetResult(PetLoginQueryLoad.Spells)); + GetSpellHistory().LoadFromDB(holder.GetResult(PetLoginQueryLoad.Cooldowns), holder.GetResult(PetLoginQueryLoad.Charges)); + LearnPetPassives(); + InitLevelupSpellsForLevel(); + if (GetMap().IsBattleArena()) + RemoveArenaAuras(); + + CastPetAuras(current); + } + + Log.outDebug(LogFilter.Pet, $"New Pet has {GetGUID()}"); + + ushort specId = specializationId; + var petSpec = CliDB.ChrSpecializationStorage.LookupByKey(specId); + if (petSpec != null) + specId = (ushort)Global.DB2Mgr.GetChrSpecializationByIndex(owner.HasAuraType(AuraType.OverridePetSpecs) ? Class.Max : 0, petSpec.OrderIndex).Id; + + SetSpecialization(specId); + + // The SetSpecialization function will run these functions if the pet's spec is not 0 + if (GetSpecialization() == 0) + { + CleanupActionBar(); // remove unknown spells from action bar after load + + owner.PetSpellInitialize(); + } + + + SetGroupUpdateFlag(GroupUpdatePetFlags.Full); + + if (GetPetType() == PetType.Hunter) + { + SQLResult result = holder.GetResult(PetLoginQueryLoad.DeclinedNames); + if (!result.IsEmpty()) + { + _declinedname = new(); + for (byte i = 0; i < SharedConst.MaxDeclinedNameCases; ++i) + _declinedname.name[i] = result.Read(i); + } + } + + // must be after SetMinion (owner guid check) + LoadTemplateImmunities(); + m_loading = false; + }); return true; } @@ -421,16 +433,6 @@ namespace Game.Entities stmt.AddValue(0, GetCharmInfo().GetPetNumber()); trans.Append(stmt); - // prevent duplicate using slot (except PET_SAVE_NOT_IN_SLOT) - if (mode <= PetSaveMode.LastStableSlot) - { - stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_SLOT); - stmt.AddValue(0, (byte)PetSaveMode.NotInSlot); - stmt.AddValue(1, ownerLowGUID); - stmt.AddValue(2, (byte)mode); - trans.Append(stmt); - } - // prevent existence another hunter pet in PET_SAVE_AS_CURRENT and PET_SAVE_NOT_IN_SLOT if (GetPetType() == PetType.Hunter && (mode == PetSaveMode.AsCurrent || mode > PetSaveMode.LastStableSlot)) { @@ -442,6 +444,11 @@ namespace Game.Entities } // save pet + string actionBar = GenerateActionBarData(); + + Cypher.Assert(owner.GetPetStable().CurrentPet != null && owner.GetPetStable().CurrentPet.PetNumber == GetCharmInfo().GetPetNumber()); + FillPetInfo(owner.GetPetStable().CurrentPet); + stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_PET); stmt.AddValue(0, GetCharmInfo().GetPetNumber()); stmt.AddValue(1, GetEntry()); @@ -455,11 +462,13 @@ namespace Game.Entities stmt.AddValue(9, HasPetFlag(UnitPetFlags.CanBeRenamed) ? 0 : 1); stmt.AddValue(10, curhealth); stmt.AddValue(11, curmana); - stmt.AddValue(12, GenerateActionBarData()); + + stmt.AddValue(12, actionBar); + stmt.AddValue(13, GameTime.GetGameTime()); stmt.AddValue(14, m_unitData.CreatedBySpell); stmt.AddValue(15, (byte)GetPetType()); - stmt.AddValue(16, m_petSpecialization); + stmt.AddValue(16, GetSpecialization()); trans.Append(stmt); DB.Characters.CommitTransaction(trans); @@ -472,36 +481,55 @@ namespace Game.Entities } } - public static void DeleteFromDB(uint guidlow) + public void FillPetInfo(PetStable.PetInfo petInfo) + { + petInfo.PetNumber = GetCharmInfo().GetPetNumber(); + petInfo.CreatureId = GetEntry(); + petInfo.DisplayId = GetNativeDisplayId(); + petInfo.Level = (byte)GetLevel(); + petInfo.Experience = m_unitData.PetExperience; + petInfo.ReactState = GetReactState(); + petInfo.Name = GetName(); + petInfo.WasRenamed = !HasPetFlag(UnitPetFlags.CanBeRenamed); + petInfo.Health = (uint)GetHealth(); + petInfo.Mana = (uint)GetPower(PowerType.Mana); + petInfo.ActionBar = GenerateActionBarData(); + petInfo.LastSaveTime = (uint)GameTime.GetGameTime(); + petInfo.CreatedBySpellId = m_unitData.CreatedBySpell; + petInfo.Type = GetPetType(); + petInfo.SpecializationId = GetSpecialization(); + } + + public static void DeleteFromDB(uint petNumber) { SQLTransaction trans = new(); PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_CHAR_PET_BY_ID); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_CHAR_PET_DECLINEDNAME); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_PET_AURA_EFFECTS); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_PET_AURAS); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_PET_SPELLS); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_PET_SPELL_COOLDOWNS); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_PET_SPELL_CHARGES); - stmt.AddValue(0, guidlow); + stmt.AddValue(0, petNumber); trans.Append(stmt); DB.Characters.CommitTransaction(trans); @@ -536,72 +564,73 @@ namespace Game.Entities switch (m_deathState) { case DeathState.Corpse: + { + if (GetPetType() != PetType.Hunter || m_corpseRemoveTime <= GameTime.GetGameTime()) { - if (GetPetType() != PetType.Hunter || m_corpseRemoveTime <= GameTime.GetGameTime()) - { - Remove(PetSaveMode.NotInSlot); //hunters' pets never get removed because of death, NEVER! - return; - } - break; + Remove(PetSaveMode.NotInSlot); //hunters' pets never get removed because of death, NEVER! + return; } + break; + } case DeathState.Alive: + { + // unsummon pet that lost owner + Player owner = GetOwner(); + if (owner == null || (!IsWithinDistInMap(owner, GetMap().GetVisibilityRange()) && !IsPossessed()) || (IsControlled() && owner.GetPetGUID().IsEmpty())) { - // unsummon pet that lost owner - Player owner = GetOwner(); - if (owner == null || (!IsWithinDistInMap(owner, GetMap().GetVisibilityRange()) && !IsPossessed()) || (IsControlled() && owner.GetPetGUID().IsEmpty())) + Remove(PetSaveMode.NotInSlot, true); + return; + } + + if (IsControlled()) + { + if (owner.GetPetGUID() != GetGUID()) { - Remove(PetSaveMode.NotInSlot, true); + Log.outError(LogFilter.Pet, $"Pet {GetEntry()} is not pet of owner {GetOwner().GetName()}, removed"); + Cypher.Assert(GetPetType() != PetType.Hunter, $"Unexpected unlinked pet found for owner {owner.GetSession().GetPlayerInfo()}"); + Remove(PetSaveMode.NotInSlot); return; } - - if (IsControlled()) - { - if (owner.GetPetGUID() != GetGUID()) - { - Log.outError(LogFilter.Pet, "Pet {0} is not pet of owner {1}, removed", GetEntry(), GetOwner().GetName()); - Remove(GetPetType() == PetType.Hunter ? PetSaveMode.AsDeleted : PetSaveMode.NotInSlot); - return; - } - } - - if (m_duration > 0) - { - if (m_duration > diff) - m_duration -= (int)diff; - else - { - Remove(GetPetType() != PetType.Summon ? PetSaveMode.AsDeleted : PetSaveMode.NotInSlot); - return; - } - } - - //regenerate focus for hunter pets or energy for deathknight's ghoul - if (m_focusRegenTimer != 0) - { - if (m_focusRegenTimer > diff) - m_focusRegenTimer -= diff; - else - { - switch (GetPowerType()) - { - case PowerType.Focus: - Regenerate(PowerType.Focus); - m_focusRegenTimer += PetFocusRegenInterval - diff; - if (m_focusRegenTimer == 0) - ++m_focusRegenTimer; - - // Reset if large diff (lag) causes focus to get 'stuck' - if (m_focusRegenTimer > PetFocusRegenInterval) - m_focusRegenTimer = PetFocusRegenInterval; - break; - default: - m_focusRegenTimer = 0; - break; - } - } - } - break; } + + if (m_duration > 0) + { + if (m_duration > diff) + m_duration -= (int)diff; + else + { + Remove(GetPetType() != PetType.Summon ? PetSaveMode.AsDeleted : PetSaveMode.NotInSlot); + return; + } + } + + //regenerate focus for hunter pets or energy for deathknight's ghoul + if (m_focusRegenTimer != 0) + { + if (m_focusRegenTimer > diff) + m_focusRegenTimer -= diff; + else + { + switch (GetPowerType()) + { + case PowerType.Focus: + Regenerate(PowerType.Focus); + m_focusRegenTimer += PetFocusRegenInterval - diff; + if (m_focusRegenTimer == 0) + ++m_focusRegenTimer; + + // Reset if large diff (lag) causes focus to get 'stuck' + if (m_focusRegenTimer > PetFocusRegenInterval) + m_focusRegenTimer = PetFocusRegenInterval; + break; + default: + m_focusRegenTimer = 0; + break; + } + } + } + break; + } default: break; } @@ -714,7 +743,7 @@ namespace Game.Entities bool CreateBaseAtTamed(CreatureTemplate cinfo, Map map) { Log.outDebug(LogFilter.Pet, "CreateBaseForTamed"); - if (!Create(map.GenerateLowGuid(HighGuid.Pet), map, cinfo.Entry)) + if (!Create(map.GenerateLowGuid(HighGuid.Pet), map, cinfo.Entry, Global.ObjectMgr.GeneratePetNumber())) return false; SetPetNameTimestamp(0); @@ -753,24 +782,8 @@ namespace Game.Entities return diet.HasAnyFlag(FoodMask); } - void _LoadSpellCooldowns() + void _LoadSpells(SQLResult result) { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL_COOLDOWN); - stmt.AddValue(0, GetCharmInfo().GetPetNumber()); - SQLResult cooldownsResult = DB.Characters.Query(stmt); - - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL_CHARGES); - SQLResult chargesResult = DB.Characters.Query(stmt); - - GetSpellHistory().LoadFromDB(cooldownsResult, chargesResult); - } - - void _LoadSpells() - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL); - stmt.AddValue(0, GetCharmInfo().GetPetNumber()); - SQLResult result = DB.Characters.Query(stmt); - if (!result.IsEmpty()) { do @@ -827,58 +840,52 @@ namespace Game.Entities } } - void _LoadAuras(uint timediff) + void _LoadAuras(SQLResult auraResult, SQLResult effectResult, uint timediff) { Log.outDebug(LogFilter.Pet, "Loading auras for {0}", GetGUID().ToString()); - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_AURA_EFFECT); - stmt.AddValue(0, GetCharmInfo().GetPetNumber()); - - ObjectGuid casterGuid = new(); - ObjectGuid itemGuid = new(); + ObjectGuid casterGuid = default; + ObjectGuid itemGuid = default; Dictionary effectInfo = new(); - SQLResult result = DB.Characters.Query(stmt); - if (!result.IsEmpty()) + + if (!effectResult.IsEmpty()) { do { - uint effectIndex = result.Read(3); + uint effectIndex = effectResult.Read(3); if (effectIndex < SpellConst.MaxEffects) { - casterGuid.SetRawValue(result.Read(0)); + casterGuid.SetRawValue(effectResult.Read(0)); if (casterGuid.IsEmpty()) casterGuid = GetGUID(); - AuraKey key = new(casterGuid, itemGuid, result.Read(1), result.Read(2)); + AuraKey key = new(casterGuid, itemGuid, effectResult.Read(1), effectResult.Read(2)); if (!effectInfo.ContainsKey(key)) effectInfo[key] = new AuraLoadEffectInfo(); var info = effectInfo[key]; - info.Amounts[effectIndex] = result.Read(4); - info.BaseAmounts[effectIndex] = result.Read(5); + info.Amounts[effectIndex] = effectResult.Read(4); + info.BaseAmounts[effectIndex] = effectResult.Read(5); } - } while (result.NextRow()); + } while (effectResult.NextRow()); } - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_AURA); - stmt.AddValue(0, GetCharmInfo().GetPetNumber()); - result = DB.Characters.Query(stmt); - if (!result.IsEmpty()) + if (!auraResult.IsEmpty()) { do { // NULL guid stored - pet is the caster of the spell - see Pet._SaveAuras - casterGuid.SetRawValue(result.Read(0)); + casterGuid.SetRawValue(auraResult.Read(0)); if (casterGuid.IsEmpty()) casterGuid = GetGUID(); - AuraKey key = new(casterGuid, itemGuid, result.Read(1), result.Read(2)); - uint recalculateMask = result.Read(3); - Difficulty difficulty = (Difficulty)result.Read(4); - byte stackCount = result.Read(5); - int maxDuration = result.Read(6); - int remainTime = result.Read(7); - byte remainCharges = result.Read(8); + AuraKey key = new(casterGuid, itemGuid, auraResult.Read(1), auraResult.Read(2)); + uint recalculateMask = auraResult.Read(3); + Difficulty difficulty = (Difficulty)auraResult.Read(4); + byte stackCount = auraResult.Read(5); + int maxDuration = auraResult.Read(6); + int remainTime = auraResult.Read(7); + byte remainCharges = auraResult.Read(8); SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(key.SpellId, difficulty); if (spellInfo == null) @@ -931,7 +938,7 @@ namespace Game.Entities Log.outInfo(LogFilter.Pet, "Added aura spellid {0}, effectmask {1}", spellInfo.Id, key.EffectMask); } } - while (result.NextRow()); + while (auraResult.NextRow()); } } @@ -1152,9 +1159,9 @@ namespace Game.Entities PetDefaultSpellsEntry defSpells = Global.SpellMgr.GetPetDefaultSpellsEntry((int)GetEntry()); if (defSpells != null) { - for (byte i = 0; i < SharedConst.MaxCreatureSpellDataSlots; ++i) + foreach (uint spellId in defSpells.spellid) { - SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(defSpells.spellid[i], Difficulty.None); + SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(spellId, Difficulty.None); if (spellInfo == null) continue; @@ -1334,17 +1341,18 @@ namespace Game.Entities } } - public bool Create(ulong guidlow, Map map, uint Entry) + public bool Create(ulong guidlow, Map map, uint entry, uint petNumber) { Cypher.Assert(map); SetMap(map); - _Create(ObjectGuid.Create(HighGuid.Pet, map.GetId(), Entry, guidlow)); + // TODO: counter should be constructed as (summon_count << 32) | petNumber + _Create(ObjectGuid.Create(HighGuid.Pet, map.GetId(), entry, guidlow)); m_spawnId = guidlow; - m_originalEntry = Entry; + m_originalEntry = entry; - if (!InitEntry(Entry)) + if (!InitEntry(entry)) return false; // Force regen flag for player pets, just like we do for players themselves @@ -1379,8 +1387,8 @@ namespace Game.Entities // For general hunter pets skill 270 // Passive 01~10, Passive 00 (20782, not used), Ferocious Inspiration (34457) // Scale 01~03 (34902~34904, bonus from owner, not used) - foreach (var petSet in petStore) - AddSpell(petSet, ActiveStates.Decide, PetSpellState.New, PetSpellType.Family); + foreach (var spellId in petStore) + AddSpell(spellId, ActiveStates.Decide, PetSpellState.New, PetSpellType.Family); } } @@ -1420,10 +1428,9 @@ namespace Game.Entities // if the owner has that pet aura, return true foreach (var petAura in owner.m_petAuras) - { if (petAura.GetAura(GetEntry()) == aura.GetId()) return true; - } + return false; } @@ -1476,7 +1483,7 @@ namespace Game.Entities return base.GetNativeObjectScale(); } - + public override void SetDisplayId(uint modelId, float displayScale = 1f) { base.SetDisplayId(modelId, displayScale); @@ -1619,9 +1626,9 @@ namespace Game.Entities public override string GetDebugInfo() { - return $"{base.GetDebugInfo()}\nPetType: {GetPetType()}"; + return $"{base.GetDebugInfo()}\nPetType: {GetPetType()} PetNumber: {GetCharmInfo().GetPetNumber()}"; } - + public DeclinedName GetDeclinedNames() { return _declinedname; } public new Dictionary m_spells = new(); @@ -1644,6 +1651,38 @@ namespace Game.Entities public PetSpellType type; } + public class PetStable + { + public class PetInfo + { + public string Name; + public string ActionBar; + public uint PetNumber; + public uint CreatureId; + public uint DisplayId; + public uint Experience; + public uint Health; + public uint Mana; + public uint LastSaveTime; + public uint CreatedBySpellId; + public ushort SpecializationId; + public byte Level = 0; + public ReactStates ReactState; + public PetType Type = PetType.Max; + public bool WasRenamed; + } + + public PetInfo CurrentPet; // PET_SAVE_AS_CURRENT + public PetInfo[] StabledPets = new PetInfo[SharedConst.MaxPetStables]; // PET_SAVE_FIRST_STABLE_SLOT - PET_SAVE_LAST_STABLE_SLOT + public uint MaxStabledPets = 0; + public List UnslottedPets = new(); // PET_SAVE_NOT_IN_SLOT + + public PetInfo GetUnslottedHunterPet() + { + return UnslottedPets.Count == 1 && UnslottedPets[0].Type == PetType.Hunter ? UnslottedPets[0] : null; + } + } + public enum ActiveStates { Passive = 0x01, // 0x01 - passive @@ -1653,4 +1692,45 @@ namespace Game.Entities Reaction = 0x06, // 0x02 | 0x04 Decide = 0x00 // custom } + + enum PetLoginQueryLoad + { + DeclinedNames, + Auras, + AuraEffects, + Spells, + Cooldowns, + Charges + } + + class PetLoadQueryHolder : SQLQueryHolder + { + public PetLoadQueryHolder(ulong ownerGuid, uint petNumber) + { + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_DECLINED_NAME); + stmt.AddValue(0, ownerGuid); + stmt.AddValue(1, petNumber); + SetQuery(PetLoginQueryLoad.DeclinedNames, stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_AURA); + stmt.AddValue(0, petNumber); + SetQuery(PetLoginQueryLoad.Auras, stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_AURA_EFFECT); + stmt.AddValue(0, petNumber); + SetQuery(PetLoginQueryLoad.AuraEffects, stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL); + stmt.AddValue(0, petNumber); + SetQuery(PetLoginQueryLoad.Spells, stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL_COOLDOWN); + stmt.AddValue(0, petNumber); + SetQuery(PetLoginQueryLoad.Cooldowns, stmt); + + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SPELL_CHARGES); + stmt.AddValue(0, petNumber); + SetQuery(PetLoginQueryLoad.Charges, stmt); + } + } } diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index b71b7fd5f..b0172f77c 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -1538,6 +1538,53 @@ namespace Game.Entities m_bgData.taxiPath[1] = result.Read(8); m_bgData.mountSpell = result.Read(9); } + void _LoadPetStable(byte petStableSlots, SQLResult result) + { + if (petStableSlots == 0 && result.IsEmpty()) + return; + + m_petStable = new(); + m_petStable.MaxStabledPets = petStableSlots; + if (m_petStable.MaxStabledPets > SharedConst.MaxPetStables) + { + Log.outError(LogFilter.Player, $"Player::LoadFromDB: Player ({GetGUID()}) can't have more stable slots than {SharedConst.MaxPetStables}, but has {m_petStable.MaxStabledPets} in DB"); + m_petStable.MaxStabledPets = SharedConst.MaxPetStables; + } + + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + // SELECT id, entry, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization FROM character_pet WHERE owner = ? + if (!result.IsEmpty()) + { + do + { + PetStable.PetInfo petInfo = new(); + petInfo.PetNumber = result.Read(0); + petInfo.CreatureId = result.Read(1); + petInfo.DisplayId = result.Read(2); + petInfo.Level = result.Read(3); + petInfo.Experience = result.Read(4); + petInfo.ReactState = (ReactStates)result.Read(5); + PetSaveMode slot = (PetSaveMode)result.Read(6); + petInfo.Name = result.Read(7); + petInfo.WasRenamed = result.Read(8); + petInfo.Health = result.Read(9); + petInfo.Mana = result.Read(10); + petInfo.ActionBar = result.Read(11); + petInfo.LastSaveTime = result.Read(12); + petInfo.CreatedBySpellId = result.Read(13); + petInfo.Type = (PetType)result.Read(14); + petInfo.SpecializationId = result.Read(15); + if (slot == PetSaveMode.AsCurrent) + m_petStable.CurrentPet = petInfo; + else if (slot >= PetSaveMode.FirstStableSlot && slot <= PetSaveMode.LastStableSlot) + m_petStable.StabledPets[(int)slot - 1] = petInfo; + else if (slot == PetSaveMode.NotInSlot) + m_petStable.UnslottedPets.Add(petInfo); + + } while (result.NextRow()); + } + } + void _SaveInventory(SQLTransaction trans) { @@ -3039,12 +3086,7 @@ namespace Game.Entities m_taxi.LoadTaxiMask(taximask); // must be before InitTaxiNodesForLevel - m_stableSlots = stable_slots; - if (m_stableSlots > 4) - { - Log.outError(LogFilter.Player, "Player can have not more {0} stable slots, but have in DB {1}", 4, m_stableSlots); - m_stableSlots = 4; - } + _LoadPetStable(stable_slots, holder.GetResult(PlayerLoginQueryLoad.PetSlots)); // Honor system // Update Honor kills data @@ -3411,7 +3453,7 @@ namespace Game.Entities stmt.AddValue(index++, GetTalentResetTime()); stmt.AddValue(index++, GetPrimarySpecialization()); stmt.AddValue(index++, (ushort)m_ExtraFlags); - stmt.AddValue(index++, m_stableSlots); + stmt.AddValue(index++, m_petStable != null ? m_petStable.MaxStabledPets : 0); stmt.AddValue(index++, (ushort)atLoginFlags); stmt.AddValue(index++, m_deathExpireTime); @@ -3551,7 +3593,7 @@ namespace Game.Entities stmt.AddValue(index++, GetNumRespecs()); stmt.AddValue(index++, GetPrimarySpecialization()); stmt.AddValue(index++, (ushort)m_ExtraFlags); - stmt.AddValue(index++, m_stableSlots); + stmt.AddValue(index++, m_petStable != null ? m_petStable.MaxStabledPets : 0); stmt.AddValue(index++, (ushort)atLoginFlags); stmt.AddValue(index++, GetZoneId()); stmt.AddValue(index++, m_deathExpireTime); @@ -3937,7 +3979,7 @@ namespace Game.Entities // Unsummon and delete for pets in world is not required: player deleted from CLI or character list with not loaded pet. // NOW we can finally clear other DB data related to character - stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PETS); + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PET_IDS); stmt.AddValue(0, guid); SQLResult resultPets = DB.Characters.Query(stmt); diff --git a/Source/Game/Entities/Player/Player.Fields.cs b/Source/Game/Entities/Player/Player.Fields.cs index 5a2ba24e6..cfeb52d0f 100644 --- a/Source/Game/Entities/Player/Player.Fields.cs +++ b/Source/Game/Entities/Player/Player.Fields.cs @@ -130,8 +130,8 @@ namespace Game.Entities public bool m_mailsUpdated; //Pets + PetStable m_petStable; public List m_petAuras = new(); - public uint m_stableSlots; uint m_temporaryUnsummonedPetNumber; uint m_lastpetnumber; diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index c3344898b..38c238e8e 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -884,7 +884,16 @@ namespace Game.Entities public void SetCommandStatusOff(PlayerCommandStates command) { _activeCheats &= ~command; } //Pet - Summons - Vehicles + public PetStable GetPetStable() { return m_petStable; } + public PetStable GetOrInitPetStable() + { + if (m_petStable == null) + m_petStable = new(); + + return m_petStable; + } + // last used pet number (for BG's) public uint GetLastPetNumber() { return m_lastpetnumber; } public void SetLastPetNumber(uint petnumber) { m_lastpetnumber = petnumber; } @@ -892,7 +901,7 @@ namespace Game.Entities { //fixme: the pet should still be loaded if the player is not in world // just not added to the map - if (IsInWorld) + if (m_petStable != null && IsInWorld) { Pet pet = new(this); pet.LoadPetFromDB(this, 0, 0, true); @@ -4702,18 +4711,13 @@ namespace Game.Entities return null; } - public Pet SummonPet(uint entry, float x, float y, float z, float ang, PetType petType, uint duration, bool aliveOnly = false) + public Pet SummonPet(uint entry, float x, float y, float z, float ang, PetType petType, uint duration) { - Pet pet = new(this, petType); - if (petType == PetType.Summon && pet.LoadPetFromDB(this, entry)) - { - if (aliveOnly && !pet.IsAlive()) - { - pet.DespawnOrUnsummon(); - SendTameFailure(PetTameResult.Dead); - return null; - } + PetStable petStable = GetOrInitPetStable(); + Pet pet = new(this, petType); + if (petType == PetType.Summon && pet.LoadPetFromDB(this, entry, 0, false)) + { if (duration > 0) pet.SetDuration(duration); @@ -4733,8 +4737,8 @@ namespace Game.Entities } Map map = GetMap(); - uint pet_number = Global.ObjectMgr.GeneratePetNumber(); - if (!pet.Create(map.GenerateLowGuid(HighGuid.Pet), map, entry)) + uint petNumber = Global.ObjectMgr.GeneratePetNumber(); + if (!pet.Create(map.GenerateLowGuid(HighGuid.Pet), map, entry, petNumber)) { Log.outError(LogFilter.Server, "no such creature entry {0}", entry); return null; @@ -4750,11 +4754,15 @@ namespace Game.Entities SetMinion(pet, true); + Cypher.Assert(petStable.CurrentPet == null && (petType != PetType.Hunter || petStable.GetUnslottedHunterPet() == null)); + petStable.CurrentPet = new(); + pet.FillPetInfo(petStable.CurrentPet); + switch (petType) { case PetType.Summon: // this enables pet details window (Shift+P) - pet.GetCharmInfo().SetPetNumber(pet_number, true); + pet.GetCharmInfo().SetPetNumber(petNumber, true); pet.SetClass(Class.Mage); pet.SetPetExperience(0); pet.SetPetNextLevelExperience(1000); @@ -4848,6 +4856,17 @@ namespace Game.Entities // only if current pet in slot pet.SavePetToDB(mode); + Cypher.Assert(m_petStable.CurrentPet != null && m_petStable.CurrentPet.PetNumber == pet.GetCharmInfo().GetPetNumber()); + if (mode == PetSaveMode.NotInSlot) + { + m_petStable.UnslottedPets.Add(m_petStable.CurrentPet); + m_petStable.CurrentPet = null; + } + else if (mode == PetSaveMode.AsDeleted) + m_petStable.CurrentPet = null; + // else if (stable slots) handled in opcode handlers due to required swaps + // else (current pet) doesnt need to do anything + SetMinion(pet, false); pet.AddObjectToRemoveList(); @@ -4862,7 +4881,7 @@ namespace Game.Entities } } - void SendTameFailure(PetTameResult result) + public void SendTameFailure(PetTameResult result) { PetTameFailure petTameFailure = new(); petTameFailure.Result = (byte)result; diff --git a/Source/Game/Entities/Unit/Unit.Pets.cs b/Source/Game/Entities/Unit/Unit.Pets.cs index 96c9acdd4..34c2712aa 100644 --- a/Source/Game/Entities/Unit/Unit.Pets.cs +++ b/Source/Game/Entities/Unit/Unit.Pets.cs @@ -730,7 +730,11 @@ namespace Game.Entities uint level = creatureTarget.GetLevelForTarget(this) + 5 < GetLevel() ? (GetLevel() - 5) : creatureTarget.GetLevelForTarget(this); - InitTamedPet(pet, level, spell_id); + if (!InitTamedPet(pet, level, spell_id)) + { + pet.Dispose(); + return null; + } return pet; } @@ -754,6 +758,11 @@ namespace Game.Entities bool InitTamedPet(Pet pet, uint level, uint spell_id) { + Player player = ToPlayer(); + PetStable petStable = player.GetOrInitPetStable(); + if (petStable.CurrentPet != null || petStable.GetUnslottedHunterPet() != null) + return false; + pet.SetCreatorGUID(GetGUID()); pet.SetFaction(GetFaction()); pet.SetCreatedBySpell(spell_id); @@ -773,6 +782,9 @@ namespace Game.Entities // this enables pet details window (Shift+P) pet.InitPetCreateSpells(); pet.SetFullHealth(); + + petStable.CurrentPet = new(); + pet.FillPetInfo(petStable.CurrentPet); return true; } diff --git a/Source/Game/Handlers/CharacterHandler.cs b/Source/Game/Handlers/CharacterHandler.cs index 79e3e1994..9c1bbafdf 100644 --- a/Source/Game/Handlers/CharacterHandler.cs +++ b/Source/Game/Handlers/CharacterHandler.cs @@ -2820,6 +2820,10 @@ namespace Game stmt.AddValue(0, lowGuid); SetQuery(PlayerLoginQueryLoad.CorpseLocation, stmt); + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHAR_PETS); + stmt.AddValue(0, lowGuid); + SetQuery(PlayerLoginQueryLoad.PetSlots, stmt); + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHARACTER_GARRISON); stmt.AddValue(0, lowGuid); SetQuery(PlayerLoginQueryLoad.Garrison, stmt); @@ -2938,6 +2942,7 @@ namespace Game Currency, CufProfiles, CorpseLocation, + PetSlots, Garrison, GarrisonBlueprints, GarrisonBuildings, diff --git a/Source/Game/Handlers/NPCHandler.cs b/Source/Game/Handlers/NPCHandler.cs index a86fb1dfc..1911487bd 100644 --- a/Source/Game/Handlers/NPCHandler.cs +++ b/Source/Game/Handlers/NPCHandler.cs @@ -343,59 +343,68 @@ namespace Game public void SendStablePet(ObjectGuid guid) { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_PET_SLOTS_DETAIL); - stmt.AddValue(0, guid.GetCounter()); - stmt.AddValue(1, (byte)PetSaveMode.FirstStableSlot); - stmt.AddValue(2, (byte)PetSaveMode.LastStableSlot); - - _queryProcessor.AddCallback(DB.Characters.AsyncQuery(stmt).WithCallback(SendStablePetCallback, guid)); - } - - void SendStablePetCallback(ObjectGuid guid, SQLResult result) - { - if (!GetPlayer()) - return; - PetStableList packet = new(); packet.StableMaster = guid; - Pet pet = GetPlayer().GetPet(); + PetStable petStable = GetPlayer().GetPetStable(); + if (petStable == null) + { + SendPacket(packet); + return; + } uint petSlot = 0; - // not let move dead pet in slot - if (pet && pet.IsAlive() && pet.GetPetType() == PetType.Hunter) + if (petStable.CurrentPet != null) { - PetStableInfo stableEntry;// = new PetStableInfo(); + PetStable.PetInfo pet = petStable.CurrentPet; + + PetStableInfo stableEntry; stableEntry.PetSlot = petSlot; - stableEntry.PetNumber = pet.GetCharmInfo().GetPetNumber(); - stableEntry.CreatureID = pet.GetEntry(); - stableEntry.DisplayID = pet.GetDisplayId(); - stableEntry.ExperienceLevel = pet.GetLevel(); + stableEntry.PetNumber = pet.PetNumber; + stableEntry.CreatureID = pet.CreatureId; + stableEntry.DisplayID = pet.DisplayId; + stableEntry.ExperienceLevel = pet.Level; stableEntry.PetFlags = PetStableinfo.Active; - stableEntry.PetName = pet.GetName(); + stableEntry.PetName = pet.Name; ++petSlot; packet.Pets.Add(stableEntry); } - - if (!result.IsEmpty()) + else { - do + PetStable.PetInfo pet = petStable.GetUnslottedHunterPet(); + if (pet != null) { - PetStableInfo stableEntry;// = new PetStableInfo(); - + PetStableInfo stableEntry; stableEntry.PetSlot = petSlot; - stableEntry.PetNumber = result.Read(1); // petnumber - stableEntry.CreatureID = result.Read(2); // creature entry - stableEntry.DisplayID = result.Read(5); // creature displayid - stableEntry.ExperienceLevel = result.Read(3); // level - stableEntry.PetFlags = PetStableinfo.Inactive; - stableEntry.PetName = result.Read(4); // Name + stableEntry.PetNumber = pet.PetNumber; + stableEntry.CreatureID = pet.CreatureId; + stableEntry.DisplayID = pet.DisplayId; + stableEntry.ExperienceLevel = pet.Level; + stableEntry.PetFlags = PetStableinfo.Active; + stableEntry.PetName = pet.Name; + + ++petSlot; + packet.Pets.Add(stableEntry); + } + } + + foreach (var pet in petStable.StabledPets) + { + if (pet != null) + { + PetStableInfo stableEntry; + stableEntry.PetSlot = petSlot; + stableEntry.PetNumber = pet.PetNumber; + stableEntry.CreatureID = pet.CreatureId; + stableEntry.DisplayID = pet.DisplayId; + stableEntry.ExperienceLevel = pet.Level; + stableEntry.PetFlags = PetStableinfo.Inactive; + stableEntry.PetName = pet.Name; ++petSlot; packet.Pets.Add(stableEntry); } - while (result.NextRow()); } SendPacket(packet); diff --git a/Source/Game/Handlers/PetHandler.cs b/Source/Game/Handlers/PetHandler.cs index cde86132f..61ab785bc 100644 --- a/Source/Game/Handlers/PetHandler.cs +++ b/Source/Game/Handlers/PetHandler.cs @@ -536,10 +536,12 @@ namespace Game bool isdeclined = packet.RenameData.HasDeclinedNames; string name = packet.RenameData.NewName; + PetStable petStable = _player.GetPetStable(); Pet pet = ObjectAccessor.GetPet(GetPlayer(), petguid); // check it! if (!pet || !pet.IsPet() || pet.ToPet().GetPetType() != PetType.Hunter || !pet.HasPetFlag(UnitPetFlags.CanBeRenamed) || - pet.GetOwnerGUID() != GetPlayer().GetGUID() || pet.GetCharmInfo() == null) + pet.GetOwnerGUID() != _player.GetGUID() || pet.GetCharmInfo() == null || + petStable == null || petStable.CurrentPet == null || petStable.CurrentPet.PetNumber != pet.GetCharmInfo().GetPetNumber()) return; PetNameInvalidReason res = ObjectManager.CheckPetName(name); @@ -559,6 +561,9 @@ namespace Game pet.SetGroupUpdateFlag(GroupUpdatePetFlags.Name); pet.RemovePetFlag(UnitPetFlags.CanBeRenamed); + petStable.CurrentPet.Name = name; + petStable.CurrentPet.WasRenamed = true; + PreparedStatement stmt; SQLTransaction trans = new(); if (isdeclined) diff --git a/Source/Game/Server/WorldSession.cs b/Source/Game/Server/WorldSession.cs index 338becaba..dfed3f108 100644 --- a/Source/Game/Server/WorldSession.cs +++ b/Source/Game/Server/WorldSession.cs @@ -713,7 +713,7 @@ namespace Game return _transactionCallbacks.AddCallback(callback); } - SQLQueryHolderCallback AddQueryHolderCallback(SQLQueryHolderCallback callback) + public SQLQueryHolderCallback AddQueryHolderCallback(SQLQueryHolderCallback callback) { return (SQLQueryHolderCallback)_queryHolderProcessor.AddCallback(callback); } diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index dd29d1384..5a95397bb 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -5217,6 +5217,35 @@ namespace Game.Spells if (!unitCaster.GetCharmedGUID().IsEmpty()) return SpellCastResult.AlreadyHaveCharm; + + Player playerCaster = unitCaster.ToPlayer(); + if (playerCaster != null && playerCaster.GetPetStable() != null) + { + var info = Pet.GetLoadPetInfo(playerCaster.GetPetStable(), (uint)spellEffectInfo.MiscValue, 0, false); + if (info.Item1 == null) + { + playerCaster.SendTameFailure(PetTameResult.NoPetAvailable); + return SpellCastResult.DontReport; + } + + if (info.Item1.Type == PetType.Hunter && info.Item1.Health == 0) + { + playerCaster.SendTameFailure(PetTameResult.Dead); + return SpellCastResult.DontReport; + } + + CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(info.Item1.CreatureId); + if (creatureInfo == null || !creatureInfo.IsTameable(playerCaster.CanTameExoticPets())) + { + // if problem in exotic pet + if (creatureInfo != null && creatureInfo.IsTameable(true)) + playerCaster.SendTameFailure(PetTameResult.CantControlExotic); + else + playerCaster.SendTameFailure(PetTameResult.NoPetAvailable); + + return SpellCastResult.DontReport; + } + } break; } case SpellEffectName.SummonPlayer: diff --git a/Source/Game/Spells/SpellEffects.cs b/Source/Game/Spells/SpellEffects.cs index 347cc07ca..0c3b08b4a 100644 --- a/Source/Game/Spells/SpellEffects.cs +++ b/Source/Game/Spells/SpellEffects.cs @@ -2293,14 +2293,14 @@ namespace Game.Spells } if (owner.IsTypeId(TypeId.Player)) - owner.ToPlayer().RemovePet(OldSummon, (OldSummon.GetPetType() == PetType.Hunter ? PetSaveMode.AsDeleted : PetSaveMode.NotInSlot), false); + owner.ToPlayer().RemovePet(OldSummon, PetSaveMode.NotInSlot, false); else return; } float x, y, z; owner.GetClosePoint(out x, out y, out z, owner.GetCombatReach()); - Pet pet = owner.SummonPet(petentry, x, y, z, owner.Orientation, PetType.Summon, 0, true); + Pet pet = owner.SummonPet(petentry, x, y, z, owner.Orientation, PetType.Summon, 0); if (!pet) return; diff --git a/Source/Scripts/Spells/Hunter.cs b/Source/Scripts/Spells/Hunter.cs index e378bfeee..17c92fe87 100644 --- a/Source/Scripts/Spells/Hunter.cs +++ b/Source/Scripts/Spells/Hunter.cs @@ -407,8 +407,8 @@ namespace Scripts.Spells.Hunter { SpellCastResult CheckCast() { - Unit caster = GetCaster(); - if (!caster.IsTypeId(TypeId.Player)) + Player caster = GetCaster().ToPlayer(); + if (caster == null) return SpellCastResult.DontReport; if (!GetExplTargetUnit()) @@ -421,11 +421,21 @@ namespace Scripts.Spells.Hunter return SpellCastResult.Highlevel; // use SMSG_PET_TAME_FAILURE? - if (!target.GetCreatureTemplate().IsTameable(caster.ToPlayer().CanTameExoticPets())) + if (!target.GetCreatureTemplate().IsTameable(caster.CanTameExoticPets())) return SpellCastResult.BadTargets; - if (!caster.GetPetGUID().IsEmpty()) - return SpellCastResult.AlreadyHaveSummon; + PetStable petStable = caster.GetPetStable(); + if (petStable != null) + { + if (petStable.CurrentPet != null) + return SpellCastResult.AlreadyHaveSummon; + + if (petStable.GetUnslottedHunterPet() != null) + { + caster.SendTameFailure(PetTameResult.TooMany); + return SpellCastResult.DontReport; + } + } if (!caster.GetCharmedGUID().IsEmpty()) return SpellCastResult.AlreadyHaveCharm;