diff --git a/Source/Framework/Constants/PetConst.cs b/Source/Framework/Constants/PetConst.cs index f89a9c57b..dc0e6557d 100644 --- a/Source/Framework/Constants/PetConst.cs +++ b/Source/Framework/Constants/PetConst.cs @@ -34,11 +34,13 @@ namespace Framework.Constants public enum PetSaveMode { - NotInSlot = -1, // for avoid conflict with stable size grow will use negative value AsDeleted = -2, // not saved in fact - AsCurrent = 0, // in current slot (with player) - FirstStableSlot = 1, - LastStableSlot = SharedConst.MaxPetStables, // last in DB stable slot index (including), all higher have same meaning as PET_SAVE_NOT_IN_SLOT + AsCurrent = -3, // in current slot (with player) + FirstActiveSlot = 0, + LastActiveSlot = FirstActiveSlot + SharedConst.MaxActivePets, + FirstStableSlot = 5, + LastStableSlot = FirstStableSlot + SharedConst.MaxPetStables, // last in DB stable slot index + NotInSlot = -1, // for avoid conflict with stable size grow will use negative value } public enum PetSpellState diff --git a/Source/Framework/Constants/SharedConst.cs b/Source/Framework/Constants/SharedConst.cs index 404433b3a..e917c0172 100644 --- a/Source/Framework/Constants/SharedConst.cs +++ b/Source/Framework/Constants/SharedConst.cs @@ -219,7 +219,9 @@ namespace Framework.Constants /// /// Pet Const /// - public const int MaxPetStables = 4; + public const int MaxActivePets = 5; + public const int MaxPetStables = 200; + public const uint CallPetSpellId = 883; public const float PetFollowDist = 1.0f; public const float PetFollowAngle = MathF.PI; public const int MaxSpellCharm = 4; @@ -434,6 +436,16 @@ namespace Framework.Constants { return raceId < Race.Max && raceBits[(int)raceId] >= 0 && raceBits[(int)raceId] < 64 ? (1 << raceBits[(int)raceId]) : 0; } + + public static bool IsActivePetSlot(PetSaveMode slot) + { + return slot >= PetSaveMode.FirstActiveSlot && slot < PetSaveMode.LastActiveSlot; + } + + public static bool IsStabledPetSlot(PetSaveMode slot) + { + return slot >= PetSaveMode.FirstStableSlot && slot < PetSaveMode.LastStableSlot; + } } public enum Locale diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index cd87c0cf1..5e24dbc7f 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -721,7 +721,6 @@ namespace Framework.Database PrepareStatement(CharStatements.UPD_CHAR_PET_NAME, "UPDATE character_pet SET name = ?, renamed = 1 WHERE owner = ? AND id = ?"); 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 IN (?, ?)"); PrepareStatement(CharStatements.DEL_ALL_PET_SPELLS_BY_OWNER, "DELETE FROM pet_spell WHERE guid in (SELECT id FROM character_pet WHERE owner=?)"); PrepareStatement(CharStatements.UPD_PET_SPECS_BY_OWNER, "UPDATE character_pet SET specialization = 0 WHERE owner=?"); PrepareStatement(CharStatements.INS_PET, "INSERT INTO character_pet (id, entry, owner, modelid, level, exp, Reactstate, slot, name, renamed, curhealth, curmana, abdata, savetime, CreatedBySpell, PetType, specialization) " + @@ -1353,7 +1352,6 @@ namespace Framework.Database UPD_CHAR_PET_NAME, UPD_CHAR_PET_SLOT_BY_ID, DEL_CHAR_PET_BY_ID, - DEL_CHAR_PET_BY_SLOT, DEL_ALL_PET_SPELLS_BY_OWNER, UPD_PET_SPECS_BY_OWNER, INS_PET, diff --git a/Source/Game/Entities/Pet.cs b/Source/Game/Entities/Pet.cs index e76ac4c44..5e39c4a37 100644 --- a/Source/Game/Entities/Pet.cs +++ b/Source/Game/Entities/Pet.cs @@ -96,13 +96,14 @@ namespace Game.Entities } } - public static Tuple GetLoadPetInfo(PetStable stable, uint petEntry, uint petnumber, bool current) + public static Tuple GetLoadPetInfo(PetStable stable, uint petEntry, uint petnumber, PetSaveMode? slot) { if (petnumber != 0) { // Known petnumber entry - if (stable.CurrentPet != null && stable.CurrentPet.PetNumber == petnumber) - return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + for (var activeSlot = 0; activeSlot < stable.ActivePets.Length; ++activeSlot) + if (stable.ActivePets[activeSlot] != null && stable.ActivePets[activeSlot].PetNumber == petnumber) + return Tuple.Create(stable.ActivePets[activeSlot], PetSaveMode.FirstActiveSlot + activeSlot); for (var stableSlot = 0; stableSlot < stable.StabledPets.Length; ++stableSlot) if (stable.StabledPets[stableSlot] != null && stable.StabledPets[stableSlot].PetNumber == petnumber) @@ -112,17 +113,24 @@ namespace Game.Entities if (pet.PetNumber == petnumber) return Tuple.Create(pet, PetSaveMode.NotInSlot); } - else if (current) + else if (slot.HasValue) { - // Current pet (slot 0) - if (stable.CurrentPet != null) - return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + // Current pet + if (slot == PetSaveMode.AsCurrent) + if (stable.GetCurrentActivePetIndex().HasValue && stable.ActivePets[stable.GetCurrentActivePetIndex().Value] != null) + return Tuple.Create(stable.ActivePets[stable.GetCurrentActivePetIndex().Value], (PetSaveMode)stable.GetCurrentActivePetIndex()); + + if (slot >= PetSaveMode.FirstActiveSlot && slot < PetSaveMode.LastActiveSlot) + if (stable.ActivePets[(int)slot.Value] != null) + return Tuple.Create(stable.ActivePets[(int)slot.Value], slot.Value); + + if (slot >= PetSaveMode.FirstStableSlot && slot < PetSaveMode.LastStableSlot) + if (stable.StabledPets[(int)slot.Value] != null) + return Tuple.Create(stable.StabledPets[(int)slot.Value], slot.Value); } else if (petEntry != 0) { // known petEntry entry (unique for summoned pet, but non unique for hunter pet (only from current or not stabled pets) - 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) @@ -131,8 +139,8 @@ namespace Game.Entities else { // Any current or other non-stabled pet (for hunter "call pet") - if (stable.CurrentPet != null) - return Tuple.Create(stable.CurrentPet, PetSaveMode.AsCurrent); + if (stable.ActivePets[0] != null) + return Tuple.Create(stable.ActivePets[0], PetSaveMode.FirstActiveSlot); if (!stable.UnslottedPets.Empty()) return Tuple.Create(stable.UnslottedPets.First(), PetSaveMode.NotInSlot); @@ -141,22 +149,22 @@ namespace Game.Entities return Tuple.Create(null, PetSaveMode.AsDeleted); } - public bool LoadPetFromDB(Player owner, uint petEntry = 0, uint petnumber = 0, bool current = false) + public bool LoadPetFromDB(Player owner, uint petEntry = 0, uint petnumber = 0, bool current = false, PetSaveMode? forcedSlot = null) { 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) + (PetStable.PetInfo petInfo, PetSaveMode slot) = GetLoadPetInfo(petStable, petEntry, petnumber, forcedSlot); + if (petInfo == null || (slot >= PetSaveMode.FirstStableSlot && slot < PetSaveMode.LastStableSlot)) { m_loading = false; return false; } // Don't try to reload the current pet - if (petStable.CurrentPet != null && owner.GetPet() != null && petStable.CurrentPet.PetNumber == petInfo.PetNumber) + if (petStable.GetCurrentPet() != null && owner.GetPet() != null && petStable.GetCurrentPet().PetNumber == petInfo.PetNumber) return false; SpellInfo spellInfo = Global.SpellMgr.GetSpellInfo(petInfo.CreatedBySpellId, owner.GetMap().GetDifficultyID()); @@ -273,36 +281,28 @@ namespace Game.Entities } // set current pet as current - // 0=current - // 1..MAX_PET_STABLES in stable slot - // PET_SAVE_NOT_IN_SLOT(100) = not stable slot (summoning)) + // 0-4=current + // PET_SAVE_NOT_IN_SLOT(-1) = not stable slot (summoning)) if (slot == PetSaveMode.NotInSlot) { uint petInfoNumber = petInfo.PetNumber; - if (petStable.CurrentPet != null) + if (petStable.CurrentPetIndex != 0) owner.RemovePet(null, PetSaveMode.NotInSlot); - var unslottedPetInfo = petStable.UnslottedPets.Find(unslottedPet => unslottedPet.PetNumber == petInfoNumber); - Cypher.Assert(petStable.CurrentPet == null); - Cypher.Assert(unslottedPetInfo != null); + var unslottedPetIndex = petStable.UnslottedPets.FindIndex(unslottedPet => unslottedPet.PetNumber == petInfoNumber); + Cypher.Assert(petStable.CurrentPetIndex == 0); + Cypher.Assert(unslottedPetIndex != -1); - petStable.CurrentPet = unslottedPetInfo; - petStable.UnslottedPets.Remove(unslottedPetInfo); - - // old petInfo is no longer valid, refresh it - petInfo = petStable.CurrentPet; + petStable.SetCurrentUnslottedPetIndex((uint)unslottedPetIndex); } - else if (PetSaveMode.FirstStableSlot <= slot && slot <= PetSaveMode.LastStableSlot) + else if (PetSaveMode.FirstActiveSlot <= slot && slot <= PetSaveMode.LastActiveSlot) { - var index = Array.FindIndex(petStable.StabledPets, pet => pet?.PetNumber == petnumber); - var stabledPet = petStable.StabledPets[index]; - Cypher.Assert(index != -1); + var activePetIndex = Array.FindIndex(petStable.ActivePets, pet => pet?.PetNumber == petnumber); - petStable.StabledPets[index] = petStable.CurrentPet; - petStable.CurrentPet = stabledPet; + Cypher.Assert(petStable.CurrentPetIndex == 0); + Cypher.Assert(activePetIndex != -1); - // old petInfo pointer is no longer valid, refresh it - petInfo = petStable.CurrentPet; + petStable.SetCurrentActivePetIndex((uint)activePetIndex); } // Send fake summon spell cast - this is needed for correct cooldown application for spells @@ -437,8 +437,15 @@ namespace Game.Entities // save auras before possibly removing them _SaveAuras(trans); + if (mode == PetSaveMode.AsCurrent) + { + var activeSlot = owner.GetPetStable().GetCurrentActivePetIndex(); + if (activeSlot.HasValue) + mode = (PetSaveMode)activeSlot; + } + // stable and not in slot saves - if (mode > PetSaveMode.AsCurrent) + if (mode < PetSaveMode.FirstActiveSlot || mode >= PetSaveMode.LastActiveSlot) RemoveAllAuras(); _SaveSpells(trans); @@ -446,7 +453,7 @@ namespace Game.Entities DB.Characters.CommitTransaction(trans); // current/stable/not_in_slot - if (mode >= PetSaveMode.AsCurrent) + if (mode != PetSaveMode.AsDeleted) { ulong ownerLowGUID = GetOwnerGUID().GetCounter(); trans = new SQLTransaction(); @@ -456,21 +463,11 @@ namespace Game.Entities stmt.AddValue(0, GetCharmInfo().GetPetNumber()); 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)) - { - stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_CHAR_PET_BY_SLOT); - stmt.AddValue(0, ownerLowGUID); - stmt.AddValue(1, (short)mode); - stmt.AddValue(2, (short)PetSaveMode.NotInSlot); - trans.Append(stmt); - } - // save pet string actionBar = GenerateActionBarData(); - Cypher.Assert(owner.GetPetStable().CurrentPet != null && owner.GetPetStable().CurrentPet.PetNumber == GetCharmInfo().GetPetNumber()); - FillPetInfo(owner.GetPetStable().CurrentPet); + Cypher.Assert(owner.GetPetStable().GetCurrentPet() != null && owner.GetPetStable().GetCurrentPet().PetNumber == GetCharmInfo().GetPetNumber()); + FillPetInfo(owner.GetPetStable().GetCurrentPet()); stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_PET); stmt.AddValue(0, GetCharmInfo().GetPetNumber()); @@ -480,7 +477,7 @@ namespace Game.Entities stmt.AddValue(4, GetLevel()); stmt.AddValue(5, m_unitData.PetExperience); stmt.AddValue(6, (byte)GetReactState()); - stmt.AddValue(7, (byte)mode); + stmt.AddValue(7, (owner.GetPetStable().GetCurrentActivePetIndex().HasValue ? (short)owner.GetPetStable().GetCurrentActivePetIndex().Value : (short)PetSaveMode.NotInSlot)); stmt.AddValue(8, GetName()); stmt.AddValue(9, HasPetFlag(UnitPetFlags.CanBeRenamed) ? 0 : 1); stmt.AddValue(10, curhealth); @@ -1676,6 +1673,8 @@ namespace Game.Entities public class PetStable { + static uint UnslottedPetIndexMask = 0x80000000; + public class PetInfo { public string Name; @@ -1695,14 +1694,31 @@ namespace Game.Entities public bool WasRenamed; } - public PetInfo CurrentPet; // PET_SAVE_AS_CURRENT + public uint? CurrentPetIndex; // index into ActivePets or UnslottedPets if highest bit is set + public PetInfo[] ActivePets = new PetInfo[SharedConst.MaxActivePets]; // PET_SAVE_FIRST_ACTIVE_SLOT - PET_SAVE_LAST_ACTIVE_SLOT public PetInfo[] StabledPets = new PetInfo[SharedConst.MaxPetStables]; // PET_SAVE_FIRST_STABLE_SLOT - PET_SAVE_LAST_STABLE_SLOT public List UnslottedPets = new(); // PET_SAVE_NOT_IN_SLOT - public PetInfo GetUnslottedHunterPet() + public PetInfo GetCurrentPet() { - return UnslottedPets.Count == 1 && UnslottedPets[0].Type == PetType.Hunter ? UnslottedPets[0] : null; + if (!CurrentPetIndex.HasValue) + return null; + + uint? activePetIndex = GetCurrentActivePetIndex(); + if (activePetIndex.HasValue) + return ActivePets[activePetIndex.Value] != null ? ActivePets[activePetIndex.Value] : null; + + uint? unslottedPetIndex = GetCurrentUnslottedPetIndex(); + if (unslottedPetIndex.HasValue) + return unslottedPetIndex < UnslottedPets.Count ? UnslottedPets[(int)unslottedPetIndex.Value] : null; + + return null; } + + public uint? GetCurrentActivePetIndex() { return CurrentPetIndex.HasValue && ((CurrentPetIndex & UnslottedPetIndexMask) == 0) ? CurrentPetIndex : null; } + public void SetCurrentActivePetIndex(uint index) { CurrentPetIndex = index; } + uint? GetCurrentUnslottedPetIndex() { return CurrentPetIndex.HasValue && ((CurrentPetIndex & UnslottedPetIndexMask) != 0) ? (CurrentPetIndex & ~UnslottedPetIndexMask) : null; } + public void SetCurrentUnslottedPetIndex(uint index) { CurrentPetIndex = index | UnslottedPetIndexMask; } } public enum ActiveStates diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 7516f8508..79426eea9 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -1538,7 +1538,7 @@ namespace Game.Entities m_bgData.taxiPath[1] = result.Read(8); m_bgData.mountSpell = result.Read(9); } - void _LoadPetStable(SQLResult result) + void _LoadPetStable(uint summonedPetNumber, SQLResult result) { if (result.IsEmpty()) return; @@ -1568,15 +1568,18 @@ namespace Game.Entities 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; + if (slot >= PetSaveMode.FirstActiveSlot && slot < PetSaveMode.LastActiveSlot) + m_petStable.ActivePets[(int)slot] = petInfo; + else if (slot >= PetSaveMode.FirstStableSlot && slot < PetSaveMode.LastStableSlot) + m_petStable.StabledPets[slot - PetSaveMode.FirstStableSlot] = petInfo; else if (slot == PetSaveMode.NotInSlot) m_petStable.UnslottedPets.Add(petInfo); } while (result.NextRow()); } + + if (Pet.GetLoadPetInfo(m_petStable, 0, summonedPetNumber, null).Item1 != null) + m_temporaryUnsummonedPetNumber = summonedPetNumber; } @@ -3080,8 +3083,7 @@ namespace Game.Entities m_taxi.LoadTaxiMask(taximask); // must be before InitTaxiNodesForLevel - _LoadPetStable(holder.GetResult(PlayerLoginQueryLoad.PetSlots)); - m_temporaryUnsummonedPetNumber = summonedPetNumber; + _LoadPetStable(summonedPetNumber, holder.GetResult(PlayerLoginQueryLoad.PetSlots)); // Honor system // Update Honor kills data @@ -3590,7 +3592,7 @@ namespace Game.Entities stmt.AddValue(index++, (ushort)m_ExtraFlags); PetStable petStable = GetPetStable(); if (petStable != null) - stmt.AddValue(index++, petStable.CurrentPet != null && petStable.CurrentPet.Health > 0 ? petStable.CurrentPet.PetNumber : 0); // summonedPetNumber + stmt.AddValue(index++, petStable.GetCurrentPet() != null && petStable.GetCurrentPet().Health > 0 ? petStable.GetCurrentPet().PetNumber : 0); // summonedPetNumber else stmt.AddValue(index++, 0); // summonedPetNumber stmt.AddValue(index++, (ushort)atLoginFlags); diff --git a/Source/Game/Entities/Player/Player.cs b/Source/Game/Entities/Player/Player.cs index d3799f816..a7feb2134 100644 --- a/Source/Game/Entities/Player/Player.cs +++ b/Source/Game/Entities/Player/Player.cs @@ -4701,12 +4701,19 @@ namespace Game.Entities return null; } - public Pet SummonPet(uint entry, float x, float y, float z, float ang, PetType petType, uint duration) + public Pet SummonPet(uint entry, PetSaveMode? slot, float x, float y, float z, float ang, uint duration) { + return SummonPet(entry, slot, x, y, z, ang, duration, out _); + } + + public Pet SummonPet(uint entry, PetSaveMode? slot, float x, float y, float z, float ang, uint duration, out bool isNew) + { + isNew = false; + PetStable petStable = GetOrInitPetStable(); - Pet pet = new(this, petType); - if (petType == PetType.Summon && pet.LoadPetFromDB(this, entry, 0, false)) + Pet pet = new(this, PetType.Summon); + if (pet.LoadPetFromDB(this, entry, 0, false, slot)) { if (duration > 0) pet.SetDuration(duration); @@ -4718,6 +4725,8 @@ namespace Game.Entities if (entry == 0) return null; + // only SUMMON_PET are handled here + pet.Relocate(x, y, z, ang); if (!pet.IsPositionValid()) { @@ -4734,7 +4743,7 @@ namespace Game.Entities return null; } - if (petType == PetType.Summon && petStable.CurrentPet != null) + if (petStable.GetCurrentPet() != null) RemovePet(null, PetSaveMode.NotInSlot); PhasingHandler.InheritPhaseShift(pet, this); @@ -4747,44 +4756,34 @@ 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(petNumber, true); - pet.SetClass(Class.Mage); - pet.SetPetExperience(0); - pet.SetPetNextLevelExperience(1000); - pet.SetFullHealth(); - pet.SetFullPower(PowerType.Mana); - pet.SetPetNameTimestamp((uint)GameTime.GetGameTime()); - break; - default: - break; - } + // this enables pet details window (Shift+P) + pet.GetCharmInfo().SetPetNumber(petNumber, true); + pet.SetClass(Class.Mage); + pet.SetPetExperience(0); + pet.SetPetNextLevelExperience(1000); + pet.SetFullHealth(); + pet.SetFullPower(PowerType.Mana); + pet.SetPetNameTimestamp((uint)GameTime.GetGameTime()); map.AddToMap(pet.ToCreature()); - switch (petType) - { - case PetType.Summon: - pet.InitPetCreateSpells(); - pet.SavePetToDB(PetSaveMode.AsCurrent); - PetSpellInitialize(); - break; - default: - break; - } + Cypher.Assert(petStable.CurrentPetIndex == 0); + petStable.SetCurrentUnslottedPetIndex((uint)petStable.UnslottedPets.Count); + PetStable.PetInfo petInfo = new(); + pet.FillPetInfo(petInfo); + petStable.UnslottedPets.Add(petInfo); + + pet.InitPetCreateSpells(); + pet.SavePetToDB(PetSaveMode.AsCurrent); + PetSpellInitialize(); if (duration > 0) pet.SetDuration(duration); //ObjectAccessor.UpdateObjectVisibility(pet); + isNew = true; + return pet; } @@ -4829,49 +4828,22 @@ namespace Game.Entities if (pet == null) { - if (mode == PetSaveMode.NotInSlot && m_petStable?.CurrentPet != null) - { - // Handle removing pet while it is in "temporarily unsummoned" state, for example on mount - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_ID); - stmt.AddValue(0, (short)PetSaveMode.NotInSlot); - stmt.AddValue(1, GetGUID().GetCounter()); - stmt.AddValue(2, m_petStable.CurrentPet.PetNumber); - DB.Characters.Execute(stmt); - - m_petStable.UnslottedPets.Add(m_petStable.CurrentPet); - m_petStable.CurrentPet = null; - } + // Handle removing pet while it is in "temporarily unsummoned" state, for example on mount + if (mode == PetSaveMode.NotInSlot && m_petStable != null && m_petStable.CurrentPetIndex.HasValue) + m_petStable.CurrentPetIndex = null; return; } pet.CombatStop(); - if (returnreagent) - { - switch (pet.GetEntry()) - { - //warlock pets except imp are removed(?) when logging out - case 1860: - case 1863: - case 417: - case 17252: - mode = PetSaveMode.NotInSlot; - break; - } - } - // 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; + PetStable.PetInfo currentPet = m_petStable.GetCurrentPet(); + Cypher.Assert(currentPet != null && currentPet.PetNumber == pet.GetCharmInfo().GetPetNumber()); + if (mode == PetSaveMode.NotInSlot || mode == PetSaveMode.AsDeleted) + m_petStable.CurrentPetIndex = null; // else if (stable slots) handled in opcode handlers due to required swaps // else (current pet) doesnt need to do anything diff --git a/Source/Game/Entities/Unit/Unit.Pets.cs b/Source/Game/Entities/Unit/Unit.Pets.cs index d1fa62988..1635eaaf8 100644 --- a/Source/Game/Entities/Unit/Unit.Pets.cs +++ b/Source/Game/Entities/Unit/Unit.Pets.cs @@ -761,7 +761,9 @@ namespace Game.Entities { Player player = ToPlayer(); PetStable petStable = player.GetOrInitPetStable(); - if (petStable.CurrentPet != null || petStable.GetUnslottedHunterPet() != null) + + var freeActiveSlot = Array.FindIndex(petStable.ActivePets, petInfo => petInfo == null); + if (freeActiveSlot == -1) return false; pet.SetCreatorGUID(GetGUID()); @@ -784,8 +786,11 @@ namespace Game.Entities pet.InitPetCreateSpells(); pet.SetFullHealth(); - petStable.CurrentPet = new(); - pet.FillPetInfo(petStable.CurrentPet); + petStable.SetCurrentActivePetIndex((uint)freeActiveSlot); + + PetStable.PetInfo petInfo = new(); + pet.FillPetInfo(petInfo); + petStable.ActivePets[freeActiveSlot] = petInfo; return true; } diff --git a/Source/Game/Handlers/CharacterHandler.cs b/Source/Game/Handlers/CharacterHandler.cs index 6c9b9acd7..301460128 100644 --- a/Source/Game/Handlers/CharacterHandler.cs +++ b/Source/Game/Handlers/CharacterHandler.cs @@ -792,6 +792,10 @@ namespace Game pCurrChar.SetGuildLevel(0); } + // Send stable contents to display icons on Call Pet spells + if (pCurrChar.HasSpell(SharedConst.CallPetSpellId)) + SendStablePet(ObjectGuid.Empty); + pCurrChar.GetSession().GetBattlePetMgr().SendJournalLockStatus(); pCurrChar.SendInitialPacketsBeforeAddToMap(); diff --git a/Source/Game/Handlers/NPCHandler.cs b/Source/Game/Handlers/NPCHandler.cs index 1911487bd..bd570834c 100644 --- a/Source/Game/Handlers/NPCHandler.cs +++ b/Source/Game/Handlers/NPCHandler.cs @@ -324,7 +324,7 @@ namespace Game GetPlayer().PlayerTalkClass.SendCloseGossip(); } - [WorldPacketHandler(ClientOpcodes.RequestStabledPets)] + [WorldPacketHandler(ClientOpcodes.RequestStabledPets, Processing = PacketProcessing.Inplace)] void HandleRequestStabledPets(RequestStabledPets packet) { if (!CheckStableMaster(packet.StableMaster)) @@ -353,58 +353,40 @@ namespace Game return; } - uint petSlot = 0; - if (petStable.CurrentPet != null) + for (uint petSlot = 0; petSlot < petStable.ActivePets.Length; ++petSlot) { - PetStable.PetInfo pet = petStable.CurrentPet; + if (petStable.ActivePets[petSlot] == null) + continue; + PetStable.PetInfo pet = petStable.ActivePets[petSlot]; PetStableInfo stableEntry; - stableEntry.PetSlot = petSlot; + stableEntry.PetSlot = petSlot + (int)PetSaveMode.FirstActiveSlot; 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); } - else + + for (uint petSlot = 0; petSlot < petStable.StabledPets.Length; ++petSlot) { - PetStable.PetInfo pet = petStable.GetUnslottedHunterPet(); - 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.Active; - stableEntry.PetName = pet.Name; + if (petStable.StabledPets[petSlot] == null) + continue; - ++petSlot; - packet.Pets.Add(stableEntry); - } - } + PetStable.PetInfo pet = petStable.StabledPets[petSlot]; + PetStableInfo stableEntry; + stableEntry.PetSlot = petSlot + (int)PetSaveMode.FirstStableSlot; + stableEntry.PetNumber = pet.PetNumber; + stableEntry.CreatureID = pet.CreatureId; + stableEntry.DisplayID = pet.DisplayId; + stableEntry.ExperienceLevel = pet.Level; + stableEntry.PetFlags = PetStableinfo.Inactive; + stableEntry.PetName = pet.Name; - 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); - } + packet.Pets.Add(stableEntry); } SendPacket(packet); @@ -417,6 +399,152 @@ namespace Game SendPacket(petStableResult); } + [WorldPacketHandler(ClientOpcodes.SetPetSlot)] + void HandleSetPetSlot(SetPetSlot setPetSlot) + { + if (!CheckStableMaster(setPetSlot.StableMaster) || setPetSlot.DestSlot >= (byte)PetSaveMode.LastStableSlot) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + GetPlayer().RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags.Interacting); + + PetStable petStable = GetPlayer().GetPetStable(); + if (petStable == null) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + (PetStable.PetInfo srcPet, PetSaveMode srcPetSlot) = Pet.GetLoadPetInfo(petStable, 0, setPetSlot.PetNumber, null); + PetSaveMode dstPetSlot = (PetSaveMode)setPetSlot.DestSlot; + PetStable.PetInfo dstPet = Pet.GetLoadPetInfo(petStable, 0, 0, dstPetSlot).Item1; + + if (srcPet == null || srcPet.Type != PetType.Hunter) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + if (dstPet != null && dstPet.Type != PetType.Hunter) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + PetStable.PetInfo src = null; + PetStable.PetInfo dst = null; + PetSaveMode? newActivePetIndex = null; + + if (SharedConst.IsActivePetSlot(srcPetSlot) && SharedConst.IsActivePetSlot(dstPetSlot)) + { + // active<.active: only swap ActivePets and CurrentPetIndex (do not despawn pets) + src = petStable.ActivePets[srcPetSlot - PetSaveMode.FirstActiveSlot]; + dst = petStable.ActivePets[dstPetSlot - PetSaveMode.FirstActiveSlot]; + + if (petStable.GetCurrentActivePetIndex().Value == (uint)srcPetSlot) + newActivePetIndex = dstPetSlot; + else if (petStable.GetCurrentActivePetIndex().Value == (uint)dstPetSlot) + newActivePetIndex = srcPetSlot; + } + else if (SharedConst.IsStabledPetSlot(srcPetSlot) && SharedConst.IsStabledPetSlot(dstPetSlot)) + { + // stabled<.stabled: only swap StabledPets + src = petStable.StabledPets[srcPetSlot - PetSaveMode.FirstStableSlot]; + dst = petStable.StabledPets[dstPetSlot - PetSaveMode.FirstStableSlot]; + } + else if (SharedConst.IsActivePetSlot(srcPetSlot) && SharedConst.IsStabledPetSlot(dstPetSlot)) + { + // active<.stabled: swap petStable contents and despawn active pet if it is involved in swap + if (petStable.CurrentPetIndex.Value == (uint)srcPetSlot) + { + Pet oldPet = _player.GetPet(); + if (oldPet != null && !oldPet.IsAlive()) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + _player.RemovePet(oldPet, PetSaveMode.NotInSlot); + } + + if (dstPet != null) + { + CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(dstPet.CreatureId); + if (creatureInfo == null || !creatureInfo.IsTameable(_player.CanTameExoticPets())) + { + SendPetStableResult(StableResult.CantControlExotic); + return; + } + } + + src = petStable.ActivePets[srcPetSlot - PetSaveMode.FirstActiveSlot]; + dst = petStable.StabledPets[dstPetSlot - PetSaveMode.FirstStableSlot]; + } + else if (SharedConst.IsStabledPetSlot(srcPetSlot) && SharedConst.IsActivePetSlot(dstPetSlot)) + { + // stabled<.active: swap petStable contents and despawn active pet if it is involved in swap + if (petStable.CurrentPetIndex.Value == (uint)dstPetSlot) + { + Pet oldPet = _player.GetPet(); + if (oldPet != null && !oldPet.IsAlive()) + { + SendPetStableResult(StableResult.InternalError); + return; + } + + _player.RemovePet(oldPet, PetSaveMode.NotInSlot); + } + + CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(srcPet.CreatureId); + if (creatureInfo == null || !creatureInfo.IsTameable(_player.CanTameExoticPets())) + { + SendPetStableResult(StableResult.CantControlExotic); + return; + } + + src = petStable.StabledPets[srcPetSlot - PetSaveMode.FirstStableSlot]; + dst = petStable.ActivePets[dstPetSlot - PetSaveMode.FirstActiveSlot]; + } + + SQLTransaction trans = new(); + + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_ID); + stmt.AddValue(0, (short)dstPetSlot); + stmt.AddValue(1, _player.GetGUID().GetCounter()); + stmt.AddValue(2, srcPet.PetNumber); + trans.Append(stmt); + + if (dstPet != null) + { + stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_CHAR_PET_SLOT_BY_ID); + stmt.AddValue(0, (short)srcPetSlot); + stmt.AddValue(1, _player.GetGUID().GetCounter()); + stmt.AddValue(2, dstPet.PetNumber); + trans.Append(stmt); + } + + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + var currentPlayerGuid = _player.GetGUID(); + if (_player && _player.GetGUID() == currentPlayerGuid) + { + if (success) + { + Extensions.Swap(ref src, ref dst); + if (newActivePetIndex.HasValue) + GetPlayer().GetPetStable().SetCurrentActivePetIndex((uint)newActivePetIndex.Value); + SendPetStableResult(StableResult.StableSuccess); + } + else + { + SendPetStableResult(StableResult.InternalError); + } + } + }); + } + [WorldPacketHandler(ClientOpcodes.RepairItem, Processing = PacketProcessing.Inplace)] void HandleRepairItem(RepairItem packet) { diff --git a/Source/Game/Handlers/PetHandler.cs b/Source/Game/Handlers/PetHandler.cs index 61ab785bc..1f770d538 100644 --- a/Source/Game/Handlers/PetHandler.cs +++ b/Source/Game/Handlers/PetHandler.cs @@ -541,7 +541,7 @@ namespace Game // check it! if (!pet || !pet.IsPet() || pet.ToPet().GetPetType() != PetType.Hunter || !pet.HasPetFlag(UnitPetFlags.CanBeRenamed) || pet.GetOwnerGUID() != _player.GetGUID() || pet.GetCharmInfo() == null || - petStable == null || petStable.CurrentPet == null || petStable.CurrentPet.PetNumber != pet.GetCharmInfo().GetPetNumber()) + petStable == null || petStable.GetCurrentPet() == null || petStable.GetCurrentPet().PetNumber != pet.GetCharmInfo().GetPetNumber()) return; PetNameInvalidReason res = ObjectManager.CheckPetName(name); @@ -561,8 +561,8 @@ namespace Game pet.SetGroupUpdateFlag(GroupUpdatePetFlags.Name); pet.RemovePetFlag(UnitPetFlags.CanBeRenamed); - petStable.CurrentPet.Name = name; - petStable.CurrentPet.WasRenamed = true; + petStable.GetCurrentPet().Name = name; + petStable.GetCurrentPet().WasRenamed = true; PreparedStatement stmt; SQLTransaction trans = new(); diff --git a/Source/Game/Networking/Packets/NPCPackets.cs b/Source/Game/Networking/Packets/NPCPackets.cs index 2cdf3c23e..cbdf90a07 100644 --- a/Source/Game/Networking/Packets/NPCPackets.cs +++ b/Source/Game/Networking/Packets/NPCPackets.cs @@ -292,6 +292,22 @@ namespace Game.Networking.Packets public ObjectGuid StableMaster; } + class SetPetSlot : ClientPacket + { + public SetPetSlot(WorldPacket packet) : base(packet) { } + + public override void Read() + { + PetNumber = _worldPacket.ReadUInt32(); + DestSlot = _worldPacket.ReadUInt8(); + StableMaster = _worldPacket.ReadPackedGuid(); + } + + public ObjectGuid StableMaster; + public uint PetNumber; + public byte DestSlot; + } + //Structs public struct TreasureItem { diff --git a/Source/Game/Spells/Spell.cs b/Source/Game/Spells/Spell.cs index 7446791cd..a11cb3f2a 100644 --- a/Source/Game/Spells/Spell.cs +++ b/Source/Game/Spells/Spell.cs @@ -5148,13 +5148,20 @@ namespace Game.Spells } case SpellEffectName.ResurrectPet: { - if (unitCaster == null) + Player playerCaster = m_caster.ToPlayer(); + if (playerCaster == null || playerCaster.GetPetStable() == null) return SpellCastResult.BadTargets; - Creature pet = unitCaster.GetGuardianPet(); + Pet pet = playerCaster.GetPet(); if (pet != null && pet.IsAlive()) return SpellCastResult.AlreadyHaveSummon; + PetStable petStable = playerCaster.GetPetStable(); + var deadPetInfo = petStable.ActivePets.FirstOrDefault(petInfo => petInfo?.Health == 0); + + if (deadPetInfo == null) + return SpellCastResult.BadTargets; + break; } // This is generic summon effect @@ -5221,17 +5228,27 @@ namespace Game.Spells 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) + PetSaveMode? petSlot = null; + if (spellEffectInfo.MiscValue == 0) { - if (info.Item1.Type == PetType.Hunter) + petSlot = (PetSaveMode)spellEffectInfo.CalcValue(); + + // No pet can be summoned if any pet is dead + foreach (var activePet in playerCaster.GetPetStable().ActivePets) { - if (info.Item1.Health == 0) + if (activePet?.Health == 0) { playerCaster.SendTameFailure(PetTameResult.Dead); return SpellCastResult.DontReport; } + } + } + var info = Pet.GetLoadPetInfo(playerCaster.GetPetStable(), (uint)spellEffectInfo.MiscValue, 0, petSlot); + if (info.Item1 != null) + { + if (info.Item1.Type == PetType.Hunter) + { CreatureTemplate creatureInfo = Global.ObjectMgr.GetCreatureTemplate(info.Item1.CreatureId); if (creatureInfo == null || !creatureInfo.IsTameable(playerCaster.CanTameExoticPets())) { @@ -5253,6 +5270,21 @@ namespace Game.Spells } break; } + case SpellEffectName.DismissPet: + { + Player playerCaster = m_caster.ToPlayer(); + if (playerCaster == null) + return SpellCastResult.BadTargets; + + Pet pet = playerCaster.GetPet(); + if (pet == null) + return SpellCastResult.NoPet; + + if (!pet.IsAlive()) + return SpellCastResult.TargetsDead; + + break; + } case SpellEffectName.SummonPlayer: { if (!m_caster.IsTypeId(TypeId.Player)) diff --git a/Source/Game/Spells/SpellEffects.cs b/Source/Game/Spells/SpellEffects.cs index 38d06c2b2..895cc5719 100644 --- a/Source/Game/Spells/SpellEffects.cs +++ b/Source/Game/Spells/SpellEffects.cs @@ -2298,27 +2298,34 @@ namespace Game.Spells return; } + PetSaveMode? petSlot = null; + if (petentry == 0) + petSlot = (PetSaveMode)damage; + 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); - if (!pet) + Pet pet = owner.SummonPet(petentry, petSlot, x, y, z, owner.Orientation, 0, out bool isNew); + if (pet == null) return; - if (m_caster.IsTypeId(TypeId.Unit)) + if (isNew) { - if (m_caster.ToCreature().IsTotem()) - pet.SetReactState(ReactStates.Aggressive); - else - pet.SetReactState(ReactStates.Defensive); + if (m_caster.IsCreature()) + { + if (m_caster.ToCreature().IsTotem()) + pet.SetReactState(ReactStates.Aggressive); + else + pet.SetReactState(ReactStates.Defensive); + } + + pet.SetCreatedBySpell(m_spellInfo.Id); + + // generate new name for summon pet + string new_name = Global.ObjectMgr.GeneratePetName(petentry); + if (!string.IsNullOrEmpty(new_name)) + pet.SetName(new_name); } - pet.SetCreatedBySpell(m_spellInfo.Id); - - // generate new name for summon pet - string new_name = Global.ObjectMgr.GeneratePetName(petentry); - if (!string.IsNullOrEmpty(new_name)) - pet.SetName(new_name); - ExecuteLogEffectSummonObject(effectInfo.Effect, pet); } @@ -4012,42 +4019,68 @@ namespace Game.Spells } [SpellEffectHandler(SpellEffectName.ResurrectPet)] - void EffectSummonDeadPet() + void EffectResurrectPet() { if (effectHandleMode != SpellEffectHandleMode.Hit) return; + if (damage < 0) + return; + Player player = m_caster.ToPlayer(); if (player == null) return; - Pet pet = player.GetPet(); - if (pet != null && pet.IsAlive()) - return; + // Maybe player dismissed dead pet or pet despawned? + bool hadPet = true; - if (damage < 0) - return; - - float x, y, z; - player.GetPosition(out x, out y, out z); - if (pet == null) + if (player.GetPet() == null) { - player.SummonPet(0, x, y, z, player.Orientation, PetType.Summon, 0); - pet = player.GetPet(); + PetStable petStable = player.GetPetStable(); + var deadPetIndex = Array.FindIndex(petStable.ActivePets, petInfo => petInfo?.Health == 0); + + PetSaveMode slot = (PetSaveMode)deadPetIndex; + + player.SummonPet(0, slot, 0f, 0f, 0f, 0f, 0); + hadPet = false; } - if (pet == null) + + // TODO: Better to fail Hunter's "Revive Pet" at cast instead of here when casting ends + Pet pet = player.GetPet(); // Attempt to get current pet + if (pet == null || pet.IsAlive()) return; - player.GetMap().CreatureRelocation(pet, x, y, z, player.GetOrientation()); + // If player did have a pet before reviving, teleport it + if (hadPet) + { + // Reposition the pet's corpse before reviving so as not to grab aggro + // We can use a different, more accurate version of GetClosePoint() since we have a pet + // Will be used later to reposition the pet if we have one + player.GetClosePoint(out float x, out float y, out float z, pet.GetCombatReach(), SharedConst.PetFollowDist, pet.GetFollowAngle()); + pet.NearTeleportTo(x, y, z, player.GetOrientation()); + pet.Relocate(x, y, z, player.GetOrientation()); // This is needed so SaveStayPosition() will get the proper coords. + } - pet.SetDynamicFlags(UnitDynFlags.HideModel); + pet.SetDynamicFlags(UnitDynFlags.None); pet.RemoveUnitFlag(UnitFlags.Skinnable); pet.SetDeathState(DeathState.Alive); pet.ClearUnitState(UnitState.AllErasable); pet.SetHealth(pet.CountPctFromMaxHealth(damage)); - pet.InitializeAI(); - player.PetSpellInitialize(); + // Reset things for when the AI to takes over + CharmInfo ci = pet.GetCharmInfo(); + if (ci != null) + { + // In case the pet was at stay, we don't want it running back + ci.SaveStayPosition(); + ci.SetIsAtStay(ci.HasCommandState(CommandStates.Stay)); + + ci.SetIsFollowing(false); + ci.SetIsCommandAttack(false); + ci.SetIsCommandFollow(false); + ci.SetIsReturning(false); + } + pet.SavePetToDB(PetSaveMode.AsCurrent); } diff --git a/Source/Scripts/Spells/Hunter.cs b/Source/Scripts/Spells/Hunter.cs index 17c92fe87..dfc17237a 100644 --- a/Source/Scripts/Spells/Hunter.cs +++ b/Source/Scripts/Spells/Hunter.cs @@ -405,6 +405,8 @@ namespace Scripts.Spells.Hunter [Script] class spell_hun_tame_beast : SpellScript { + static uint[] CallPetSpellIds = { 883, 83242, 83243, 83244, 83245, }; + SpellCastResult CheckCast() { Player caster = GetCaster().ToPlayer(); @@ -427,10 +429,18 @@ namespace Scripts.Spells.Hunter PetStable petStable = caster.GetPetStable(); if (petStable != null) { - if (petStable.CurrentPet != null) + if (petStable.CurrentPetIndex.HasValue) return SpellCastResult.AlreadyHaveSummon; - if (petStable.GetUnslottedHunterPet() != null) + var freeSlotIndex = Array.FindIndex(petStable.ActivePets, petInfo => petInfo == null); + if (freeSlotIndex == -1) + { + caster.SendTameFailure(PetTameResult.TooMany); + return SpellCastResult.DontReport; + } + + // Check for known Call Pet X spells + if (!caster.HasSpell(CallPetSpellIds[freeSlotIndex])) { caster.SendTameFailure(PetTameResult.TooMany); return SpellCastResult.DontReport;