From d1e0f21522a0cd24f77f2a4b0ef18d413b73fffa Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:28:57 -0500 Subject: [PATCH] fix(pets): summoned pets play their spawn animation and effect Issue 546. The client plays a pet's spawnAnim ("spawn" unless set in its config) only when the pet is constructed with state 8 (bit 0x80) set (LWOPetComponent::Deserialize 0x00cd1270). Live summons were constructed with status 0x84, played the pet's "despawn" effect (the circles and stars, effect 365) and then dropped the bit; summoned pets are now constructed that way, with the effect and the state change at the end of the spawn animation. Sending a pet back to the backpack killed it right after sending the despawn effect, so the client removed it before the effect could play. Live removed the pet some time after the effect; it is now removed once the pet's despawn animation time has passed, and does nothing in the meantime. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/PetComponent.cpp | 28 ++++++++++++++++++++++++++-- dGame/dComponents/PetComponent.h | 12 ++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index d72a85892..9ef82ab7a 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -290,6 +290,9 @@ void PetComponent::Update(float deltaTime) { return; } + // Going back to the backpack, removed once the despawn effect has played + if (m_Despawning) return; + auto* owner = GetOwner(); if (owner == nullptr) { @@ -921,7 +924,19 @@ void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { auto* owner = GetOwner(); if (owner == nullptr) return; - SetStatus(1); + + if (registerPet) { + // Summoned from the backpack. Live constructed the pet in the spawning state, so the client plays its + // spawn animation, played the pet's "despawn" effect (the circles and stars) and then cleared the state. + SetStatus(1 | PET_STATE_SPAWNING); + m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "spawn"), [this]() { + GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS); + SetStatus(m_Status & ~PET_STATE_SPAWNING); + Game::entityManager->SerializeEntity(m_Parent); + }); + } else { + SetStatus(1); + } auto databaseData = inventoryComponent->GetDatabasePet(m_DatabaseId); @@ -1047,11 +1062,20 @@ void PetComponent::AddDrainImaginationTimer(bool fromTaming) { } void PetComponent::Deactivate() { + if (m_Despawning) return; + m_Despawning = true; + GameMessages::PlayFXEffect(m_Parent->GetObjectID(), -1, u"despawn", "").Send(UNASSIGNED_SYSTEM_ADDRESS); activePets.erase(m_Owner); - m_Parent->Kill(); + auto* const movementAI = m_Parent->GetComponent(); + if (movementAI) movementAI->Stop(); + + // Live removed the pet only once its despawn effect had played; removing it at once cut the effect short + m_Parent->AddCallbackTimer(RenderComponent::GetAnimationTime(m_Parent, "despawn"), [this]() { + m_Parent->Kill(); + }); auto* owner = GetOwner(); diff --git a/dGame/dComponents/PetComponent.h b/dGame/dComponents/PetComponent.h index c7e19b0ef..8670de5f3 100644 --- a/dGame/dComponents/PetComponent.h +++ b/dGame/dComponents/PetComponent.h @@ -18,6 +18,13 @@ class PetComponent final : public Component public: static constexpr eReplicaComponentType ComponentType = eReplicaComponentType::PET; + // Bits of the serialized pet status. State number N is bit 1 << (N - 1) (the client's StateNumToIndex, + // 0x00bd19b0); the meanings are what the live server sent (2014 packet captures) and what the client does with them. + + // State 8: a pet constructed with it plays its spawnAnim (LWOPetComponent::Deserialize, 0x00cd1270). Live + // constructed summoned pets with 0x84 and cleared this bit once the spawn animation was over. + static constexpr uint32_t PET_STATE_SPAWNING = 1 << 7; + explicit PetComponent(Entity* parentEntity, const int32_t componentID); ~PetComponent() override; @@ -344,6 +351,11 @@ private: * Pet information loaded from the CDClientDatabase */ CDPetComponent m_PetInfo; + + /** + * Set once the pet has been sent back to the backpack; it is removed after its despawn effect has played + */ + bool m_Despawning{ false }; }; #endif // !PETCOMPONENT_H