diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index 26aa1a871..5935c13a5 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -228,7 +228,8 @@ void InventoryComponent::AddItem( const eInventoryType inventorySourceType, const int32_t sourceType, const bool bound, - int32_t preferredSlot) { + int32_t preferredSlot, + const LootMetrics& metrics) { if (count == 0) { LOG("Attempted to add 0 of item (%i) to the inventory!", lot); @@ -262,7 +263,7 @@ void InventoryComponent::AddItem( return; } - auto* item = new Item(lot, inventory, slot, count, config, parent, showFlyingLoot, isModMoveAndEquip, subKey, bound, lootSourceType); + auto* item = new Item(lot, inventory, slot, count, config, parent, showFlyingLoot, isModMoveAndEquip, subKey, bound, lootSourceType, metrics); if (missions != nullptr && !IsTransferInventory(inventoryType)) { missions->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", count, IsTransferInventory(inventorySourceType)); @@ -295,7 +296,7 @@ void InventoryComponent::AddItem( left -= delta; - existing->SetCount(existing->GetCount() + delta, false, true, showFlyingLoot, lootSourceType); + existing->SetCount(existing->GetCount() + delta, false, true, showFlyingLoot, lootSourceType, metrics); if (isModMoveAndEquip) { existing->Equip(); @@ -348,7 +349,7 @@ void InventoryComponent::AddItem( continue; } - auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType); + auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType, metrics); isModMoveAndEquip = false; } @@ -374,6 +375,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c GameMessages::AddItemToInventoryClientSync addItem; addItem.target = m_Parent->GetObjectID(); addItem.SetItem(*item); + addItem.AddMetrics(options.metrics); addItem.eLootTypeSource = lootSourceType; addItem.iSubkey = subKey; addItem.itemCount = static_cast(count); @@ -398,7 +400,7 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c for (const auto& [itemId, item] : inventory->GetItems()) { if (item->GetLot() == lot) before[itemId] = item->GetCount(); } - AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot); + AddItem(lot, count, lootSourceType, inventoryType, config, LWOOBJID_EMPTY, options.showFlyingLoot, options.equip, subKey, options.sourceInventory, 0, bound, options.preferredSlot, options.metrics); LWOOBJID receivedBy = LWOOBJID_EMPTY; int64_t largestGain = 0; @@ -420,7 +422,7 @@ bool InventoryComponent::IsUniqueLot(const LOT lot) { return !isBrick && info.stackSize <= 1; } -bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent) { +bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInventoryType inventoryType, const bool ignoreBound, const bool silent, const ItemRemovalSource& removalSource) { if (count == 0) { LOG("Attempted to remove 0 of item (%i) from the inventory!", lot); return false; @@ -437,7 +439,7 @@ bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInvent auto* item = FindItemByLot(lot, inventoryType, false, ignoreBound); if (!item) break; const auto delta = std::min(left, item->GetCount()); - item->SetCount(item->GetCount() - delta, silent); + item->SetCount(item->GetCount() - delta, silent, true, true, eLootSourceType::NONE, {}, removalSource); left -= delta; } return true; @@ -448,7 +450,7 @@ bool InventoryComponent::RemoveItem(const LOT lot, const uint32_t count, eInvent auto* item = inventory->FindItemByLot(lot, false, ignoreBound); if (!item) break; const auto delta = std::min(item->GetCount(), left); - item->SetCount(item->GetCount() - delta, silent); + item->SetCount(item->GetCount() - delta, silent, true, true, eLootSourceType::NONE, {}, removalSource); left -= delta; } } diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index 202399065..5a452a796 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -23,6 +23,7 @@ #include "eReplicaComponentType.h" #include "eLootSourceType.h" #include "Loot.h" +#include "LootMetrics.h" class Entity; class ItemSet; @@ -57,6 +58,7 @@ struct ReceiveItemOptions { bool equip = false; int32_t preferredSlot = -1; eInventoryType sourceInventory = INVALID; // set when moving between the player's own inventories + LootMetrics metrics{}; // where the items came from, sent with them (e.g. the mail or trade ID) }; // Which item or stack ReceiveItem put the items in @@ -153,6 +155,7 @@ public: * @param bound whether this item is bound * @param preferredSlot the preferred slot to store this item * @param lootSourceType The source of the loot. Defaults to none. + * @param metrics where the items came from (source LOT, mission, activity...), sent with them and not kept */ void AddItem( LOT lot, @@ -167,7 +170,8 @@ public: eInventoryType inventorySourceType = INVALID, int32_t sourceType = 0, bool bound = false, - int32_t preferredSlot = -1 + int32_t preferredSlot = -1, + const LootMetrics& metrics = {} ); /** @@ -190,8 +194,9 @@ public: * @param inventoryType optional inventory type to remove the item from * @param ignoreBound ignores bound items * @param silent silently remove the item + * @param removalSource what took the items, sent with the removal (e.g. a quickbuild taking its item cost) */ - bool RemoveItem(LOT lot, uint32_t count, eInventoryType inventoryType = INVALID, bool ignoreBound = false, bool silent = false); + bool RemoveItem(LOT lot, uint32_t count, eInventoryType inventoryType = INVALID, bool ignoreBound = false, bool silent = false, const ItemRemovalSource& removalSource = {}); /** * Moves an existing item to an inventory of the entity diff --git a/dGame/dGameMessages/InventoryMessages.cpp b/dGame/dGameMessages/InventoryMessages.cpp index 83fbf5693..593895b53 100644 --- a/dGame/dGameMessages/InventoryMessages.cpp +++ b/dGame/dGameMessages/InventoryMessages.cpp @@ -9,6 +9,7 @@ #include "Inventory.h" #include "InventoryComponent.h" #include "Item.h" +#include "LootMetrics.h" #include "BrickByBrick.h" #include @@ -53,6 +54,13 @@ namespace GameMessages { slotID = item.GetSlot(); } + void AddItemToInventoryClientSync::AddMetrics(const LootMetrics& metrics) { + const auto text = metrics.ToExtraInfo(); + if (text.empty()) return; + if (!extraInfo.empty()) extraInfo += u','; + extraInfo += text; + } + void AddItemToInventoryClientSync::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(bBound); bitStream.Write(bIsBOE); diff --git a/dGame/dGameMessages/InventoryMessages.h b/dGame/dGameMessages/InventoryMessages.h index ad6e7925d..4b17b7142 100644 --- a/dGame/dGameMessages/InventoryMessages.h +++ b/dGame/dGameMessages/InventoryMessages.h @@ -17,6 +17,8 @@ class Item; // and groups. // Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. // Received messages are handled by the target's InventoryComponent. +struct LootMetrics; + namespace GameMessages { // Server -> client. struct AddItemToInventoryClientSync : public NetGameMsg { @@ -28,6 +30,9 @@ namespace GameMessages { // slot). The caller sets the rest. void SetItem(const Item& item); + // Adds where the items came from (LootMetrics) to the extra info, after the item's config + void AddMetrics(const LootMetrics& metrics); + bool bBound{}; bool bIsBOE{}; bool bIsBOP{}; diff --git a/dGame/dInventory/CMakeLists.txt b/dGame/dInventory/CMakeLists.txt index cefc57e22..bd27b47af 100644 --- a/dGame/dInventory/CMakeLists.txt +++ b/dGame/dInventory/CMakeLists.txt @@ -3,7 +3,8 @@ set(DGAME_DINVENTORY_SOURCES "Inventory.cpp" "Item.cpp" "ItemSet.cpp" - "ItemSetPassiveAbility.cpp") + "ItemSetPassiveAbility.cpp" + "LootMetrics.cpp") add_library(dInventory OBJECT ${DGAME_DINVENTORY_SOURCES}) target_include_directories(dInventory PUBLIC "." diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index 4a90955ab..eeb75ef47 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -82,7 +82,8 @@ Item::Item( bool isModMoveAndEquip, LWOOBJID subKey, bool bound, - eLootSourceType lootSourceType) { + eLootSourceType lootSourceType, + const LootMetrics& metrics) { if (!Inventory::IsValidItem(lot)) { return; } @@ -113,6 +114,7 @@ Item::Item( GameMessages::AddItemToInventoryClientSync addItem; addItem.target = entity->GetObjectID(); addItem.SetItem(*this); + addItem.AddMetrics(metrics); addItem.eLootTypeSource = lootSourceType; addItem.iSubkey = subKey; addItem.itemCount = static_cast(this->count); @@ -178,7 +180,7 @@ PreconditionExpression* Item::GetPreconditionExpression() const { return preconditions; } -void Item::SetCount(const uint32_t value, const bool silent, const bool disassemble, const bool showFlyingLoot, eLootSourceType lootSourceType) { +void Item::SetCount(const uint32_t value, const bool silent, const bool disassemble, const bool showFlyingLoot, eLootSourceType lootSourceType, const LootMetrics& metrics, const ItemRemovalSource& removalSource) { if (value == count) { return; } @@ -202,6 +204,7 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem GameMessages::AddItemToInventoryClientSync addItem; addItem.target = entity->GetObjectID(); addItem.SetItem(*this); + addItem.AddMetrics(metrics); addItem.eLootTypeSource = lootSourceType; addItem.itemCount = delta; addItem.newObjID = id; @@ -213,7 +216,8 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem removeItem.target = entity->GetObjectID(); removeItem.bConfirmed = true; removeItem.eInvType = inventory->GetType(); - removeItem.eLootTypeSource = LOOTTYPE_NONE; + removeItem.eLootTypeSource = static_cast(removalSource.type); + removeItem.iLootTypeSource = removalSource.object; removeItem.iObjID = id; removeItem.iObjTemplate = lot; removeItem.iStackCount = delta; diff --git a/dGame/dInventory/Item.h b/dGame/dInventory/Item.h index baccf28fb..8aedde8e9 100644 --- a/dGame/dInventory/Item.h +++ b/dGame/dInventory/Item.h @@ -8,6 +8,7 @@ #include "Preconditions.h" #include "eInventoryType.h" #include "eLootSourceType.h" +#include "LootMetrics.h" namespace tinyxml2 { class XMLElement; @@ -58,6 +59,7 @@ public: * @param isModMoveAndEquip equips the item * @param subKey optional subkey for this item, e.g. for pets * @param bound if the item should be bound + * @param metrics where the items came from, sent with the item (not kept) */ explicit Item( LOT lot, @@ -70,7 +72,8 @@ public: bool isModMoveAndEquip = false, LWOOBJID subKey = LWOOBJID_EMPTY, bool bound = false, - eLootSourceType lootSourceType = eLootSourceType::NONE + eLootSourceType lootSourceType = eLootSourceType::NONE, + const LootMetrics& metrics = {} ); ~Item(); @@ -93,8 +96,10 @@ public: * @param silent if true, the client will not be notified of the change with GMs * @param disassemble if items were removed, this returns all the sub parts of the item individually if it had assembly part lots * @param showFlyingLoot shows flying loot to the client, if not silent + * @param metrics where added items came from, sent with them (not kept) + * @param removalSource what took removed items away, sent with the removal */ - void SetCount(uint32_t value, bool silent = false, bool disassemble = true, bool showFlyingLoot = true, eLootSourceType lootSourceType = eLootSourceType::NONE); + void SetCount(uint32_t value, bool silent = false, bool disassemble = true, bool showFlyingLoot = true, eLootSourceType lootSourceType = eLootSourceType::NONE, const LootMetrics& metrics = {}, const ItemRemovalSource& removalSource = {}); /** * Returns the number of items this item represents (e.g. for stacks) diff --git a/dGame/dInventory/LootMetrics.cpp b/dGame/dInventory/LootMetrics.cpp new file mode 100644 index 000000000..5e4c7aaa3 --- /dev/null +++ b/dGame/dInventory/LootMetrics.cpp @@ -0,0 +1,27 @@ +#include "LootMetrics.h" + +#include "LDFFormat.h" + +namespace { + template + void Append(std::u16string& out, const char16_t* key, const std::optional& value) { + if (!value) return; + if (!out.empty()) out += u','; + out += GeneralUtils::ASCIIToUTF16(LDFData(key, *value).GetString()); + } +} + +bool LootMetrics::Empty() const { + return !sourceLot && !missionId && !activityId && !currencyDelta && !mailId && !transactionId; +} + +std::u16string LootMetrics::ToExtraInfo() const { + std::u16string out; + Append(out, u"_Metric_Souce_LOT_Int", sourceLot); + Append(out, u"_Metric_Mission_ID_Int", missionId); + Append(out, u"_Metric_Activity_ID_Int", activityId); + Append(out, u"_Metric_Currency_Delta_Int", currencyDelta); + Append(out, u"_Metric_Mail_ID_Int64", mailId); + Append(out, u"_Metric_Transaction_ID_Int64", transactionId); + return out; +} diff --git a/dGame/dInventory/LootMetrics.h b/dGame/dInventory/LootMetrics.h new file mode 100644 index 000000000..331dce257 --- /dev/null +++ b/dGame/dInventory/LootMetrics.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include + +#include "dCommonVars.h" +#include "eLootSourceType.h" + +/** + * Where added items came from, as live servers put it in the extra info of AddItemToInventoryClientSync next to the + * item's own config. Only the message carries these keys; the item does not keep them. + * Live: a picked up drop had _Metric_Souce_LOT_Int (the source object's LOT, 1 for a player sourced + * drop) on all 2,344 pickups of a DropClientLoot; mission and achievement rewards the mission ID and source LOT 1; + * activity rewards the activity ID and the activity object's LOT; vendor purchases the vendor's LOT and the coins paid + * (negative); mail the mail ID; trades the trade ID. Package contents had none. + */ +struct LootMetrics { + std::optional sourceLot; // _Metric_Souce_LOT_Int (the key is misspelt in live's data) + std::optional missionId; // _Metric_Mission_ID_Int + std::optional activityId; // _Metric_Activity_ID_Int + std::optional currencyDelta; // _Metric_Currency_Delta_Int + std::optional mailId; // _Metric_Mail_ID_Int64, name value type 8 + std::optional transactionId; // _Metric_Transaction_ID_Int64, name value type 9 + + [[nodiscard]] bool Empty() const; + + /** + * The keys as name value text (key=type:value), comma separated like an item's config in the extra info. + * Empty when there are none. + */ + [[nodiscard]] std::u16string ToExtraInfo() const; +}; + +// What took items away, for RemoveItemFromInventory's loot source fields (e.g. a quickbuild taking its item cost) +struct ItemRemovalSource { + eLootSourceType type = eLootSourceType::NONE; + LWOOBJID object = LWOOBJID_EMPTY; +};