From e77535984df066307b63bc5add58d748a7cc4c64 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 07:46:13 -0500 Subject: [PATCH] test(loot): item source keys and the quickbuild item cost's source Covers the key names and name value types, a pickup from an object, from the player and from an object that is gone, metrics after an item's config (and not kept on the item), a removal's loot source, and a quickbuild taking its item cost with itself as the source. Co-Authored-By: Claude Opus 5.5 --- .../dComponentsTests/CMakeLists.txt | 1 + .../dComponentsTests/LootMetricsTests.cpp | 247 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 tests/dGameTests/dComponentsTests/LootMetricsTests.cpp diff --git a/tests/dGameTests/dComponentsTests/CMakeLists.txt b/tests/dGameTests/dComponentsTests/CMakeLists.txt index 28e0f3be7..103739781 100644 --- a/tests/dGameTests/dComponentsTests/CMakeLists.txt +++ b/tests/dGameTests/dComponentsTests/CMakeLists.txt @@ -19,6 +19,7 @@ set(DCOMPONENTS_TESTS "ReplicaConstructionTests.cpp" "ReplicaDecoderTests.cpp" "ItemRemovalTests.cpp" + "LootMetricsTests.cpp" ) # Get the folder name and prepend it to the files above diff --git a/tests/dGameTests/dComponentsTests/LootMetricsTests.cpp b/tests/dGameTests/dComponentsTests/LootMetricsTests.cpp new file mode 100644 index 000000000..d4c0caa92 --- /dev/null +++ b/tests/dGameTests/dComponentsTests/LootMetricsTests.cpp @@ -0,0 +1,247 @@ +#include "GameDependencies.h" +#include + +#include "CDClientDatabase.h" +#include "CDComponentsRegistryTable.h" +#include "CDItemComponentTable.h" +#include "Character.h" +#include "CharacterComponent.h" +#include "Entity.h" +#include "EntityManager.h" +#include "InventoryComponent.h" +#include "InventoryMessages.h" +#include "Item.h" +#include "LootMetrics.h" +#include "QuickBuildComponent.h" +#include "MissionComponent.h" +#include "DestroyableComponent.h" +#include "LevelProgressionComponent.h" +#include "eQuickBuildState.h" +#include "eReplicaComponentType.h" + +#include "dGameMessagesTests/GameMessageTestUtils.h" + +using namespace GameMessageTestUtils; + +// The _Metric_ keys live put in AddItemToInventoryClientSync's extra info saying where items came from +// (LootMetrics), and the loot source of RemoveItemFromInventory. +class LootMetricsTests : public GameDependenciesTest { +protected: + static constexpr LOT ITEM = 7000; + static constexpr LOT SMASHABLE = 4712; + static constexpr LWOOBJID PLAYER = 0x1000000000000001LL; + + std::unique_ptr entity; + std::unique_ptr character; + InventoryComponent* inventory{}; + + void SetUp() override { + SetUpDependencies(); + CDClientDatabase::Connect(":memory:"); + CDClientDatabase::ExecuteDML("CREATE TABLE ItemSets (setID INTEGER, itemIDs TEXT);"); + CDClientDatabase::ExecuteDML("CREATE TABLE Objects (id INTEGER, name TEXT, type TEXT);"); + CDClientDatabase::ExecuteDML("CREATE TABLE ComponentsRegistry (id INTEGER, component_type INTEGER, component_id INTEGER);"); + CDClientDatabase::ExecuteDML("CREATE TABLE ObjectSkills (objectTemplate INTEGER, skillID INTEGER, castOnType INTEGER, AICombatWeight INTEGER);"); + CDClientDatabase::ExecuteDML("CREATE TABLE SkillBehavior (skillID INTEGER, behaviorID INTEGER);"); + CDClientDatabase::ExecuteDML("CREATE TABLE Missions (id INTEGER, isMission INTEGER);"); + RegisterLot(ITEM); + + info.lot = 1; // a player + CDClientManager::GetEntriesMutable().insert_or_assign(static_cast(info.lot), 0); + entity = std::make_unique(PLAYER, info); + inventory = entity->AddComponent(-1); + character = std::make_unique(1, nullptr); + entity->SetCharacter(character.get()); + entity->AddComponent(-1, character.get(), ClientAddress())->InitializeStatisticsFromString(""); + Game::entityManager->_addEntity(entity.get()); // drops from the player, and the quickbuild, look it up there + ClearSent(); + } + + void TearDown() override { + Game::entityManager->_removeEntity(PLAYER); + entity->SetCharacter(nullptr); + entity.reset(); + character.reset(); + TearDownDependencies(); + } + + static void RegisterLot(const LOT lot) { + const auto componentID = static_cast(96000 + lot); + auto& registry = CDClientManager::GetEntriesMutable(); + registry.insert_or_assign(static_cast(lot), componentID); + registry.insert_or_assign(static_cast(eReplicaComponentType::ITEM) << 32 | static_cast(lot), componentID); + CDItemComponent component{}; + component.id = componentID; + component.stackSize = 999; + CDClientManager::GetEntriesMutable().insert_or_assign(componentID, component); + } + + static dServerMock& Server() { return *static_cast(Game::server); } + + static void ClearSent() { Server().ClearSentPackets(); } + + // The extra info of every AddItemToInventoryClientSync sent + static std::vector AddedExtraInfo() { + std::vector out; + for (const auto& packet : Server().GetSentPackets()) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + LWOOBJID target{}; + MessageType::Game id{}; + if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, id)) continue; + if (id != MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC) continue; + GameMessages::AddItemToInventoryClientSync msg; + EXPECT_TRUE(msg.Deserialize(bitStream)); + out.push_back(msg.extraInfo); + } + return out; + } + + static std::vector Removed() { + std::vector out; + for (const auto& packet : Server().GetSentPackets()) { + RakNet::BitStream bitStream(const_cast(packet.bytes.data()), packet.bytes.size(), false); + LWOOBJID target{}; + MessageType::Game id{}; + if (!GameMessages::NetGameMsg::ReadPacketHeader(bitStream, target, id)) continue; + if (id != MessageType::Game::REMOVE_ITEM_FROM_INVENTORY) continue; + auto& msg = out.emplace_back(); + EXPECT_TRUE(msg.Deserialize(bitStream)); + } + return out; + } + + // Drops an item for the player from the given source object and picks it up + void DropAndPickUp(const LWOOBJID source, const LWOOBJID lootId) { + GameMessages::DropClientLoot drop; + drop.target = PLAYER; + drop.ownerID = PLAYER; + drop.sourceID = source; + drop.item = ITEM; + drop.count = 1; + drop.lootID = lootId; + GameMessages::DropClientLootEvent event(drop); + entity->MsgDropClientLoot(event); + ClearSent(); + entity->PickupItem(lootId); + } +}; + +TEST_F(LootMetricsTests, KeysAsLiveWroteThem) { + EXPECT_TRUE(LootMetrics{}.Empty()); + EXPECT_EQ(LootMetrics{}.ToExtraInfo(), u""); + + LootMetrics pickup{}; + pickup.sourceLot = 1; + EXPECT_EQ(pickup.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:1"); + + LootMetrics mission{}; + mission.sourceLot = 1; + mission.missionId = 889; + EXPECT_EQ(mission.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:1,_Metric_Mission_ID_Int=1:889"); + + LootMetrics vendor{}; + vendor.sourceLot = 13380; + vendor.currencyDelta = -20000; + EXPECT_EQ(vendor.ToExtraInfo(), u"_Metric_Souce_LOT_Int=1:13380,_Metric_Currency_Delta_Int=1:-20000"); + + // Live: the mail ID as name value type 8, the trade ID as type 9 + LootMetrics mail{}; + mail.mailId = 1152921510716557582ULL; + EXPECT_EQ(mail.ToExtraInfo(), u"_Metric_Mail_ID_Int64=8:1152921510716557582"); + LootMetrics trade{}; + trade.transactionId = 1152921510834827094LL; + EXPECT_EQ(trade.ToExtraInfo(), u"_Metric_Transaction_ID_Int64=9:1152921510834827094"); +} + +TEST_F(LootMetricsTests, PickupSaysWhichObjectItDroppedFrom) { + info.lot = SMASHABLE; + auto* const smashable = Game::entityManager->CreateEntity(info, nullptr, nullptr, false, 0x40040000000100LL); + ASSERT_NE(smashable, nullptr); + + DropAndPickUp(smashable->GetObjectID(), 0x5000); + EXPECT_EQ(AddedExtraInfo(), std::vector{ u"_Metric_Souce_LOT_Int=1:4712" }); + + // A second one joins the stack; the message still says where it came from + DropAndPickUp(smashable->GetObjectID(), 0x5001); + EXPECT_EQ(AddedExtraInfo(), std::vector{ u"_Metric_Souce_LOT_Int=1:4712" }); +} + +TEST_F(LootMetricsTests, PlayerSourcedPickupIsLotOne) { + // Activity rewards and chests drop from the player (live: 1 on 741 pickups) + DropAndPickUp(PLAYER, 0x5002); + EXPECT_EQ(AddedExtraInfo(), std::vector{ u"_Metric_Souce_LOT_Int=1:1" }); +} + +TEST_F(LootMetricsTests, UnknownSourceSendsNoKey) { + DropAndPickUp(0x40040000000999LL, 0x5003); + EXPECT_EQ(AddedExtraInfo(), std::vector{ u"" }); +} + +TEST_F(LootMetricsTests, MetricsFollowTheItemConfig) { + LwoNameValue config; + config.Insert(u"a", 5); + LootMetrics metrics{}; + metrics.sourceLot = 1; + inventory->AddItem(ITEM, 1, eLootSourceType::PICKUP, eInventoryType::INVALID, config, LWOOBJID_EMPTY, true, false, LWOOBJID_EMPTY, eInventoryType::INVALID, 0, false, -1, metrics); + EXPECT_EQ(AddedExtraInfo(), std::vector{ u"a=1:5,_Metric_Souce_LOT_Int=1:1" }); + // Only the message carries them + const auto* const item = inventory->FindItemByLot(ITEM); + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->GetConfig().values.size(), 1u); +} + +TEST_F(LootMetricsTests, RemovalSaysWhatTookTheItems) { + inventory->AddItem(ITEM, 5, eLootSourceType::NONE); + ClearSent(); + constexpr LWOOBJID QUICKBUILD = 0x40040000000200LL; + ASSERT_TRUE(inventory->RemoveItem(ITEM, 5, eInventoryType::ALL, false, false, { eLootSourceType::QUICKBUILD, QUICKBUILD })); + const auto removed = Removed(); + ASSERT_EQ(removed.size(), 1u); + EXPECT_EQ(removed[0].eLootTypeSource, static_cast(eLootSourceType::QUICKBUILD)); + EXPECT_EQ(removed[0].iLootTypeSource, QUICKBUILD); + EXPECT_EQ(removed[0].iStackCount, 5u); + + // Without a source, as before + inventory->AddItem(ITEM, 1, eLootSourceType::NONE); + ClearSent(); + ASSERT_TRUE(inventory->RemoveItem(ITEM, 1)); + ASSERT_EQ(Removed().size(), 1u); + EXPECT_EQ(Removed()[0].eLootTypeSource, LOOTTYPE_NONE); + EXPECT_EQ(Removed()[0].iLootTypeSource, LWOOBJID_EMPTY); +} + +// Live: the FV Stone Warrior pedestal's 5 Maelstrom Infected Bricks were taken with the loot source Quickbuild and the +// quickbuild as the source object +TEST_F(LootMetricsTests, QuickbuildTakesItsCostAsTheSource) { + constexpr LOT BRICK = 6194; + constexpr int32_t PRECONDITION = 90099; // stands in for 99: have 5 of LOT 6194 + RegisterLot(BRICK); + CDClientDatabase::ExecuteDML("CREATE TABLE IF NOT EXISTS Preconditions (id INTEGER, type INTEGER, targetLOT TEXT, targetCount INTEGER);"); + CDClientDatabase::ExecuteDML("INSERT INTO Preconditions VALUES (" + std::to_string(PRECONDITION) + ", 2, '6194', 5);"); + inventory->AddItem(BRICK, 7, eLootSourceType::NONE); + // What the precondition check looks at besides the inventory + entity->AddComponent(-1); + entity->AddComponent(-1); + entity->AddComponent(-1); + + auto pedestalInfo = info; + pedestalInfo.lot = 8551; + auto pedestal = std::make_unique(0x0102030405060708LL, pedestalInfo); + pedestal->SetVar(u"CheckPrecondition", GeneralUtils::ASCIIToUTF16(std::to_string(PRECONDITION))); + auto* const quickBuild = pedestal->AddComponent(-1); + quickBuild->SetResetTime(20.0f); + quickBuild->SetCompleteTime(10.0f); + ClearSent(); + + quickBuild->OnUse(entity.get()); + EXPECT_EQ(quickBuild->GetState(), eQuickBuildState::BUILDING); + const auto removed = Removed(); + ASSERT_EQ(removed.size(), 1u); + EXPECT_EQ(removed[0].iObjTemplate, BRICK); + EXPECT_EQ(removed[0].iStackCount, 5u); + EXPECT_EQ(removed[0].eLootTypeSource, static_cast(eLootSourceType::QUICKBUILD)); + EXPECT_EQ(removed[0].iLootTypeSource, pedestal->GetObjectID()); + EXPECT_EQ(inventory->GetLotCount(BRICK), 2u); + + pedestal.reset(); +}