From 95e074644161c9480fa54c7fb2f6a6bbc0961e0d Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 14:50:33 -0600 Subject: [PATCH] fix: a worn jetpack works again after a world change Jetpack mode is not saved, and the equip skill that turns it on was cast only when the item was put on, so a player still wearing a jetpack lost it in the next world. Live sent SetJetPackMode (use, bypass checks, effect 167, the values of the jetpack's equip skill) shortly after the player's own construction in 12 of 28 world loads of a player with a jetpack, and constructed the player in jetpack mode in the other 16. The jetpack behaviors among the equip skills of the equipped items are run again when the player has loaded. Nothing new is saved. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/InventoryComponent.cpp | 15 +++++++ dGame/dComponents/InventoryComponent.h | 11 +++++ dGame/dGameMessages/ZoneMessages.cpp | 3 ++ .../dComponentsTests/EquipMessagesTests.cpp | 41 +++++++++++++++++++ 4 files changed, 70 insertions(+) diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 495ece650..f4ef4fe3e 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -32,6 +32,7 @@ #include "ZoneMessages.h" #include "MovementMessages.h" #include "SkillComponent.h" +#include "Behavior.h" #include "Character.h" #include "EntityManager.h" #include "ItemSet.h" @@ -1694,6 +1695,20 @@ std::vector InventoryComponent::FindBuffs(Item* item, bool castOnEquip return buffs; } +std::vector InventoryComponent::FindEquippedJetPacks() const { + std::vector jetPacks; + for (const auto& equipped : m_Equipped | std::views::values) { + for (const auto behaviorID : FindBuffs(FindItemById(equipped.id), false)) { + if (Behavior::GetBehaviorTemplate(behaviorID) == BehaviorTemplate::JETPACK) jetPacks.push_back(behaviorID); + } + } + return jetPacks; +} + +void InventoryComponent::RecastEquippedJetPacks() const { + for (const auto behaviorID : FindEquippedJetPacks()) SkillComponent::HandleUnmanaged(behaviorID, m_Parent->GetObjectID()); +} + void InventoryComponent::SetNPCItems(const std::vector& items) { m_Equipped.clear(); diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 332ce2674..ebbff31e0 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -364,6 +364,17 @@ public: */ std::vector FindBuffs(Item* item, bool castOnEquip) const; + /** + * The jetpack behaviors among the equip skills of the equipped items + */ + std::vector FindEquippedJetPacks() const; + + /** + * Puts a player that wears a jetpack back in jetpack mode after a world load: live sent SetJetPackMode again + * shortly after the player's own construction in the next world, as the equip skill sends it + */ + void RecastEquippedJetPacks() const; + /** * One DeletionRestrictions row (CheckDeletionRestrictionIndex @ 00c94c20). visited stops rows that refer back to * themselves; the client lets such rows through. diff --git a/dGame/dGameMessages/ZoneMessages.cpp b/dGame/dGameMessages/ZoneMessages.cpp index 5d7b11804..45b3c0809 100644 --- a/dGame/dGameMessages/ZoneMessages.cpp +++ b/dGame/dGameMessages/ZoneMessages.cpp @@ -143,6 +143,9 @@ namespace GameMessages { character->OnZoneLoad(); } + // A jetpack that is worn works again in the new world (jetpack mode is not saved) + if (auto* const inventory = entity->GetComponent()) inventory->RecastEquippedJetPacks(); + // Moved here from another instance: put back what their save doesn't keep (the pet that was out) WorldMigration::OnPlayerLoaded(entity); diff --git a/tests/dGameTests/dComponentsTests/EquipMessagesTests.cpp b/tests/dGameTests/dComponentsTests/EquipMessagesTests.cpp index 773038b5a..f129a79a8 100644 --- a/tests/dGameTests/dComponentsTests/EquipMessagesTests.cpp +++ b/tests/dGameTests/dComponentsTests/EquipMessagesTests.cpp @@ -6,7 +6,10 @@ #include "CDClientDatabase.h" #include "CDComponentsRegistryTable.h" #include "CDItemComponentTable.h" +#include "CDBehaviorTemplateTable.h" #include "CDObjectSkillsTable.h" +#include "CDSkillBehaviorTable.h" +#include "BehaviorTemplate.h" #include "Character.h" #include "CharacterComponent.h" #include "EffectsMessages.h" @@ -210,3 +213,41 @@ TEST_F(EquipMessagesTests, UnequippingAndSlotsWithoutAnEffect) { sent = Messages(Capture([&] { rocket->Equip(true); })); EXPECT_EQ(sent, (std::vector{ { CHANGE_OBJECT_WORLD_STATE, rocket->GetId(), true, "ATTACHED" } })); } + +// A worn jetpack is found again after a world load, so jetpack mode can be sent again: live sent SetJetPackMode (use, +// bypass checks, effect 167) about 100 packets after the player's own construction in 12 of 28 world loads of a player +// with a jetpack, and constructed the player in jetpack mode in the other 16. +TEST_F(EquipMessagesTests, AWornJetPackIsFoundAfterAWorldLoad) { + constexpr LOT JETPACK = 1727; // skill 306, behavior 3569 on equip + RegisterLot(JETPACK, "back"); + CDClientManager::GetEntriesMutable().push_back({ JETPACK, 306, 1, 0 }); + CDClientManager::GetEntriesMutable().push_back({ SWORD, 400, 0, 0 }); // a use skill, not an equip one + auto& skills = CDClientManager::GetEntriesMutable(); + CDSkillBehavior jetPackSkill{}; + jetPackSkill.skillID = 306; + jetPackSkill.behaviorID = 3569; + skills.insert_or_assign(306, jetPackSkill); + CDSkillBehavior hoodSkill{}; + hoodSkill.skillID = 362; + hoodSkill.behaviorID = 4000; + skills.insert_or_assign(362, hoodSkill); + auto& templates = CDClientManager::GetEntriesMutable(); + CDBehaviorTemplate jetPackTemplate{}; + jetPackTemplate.behaviorID = 3569; + jetPackTemplate.templateID = static_cast(BehaviorTemplate::JETPACK); + templates.insert_or_assign(3569, jetPackTemplate); + CDBehaviorTemplate buffTemplate{}; + buffTemplate.behaviorID = 4000; + buffTemplate.templateID = static_cast(BehaviorTemplate::BUFF); + templates.insert_or_assign(4000, buffTemplate); + + EXPECT_TRUE(inventory->FindEquippedJetPacks().empty()); + // What a save puts back: the equipped items, without their equip skills cast + auto* const hood = Give(HOOD); + auto* const jetPack = Give(JETPACK); + Give(SWORD); + inventory->UpdateSlot("hair", { hood->GetId(), HOOD, 1, 0 }); + EXPECT_TRUE(inventory->FindEquippedJetPacks().empty()); + inventory->UpdateSlot("back", { jetPack->GetId(), JETPACK, 1, 1 }); + EXPECT_EQ(inventory->FindEquippedJetPacks(), std::vector{ 3569 }); +}