mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<uint32_t>(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<MissionComponent>();
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ set(DCOMPONENTS_TESTS
|
||||
"DestroyableLevelConfigTests.cpp"
|
||||
"ProximityMonitorComponentTests.cpp"
|
||||
"RailActivatorComponentTests.cpp"
|
||||
"PropertyVendorComponentTests.cpp"
|
||||
"VendorBuybackTests.cpp"
|
||||
"RacingWrongWayTests.cpp"
|
||||
"DestroyableComponentTests.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 <gtest/gtest.h>
|
||||
|
||||
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<CDMissionsTable>();
|
||||
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<Entity>(GUARD, info);
|
||||
vendor = guard->AddComponent<PropertyVendorComponent>(-1);
|
||||
player = std::make_unique<Entity>(PLAYER, info);
|
||||
missions_ = player->AddComponent<MissionComponent>(-1);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
player.reset();
|
||||
guard.reset();
|
||||
CDClientManager::GetEntriesMutable<CDMissionsTable>().clear();
|
||||
TearDownDependencies();
|
||||
}
|
||||
|
||||
void Load(const std::string& done, const std::string& cur) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto xml = R"(<obj v="1"><mis><done>)" + done + "</done><cur>" + cur + "</cur></mis></obj>";
|
||||
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<Entity> guard;
|
||||
std::unique_ptr<Entity> 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<uint32_t>(u"property_required_mission", REQUIRED);
|
||||
EXPECT_EQ(vendor->GetRequiredMission(), REQUIRED);
|
||||
EXPECT_FALSE(vendor->MayRent(*player)); // never taken
|
||||
|
||||
Load("", R"(<m id="993001" state="2"/>)");
|
||||
EXPECT_FALSE(vendor->MayRent(*player)); // in progress
|
||||
}
|
||||
|
||||
TEST_F(PropertyVendorComponentTests, AFinishedMissionRents) {
|
||||
guard->SetVar<uint32_t>(u"property_required_mission", REQUIRED);
|
||||
Load(R"(<m id="993001" cct="1" cts="1000"/>)", "");
|
||||
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<uint32_t>(u"property_required_mission", REPEATED);
|
||||
Load(R"(<m id="993002" cct="1" cts="1000"/>)", R"(<m id="993002" state="10"/>)");
|
||||
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<uint32_t>(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<uint32_t>(u"property_required_mission", REQUIRED);
|
||||
Load(R"(<m id="993001" cct="1" cts="1000"/>)", "");
|
||||
const auto packets = Capture([&] { vendor->OnBuyFromVendor(player.get(), true, 0, 1); });
|
||||
EXPECT_TRUE(packets.empty());
|
||||
}
|
||||
Reference in New Issue
Block a user