From f53ca0942b84cb18a2b28580b42ec61b25d75855 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sun, 27 Sep 2026 08:29:29 -0500 Subject: [PATCH] fix(pets): the backpack lets go of a pet that went away on its own Issue 539. When a pet is sent away the client greys its backpack item out again by looking the item up by the ID in MarkInventoryItemAsActive (LWOInventoryComponent_Common::SendMessage 0x00d54b90). The pet kept the item ID it was summoned with, but items get new IDs when they move, so the lookup could miss and the item stayed marked active; the pet's item is now looked up by its subkey when the pet goes away. Sending a pet away also sent AddPetToPlayer with an empty pet. The client never removes anything on that message, it adds a new entry to its pet list (LWOPetControlComponent::HandleMessage 0x00d0fe00), so it left an empty pet behind; live did not send it (2014 captures) and it is no longer sent. RegisterPetID with no pet already clears the active pet and hides the pet menu. The imagination drain kept going after it had sent the pet away (and took another point of imagination); it stops there now. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/PetComponent.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 9ef82ab7a..6926c6584 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -1045,6 +1045,9 @@ void PetComponent::AddDrainImaginationTimer(bool fromTaming) { const auto* playerDestroyableComponent = owner->GetComponent(); if (!playerDestroyableComponent) return; + // Already going back to the backpack + if (m_Despawning) return; + // If we are out of imagination despawn the pet. if (playerDestroyableComponent->GetImagination() == 0) { this->Deactivate(); @@ -1055,6 +1058,8 @@ void PetComponent::AddDrainImaginationTimer(bool fromTaming) { requirementsResponse.target = playerEntity->GetObjectID(); requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet; requirementsResponse.SendToClient(playerEntity->GetSystemAddress()); + // The pet is gone: no more draining + return; } this->AddDrainImaginationTimer(); @@ -1081,23 +1086,18 @@ void PetComponent::Deactivate() { if (owner == nullptr) return; + // The client looks the item up by this ID to grey it out again (LWOInventoryComponent_Common::SendMessage), and + // items get new IDs when they move, so use the ID the pet's item has now + auto* const inventoryComponent = owner->GetComponent(); + const auto* const item = inventoryComponent ? inventoryComponent->FindItemBySubKey(m_DatabaseId) : nullptr; + GameMessages::MarkInventoryItemAsActive markActive; markActive.target = m_Owner; markActive.bActive = false; markActive.iType = eUnequippableActiveType::PET; - markActive.itemID = m_ItemId; + markActive.itemID = item ? item->GetId() : m_ItemId; markActive.Send(owner->GetSystemAddress()); - { - GameMessages::AddPetToPlayer msg; - msg.target = m_Owner; - msg.iElementalType = 0; - msg.name = u""; - msg.petDBID = LWOOBJID_EMPTY; - msg.petLOT = LOT_NULL; - msg.Send(owner->GetSystemAddress()); - } - { GameMessages::RegisterPetID msg; msg.target = m_Owner;