From baf8b24ad1cede60592e9d7d3cf6d873a57df6c2 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 15:28:18 -0600 Subject: [PATCH] fix: a property is rented only with the level's required mission LWOPropertyVendorComponent::onCheckUseRequirements (0x00c2e210) lets a player use the property guard only when the level's property_required_mission (LoadConfigData 0x00c2e550) is finished. The server opened the vendor and rented the property on any request. The guard is now not opened without the mission, and a rent request without it is answered with PropertyRentalResponse code 9, which onPropertyRentalResponse (0x00c56d70) shows as "you failed to rent" with UGG_PROPRENT_Dont_have_achievement. A mission that was finished and is being repeated counts, as in the client's list of finished missions. A guard without the key asks for nothing. The six property maps name missions 768, 872, 873, 874, 1293 and 1322. Co-Authored-By: Claude Fable 5.2 --- .../Property/PropertyVendorComponent.cpp | 27 +++++ .../Property/PropertyVendorComponent.h | 12 ++ .../dComponentsTests/CMakeLists.txt | 1 + .../PropertyVendorComponentTests.cpp | 113 ++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 tests/dGameTests/dComponentsTests/PropertyVendorComponentTests.cpp diff --git a/dGame/dComponents/Property/PropertyVendorComponent.cpp b/dGame/dComponents/Property/PropertyVendorComponent.cpp index bf1992894..8a6192fdc 100644 --- a/dGame/dComponents/Property/PropertyVendorComponent.cpp +++ b/dGame/dComponents/Property/PropertyVendorComponent.cpp @@ -10,14 +10,32 @@ #include "Logger.h" #include "PropertyManagementComponent.h" #include "UserManager.h" +#include "Mission.h" +#include "MissionComponent.h" PropertyVendorComponent::PropertyVendorComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { RegisterMsg(&PropertyVendorComponent::OnRequestUse); } +uint32_t PropertyVendorComponent::GetRequiredMission() const { + return m_Parent->GetVarAs(u"property_required_mission"); +} + +bool PropertyVendorComponent::MayRent(Entity& player) const { + const auto required = GetRequiredMission(); + if (required == 0) return true; + + const auto* const missions = player.GetComponent(); + const auto* const mission = missions ? missions->GetMission(required) : nullptr; + // The client asks its list of finished missions: one that was completed and is repeated counts as well + return mission != nullptr && (mission->IsComplete() || mission->GetCompletions() > 0); +} + bool PropertyVendorComponent::OnRequestUse(GameMessages::RequestUseEvent& event) { if (event.msg.IsMissionChoice()) return false; auto* const originator = event.user; + // LWOPropertyVendorComponent::onCheckUseRequirements (0x00c2e210): no use without the level's mission + if (!MayRent(*originator)) return true; if (PropertyManagementComponent::Instance() == nullptr) return true; OnQueryPropertyData(originator, originator->GetSystemAddress()); @@ -41,6 +59,15 @@ void PropertyVendorComponent::OnQueryPropertyData(Entity* originator, const Syst } void PropertyVendorComponent::OnBuyFromVendor(Entity* originator, const bool confirmed, const LOT lot, const uint32_t count) { + if (!MayRent(*originator)) { + LOG("Player %llu rents a property without mission %u", originator->GetObjectID(), GetRequiredMission()); + GameMessages::PropertyRentalResponse refused; + refused.target = m_Parent->GetObjectID(); + refused.code = RENTAL_MISSING_ACHIEVEMENT; + refused.Send(originator->GetSystemAddress()); + return; + } + if (PropertyManagementComponent::Instance() == nullptr) return; if (PropertyManagementComponent::Instance()->Claim(originator->GetObjectID()) == false) { diff --git a/dGame/dComponents/Property/PropertyVendorComponent.h b/dGame/dComponents/Property/PropertyVendorComponent.h index 4b706a7e8..d479111c0 100644 --- a/dGame/dComponents/Property/PropertyVendorComponent.h +++ b/dGame/dComponents/Property/PropertyVendorComponent.h @@ -34,4 +34,16 @@ public: * @param count unused */ void OnBuyFromVendor(Entity* originator, bool confirmed, LOT lot, uint32_t count); + + // PropertyRentalResponse code the client shows as "you failed to rent" with UGG_PROPRENT_Dont_have_achievement + // (LWOPropertyVendorComponent::onPropertyRentalResponse 0x00c56d70; 0 is a rented property) + static constexpr uint32_t RENTAL_MISSING_ACHIEVEMENT = 9; + + // The level's property_required_mission (LWOPropertyVendorComponent::LoadConfigData 0x00c2e550), 0 without one + uint32_t GetRequiredMission() const; + + // Whether the player has finished the level's property_required_mission, which the client asks for before the + // vendor can be used (LWOPropertyVendorComponent::onCheckUseRequirements 0x00c2e210). A vendor without the key + // asks for nothing here. + bool MayRent(Entity& player) const; }; diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 98c6bc021..95c57b4e6 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -6,6 +6,7 @@ set(DCOMPONENTS_TESTS "DestroyableLevelConfigTests.cpp" "ProximityMonitorComponentTests.cpp" "RailActivatorComponentTests.cpp" + "PropertyVendorComponentTests.cpp" "VendorBuybackTests.cpp" "RacingWrongWayTests.cpp" "DestroyableComponentTests.cpp" diff --git a/tests/dGameTests/dComponentsTests/PropertyVendorComponentTests.cpp b/tests/dGameTests/dComponentsTests/PropertyVendorComponentTests.cpp new file mode 100644 index 000000000..f07043b7b --- /dev/null +++ b/tests/dGameTests/dComponentsTests/PropertyVendorComponentTests.cpp @@ -0,0 +1,113 @@ +// The property guard's mission: LWOPropertyVendorComponent::onCheckUseRequirements (0x00c2e210) lets a player use the +// guard only with the level's property_required_mission finished. +#include "GameDependencies.h" +#include "dGameMessagesTests/GameMessageTestUtils.h" + +#include "CDClientDatabase.h" +#include "CDClientManager.h" +#include "CDMissionsTable.h" +#include "Entity.h" +#include "MissionComponent.h" +#include "PropertyMessages.h" +#include "PropertyVendorComponent.h" + +#include "tinyxml2.h" + +#include + +using namespace GameMessageTestUtils; + +namespace { + // Mission IDs only these tests use + constexpr uint32_t REQUIRED = 993001; + constexpr uint32_t REPEATED = 993002; +} + +class PropertyVendorComponentTests : public GameDependenciesTest { +protected: + void SetUp() override { + SetUpDependencies(); + CDClientDatabase::Connect(":memory:"); // MissionComponent counts the achievements + CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);"); + auto& missions = CDClientManager::GetEntriesMutable(); + for (const auto id : { REQUIRED, REPEATED }) { + CDMissions row{}; + row.id = id; + row.isMission = true; + row.repeatable = id == REPEATED; + missions.push_back(row); + } + guard = std::make_unique(GUARD, info); + vendor = guard->AddComponent(-1); + player = std::make_unique(PLAYER, info); + missions_ = player->AddComponent(-1); + } + + void TearDown() override { + player.reset(); + guard.reset(); + CDClientManager::GetEntriesMutable().clear(); + TearDownDependencies(); + } + + void Load(const std::string& done, const std::string& cur) { + tinyxml2::XMLDocument doc; + const auto xml = R"()" + done + "" + cur + ""; + ASSERT_EQ(doc.Parse(xml.c_str()), tinyxml2::XML_SUCCESS); + missions_->LoadFromSaveData(doc); + } + + static constexpr LWOOBJID GUARD = 0x0000400000112233LL; + static constexpr LWOOBJID PLAYER = 0x1000000000000001LL; + std::unique_ptr guard; + std::unique_ptr player; + PropertyVendorComponent* vendor = nullptr; + MissionComponent* missions_ = nullptr; +}; + +TEST_F(PropertyVendorComponentTests, AGuardWithoutAMissionAsksForNone) { + EXPECT_EQ(vendor->GetRequiredMission(), 0u); + EXPECT_TRUE(vendor->MayRent(*player)); +} + +TEST_F(PropertyVendorComponentTests, TheMissionMustBeFinished) { + guard->SetVar(u"property_required_mission", REQUIRED); + EXPECT_EQ(vendor->GetRequiredMission(), REQUIRED); + EXPECT_FALSE(vendor->MayRent(*player)); // never taken + + Load("", R"()"); + EXPECT_FALSE(vendor->MayRent(*player)); // in progress +} + +TEST_F(PropertyVendorComponentTests, AFinishedMissionRents) { + guard->SetVar(u"property_required_mission", REQUIRED); + Load(R"()", ""); + EXPECT_TRUE(vendor->MayRent(*player)); +} + +// The client's list of finished missions keeps a mission that is being repeated +TEST_F(PropertyVendorComponentTests, AFinishedMissionThatIsRepeatedRents) { + guard->SetVar(u"property_required_mission", REPEATED); + Load(R"()", R"()"); + EXPECT_TRUE(vendor->MayRent(*player)); +} + +// A rent request without the mission is answered with code 9, which the client shows as "you failed to rent" and +// UGG_PROPRENT_Dont_have_achievement (LWOPropertyVendorComponent::onPropertyRentalResponse 0x00c56d70): +// clone u32 0, code u32 9, property u64 0, rent due i64 0 +TEST_F(PropertyVendorComponentTests, RentingWithoutTheMissionIsRefused) { + guard->SetVar(u"property_required_mission", REQUIRED); + const auto packets = Capture([&] { vendor->OnBuyFromVendor(player.get(), true, 0, 1); }); + ASSERT_EQ(packets.size(), 1u); + EXPECT_PACKET_EQ(FromHex( + "53 05 00 0c 00 00 00 00 33 22 11 00 00 40 00 00 ee 02 " + "00 00 00 00 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00", 336), FromCapture(packets[0])); +} + +// With the mission, the request goes on to the property (none in this test: nothing is sent, nothing refused) +TEST_F(PropertyVendorComponentTests, RentingWithTheMissionIsNotRefused) { + guard->SetVar(u"property_required_mission", REQUIRED); + Load(R"()", ""); + const auto packets = Capture([&] { vendor->OnBuyFromVendor(player.get(), true, 0, 1); }); + EXPECT_TRUE(packets.empty()); +}