mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
fix(inventory): write is_bound, inventory_type and NPC item slots like live
Equipped items in the inventory component construction, as live wrote them (49,534 equipped items in the captures): - is_bound is the item's bound state (false for 2,990 items that bind neither way); DLU always wrote true. NPC items are bound when the item binds on pickup or on equip (2,640 of 2,640). - inventory_type is the item's inventory, left out for ITEMS: TempEquip for temporary equips and proxies (3,899), Model for models (232). DLU never wrote it. DLU keeps player proxies in ITEM_SETS; they are sent as TEMP_ITEMS, like live. - An NPC item's slot is its slot in the inventory it would be in: models count from 0 in MODELS and proxies from 0 in TEMP_ITEMS, so a proxy no longer takes the next ITEMS slot (1,475 of 1,491 NPC constructions match; DLU's numbering matched 1,352). - equipped_model_transforms is an empty list on every construction (11,214 of 11,214); DLU wrote none. Test: InventoryConstructionTest.NpcItemsLikeLive (NPCs 7426 and 13790 from the captures), InventoryConstructionTest.PlayerItemsLikeLive. Check in game: NPCs with gear (Numb Chuck in FV, the Ninjago ninjas, faction vendors) show all their gear; your own and other players' equipped items, proxies (ninja hoods, capes) and temporary equips (Maelstrom vacuum, quest items) show normally after zoning and after equipping/unequipping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,38 @@
|
||||
|
||||
#include <ranges>
|
||||
|
||||
namespace {
|
||||
// The equipped-item entry for a player's item. Live wrote its bound state and its inventory; DLU keeps proxies in
|
||||
// ITEM_SETS, which live kept in TEMP_ITEMS (inventory_type TempEquip on every live proxy).
|
||||
EquippedItem ToEquippedItem(const Item& item, const bool withConfig) {
|
||||
EquippedItem equipped{ item.GetId(), item.GetLot(), item.GetCount(), item.GetSlot() };
|
||||
if (withConfig) equipped.config = item.GetConfig();
|
||||
equipped.bound = item.GetBound();
|
||||
const auto inventoryType = item.GetInventory() ? item.GetInventory()->GetType() : ITEMS;
|
||||
equipped.inventoryType = inventoryType == ITEM_SETS ? TEMP_ITEMS : inventoryType;
|
||||
return equipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Equipped-item entries for an NPC's CDClient items, as live wrote them: each item's slot is its slot in the
|
||||
* inventory it would be in (models count from 0 in MODELS, proxies from 0 in TEMP_ITEMS), the count is the row's,
|
||||
* and is_bound is set for bind-on-pickup and bind-on-equip items.
|
||||
*/
|
||||
class NpcEquipSlots {
|
||||
public:
|
||||
EquippedItem Make(const LOT lot, const uint32_t count, const bool isProxy) {
|
||||
const auto& info = Inventory::FindItemComponent(lot);
|
||||
const auto inventoryType = isProxy ? TEMP_ITEMS : Inventory::FindInventoryTypeForLot(lot);
|
||||
EquippedItem item{ ObjectIDManager::GenerateObjectID(), lot, count, m_NextSlot[inventoryType]++ };
|
||||
item.bound = info.isBOP || info.isBOE;
|
||||
item.inventoryType = inventoryType;
|
||||
return item;
|
||||
}
|
||||
private:
|
||||
std::map<eInventoryType, uint32_t> m_NextSlot;
|
||||
};
|
||||
}
|
||||
|
||||
InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) {
|
||||
RegisterMsg(&InventoryComponent::OnGetObjectReportInfo);
|
||||
this->m_Dirty = true;
|
||||
@@ -85,7 +117,7 @@ InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID
|
||||
auto* inventoryComponentTable = CDClientManager::GetTable<CDInventoryComponentTable>();
|
||||
auto items = inventoryComponentTable->Query([=](const CDInventoryComponent entry) { return entry.id == componentId; });
|
||||
|
||||
auto slot = 0u;
|
||||
NpcEquipSlots slots;
|
||||
|
||||
for (const auto& item : items) {
|
||||
if (!Inventory::IsValidItem(item.itemid)) continue;
|
||||
@@ -93,11 +125,9 @@ InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID
|
||||
|
||||
if (!item.equip) continue;
|
||||
|
||||
const LWOOBJID id = ObjectIDManager::GenerateObjectID();
|
||||
|
||||
const auto& info = Inventory::FindItemComponent(item.itemid);
|
||||
|
||||
UpdateSlot(info.equipLocation, { id, static_cast<LOT>(item.itemid), item.count, slot++ });
|
||||
UpdateSlot(info.equipLocation, slots.Make(static_cast<LOT>(item.itemid), item.count, false));
|
||||
|
||||
// Equip this items proxies.
|
||||
auto subItems = info.subItems;
|
||||
@@ -111,10 +141,9 @@ InventoryComponent::InventoryComponent(Entity* parent, const int32_t componentID
|
||||
const auto proxyLOT = static_cast<LOT>(std::stoi(proxyLotAsString));
|
||||
|
||||
const auto& proxyInfo = Inventory::FindItemComponent(proxyLOT);
|
||||
const LWOOBJID proxyId = ObjectIDManager::GenerateObjectID();
|
||||
|
||||
// Use item.count since we equip item.count number of the item this is a requested proxy of
|
||||
UpdateSlot(proxyInfo.equipLocation, { proxyId, proxyLOT, item.count, slot++ });
|
||||
UpdateSlot(proxyInfo.equipLocation, slots.Make(proxyLOT, item.count, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,7 +774,7 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
|
||||
if (equipped) {
|
||||
const auto info = Inventory::FindItemComponent(lot);
|
||||
|
||||
UpdateSlot(info.equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot() });
|
||||
UpdateSlot(info.equipLocation, ToEquippedItem(*item, false));
|
||||
|
||||
AddItemSkills(item->GetLot());
|
||||
}
|
||||
@@ -894,7 +923,9 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
|
||||
outBitStream.Write(item.slot != 0);
|
||||
if (item.slot != 0) outBitStream.Write<uint16_t>(item.slot);
|
||||
|
||||
outBitStream.Write0();
|
||||
// Live left the inventory out for ITEMS and wrote it for the rest (TempEquip, Model).
|
||||
outBitStream.Write(item.inventoryType != ITEMS);
|
||||
if (item.inventoryType != ITEMS) outBitStream.Write<uint32_t>(item.inventoryType);
|
||||
|
||||
bool flag = !item.config.values.empty();
|
||||
outBitStream.Write(flag);
|
||||
@@ -916,7 +947,7 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
|
||||
outBitStream.Write(ldfStream);
|
||||
}
|
||||
|
||||
outBitStream.Write1();
|
||||
outBitStream.Write(item.bound);
|
||||
}
|
||||
|
||||
m_Dirty = false;
|
||||
@@ -924,7 +955,9 @@ void InventoryComponent::Serialize(RakNet::BitStream& outBitStream, const bool b
|
||||
outBitStream.Write(false);
|
||||
}
|
||||
|
||||
outBitStream.Write(false);
|
||||
// Equipped model transforms: live wrote an empty list on every construction, and nothing afterwards.
|
||||
outBitStream.Write(bIsInitialUpdate);
|
||||
if (bIsInitialUpdate) outBitStream.Write<uint32_t>(0);
|
||||
}
|
||||
|
||||
void InventoryComponent::Update(float deltaTime) {
|
||||
@@ -1061,7 +1094,7 @@ void InventoryComponent::EquipItem(Item* item, const bool skipChecks) {
|
||||
// skills change and the item's proxies are added
|
||||
SendEquipState(*item, true);
|
||||
|
||||
UpdateSlot(item->GetInfo().equipLocation, { item->GetId(), item->GetLot(), item->GetCount(), item->GetSlot(), item->GetConfig() });
|
||||
UpdateSlot(item->GetInfo().equipLocation, ToEquippedItem(*item, true));
|
||||
|
||||
ApplyBuff(item);
|
||||
|
||||
@@ -1560,14 +1593,12 @@ std::vector<uint32_t> InventoryComponent::FindBuffs(Item* item, bool castOnEquip
|
||||
void InventoryComponent::SetNPCItems(const std::vector<LOT>& items) {
|
||||
m_Equipped.clear();
|
||||
|
||||
auto slot = 0u;
|
||||
NpcEquipSlots slots;
|
||||
|
||||
for (const auto& item : items) {
|
||||
const LWOOBJID id = ObjectIDManager::GenerateObjectID();
|
||||
|
||||
const auto& info = Inventory::FindItemComponent(item);
|
||||
|
||||
UpdateSlot(info.equipLocation, { id, static_cast<LOT>(item), 1, slot++ }, true);
|
||||
UpdateSlot(info.equipLocation, slots.Make(static_cast<LOT>(item), 1, false), true);
|
||||
}
|
||||
|
||||
Game::entityManager->SerializeEntity(m_Parent);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "LDFFormat.h"
|
||||
#include "eInventoryType.h"
|
||||
|
||||
/**
|
||||
* An item that's equipped, generally as a smaller return type than the regular Item class
|
||||
@@ -32,4 +33,15 @@ struct EquippedItem
|
||||
* The configuration of the item with any extra data
|
||||
*/
|
||||
LwoNameValue config = {};
|
||||
|
||||
/**
|
||||
* Whether the item is bound to its owner (the construction's is_bound)
|
||||
*/
|
||||
bool bound = true;
|
||||
|
||||
/**
|
||||
* The inventory the item is in (the construction's inventory_type): TEMP_ITEMS for temporary equips and
|
||||
* proxies, MODELS for models, ITEMS for the rest
|
||||
*/
|
||||
eInventoryType inventoryType = ITEMS;
|
||||
};
|
||||
|
||||
@@ -129,3 +129,154 @@ TEST_F(ReplicaConstructionTest, CharacterConstructionAlwaysWritesGmActivityAndSo
|
||||
|
||||
player.SetCharacter(nullptr);
|
||||
}
|
||||
|
||||
class InventoryConstructionTest : public GameDependenciesTest {
|
||||
protected:
|
||||
// The items of the live sample below (CDClient 1.10.64 values)
|
||||
static constexpr LOT HOOD = 2642; // hair, bind on pickup, proxy 10482
|
||||
static constexpr LOT HOOD_PROXY = 10482; // clavicle, bind on equip
|
||||
static constexpr LOT NPC_HELMET = 8520; // hair, item type 22 (a model), bind on equip
|
||||
static constexpr LOT NPC_KNIFE = 16682; // special_r, bind on equip
|
||||
static constexpr LOT PLAIN_SHIRT = 4000; // chest, bound neither way
|
||||
|
||||
void SetUp() override {
|
||||
SetUpDependencies();
|
||||
CDClientDatabase::Connect(":memory:");
|
||||
CDClientDatabase::ExecuteDML("CREATE TABLE ItemSets (setID INTEGER, itemIDs TEXT);");
|
||||
RegisterItem(HOOD, "hair", 2, true, false, std::to_string(HOOD_PROXY));
|
||||
RegisterItem(HOOD_PROXY, "clavicle", 4, false, true);
|
||||
RegisterItem(NPC_HELMET, "hair", 22, false, true);
|
||||
RegisterItem(NPC_KNIFE, "special_r", 6, false, true);
|
||||
RegisterItem(PLAIN_SHIRT, "chest", 15, false, false);
|
||||
}
|
||||
|
||||
void TearDown() override { TearDownDependencies(); }
|
||||
|
||||
static void RegisterItem(const LOT lot, const std::string& equipLocation, const int32_t itemType, const bool isBOP, const bool isBOE, const std::string& subItems = "") {
|
||||
const auto componentID = static_cast<uint32_t>(97000 + lot);
|
||||
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
|
||||
registry.insert_or_assign(static_cast<uint64_t>(lot), componentID);
|
||||
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::ITEM) << 32 | static_cast<uint64_t>(lot), componentID);
|
||||
CDItemComponent component{};
|
||||
component.id = componentID;
|
||||
component.equipLocation = equipLocation;
|
||||
component.itemType = itemType;
|
||||
component.isBOP = isBOP;
|
||||
component.isBOE = isBOE;
|
||||
component.subItems = subItems;
|
||||
CDClientManager::GetEntriesMutable<CDItemComponentTable>().insert_or_assign(componentID, component);
|
||||
}
|
||||
|
||||
static void RegisterNpc(const LOT npc, const uint32_t componentID, const std::vector<LOT>& items) {
|
||||
auto& registry = CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>();
|
||||
registry.insert_or_assign(static_cast<uint64_t>(npc), 0); // the LOT's components are cached
|
||||
registry.insert_or_assign(static_cast<uint64_t>(eReplicaComponentType::INVENTORY) << 32 | static_cast<uint64_t>(npc), componentID);
|
||||
auto& table = CDClientManager::GetEntriesMutable<CDInventoryComponentTable>();
|
||||
for (const auto item : items) table.push_back({ componentID, static_cast<uint32_t>(item), 1, true });
|
||||
}
|
||||
|
||||
// One EquippedItemInfo the way lu_packets reads it
|
||||
static void WriteItem(RakNet::BitStream& out, const LWOOBJID id, const LOT lot, const uint16_t slot, const uint32_t inventoryType, const bool bound) {
|
||||
out.Write(id);
|
||||
out.Write(lot);
|
||||
out.Write0(); // subkey
|
||||
out.Write1(); // count
|
||||
out.Write<uint32_t>(1);
|
||||
out.Write(slot != 0);
|
||||
if (slot != 0) out.Write(slot);
|
||||
out.Write(inventoryType != 0);
|
||||
if (inventoryType != 0) out.Write(inventoryType);
|
||||
out.Write0(); // extra_info
|
||||
out.Write(bound);
|
||||
}
|
||||
|
||||
static std::vector<EquippedItem> Equipped(InventoryComponent& inventory) {
|
||||
std::vector<EquippedItem> items;
|
||||
for (const auto& item : inventory.GetEquippedItems() | std::views::values) items.push_back(item);
|
||||
return items;
|
||||
}
|
||||
};
|
||||
|
||||
// Live, NPC LOT 7426 (FV Numb Chuck; capture 60a58346ece1 idx 58): the helmet, a model, is in the MODELS inventory, so
|
||||
// it has inventory_type Model and slot 0 like the knife in ITEMS; both bind on equip, so both are bound.
|
||||
// Live, NPC LOT 13790 (NJ Cole; capture 8846569a7d52 idx 10614): the hood's proxy is TempEquip, slot 0, bound.
|
||||
// Every live construction ended with an empty equipped_model_transforms list.
|
||||
TEST_F(InventoryConstructionTest, NpcItemsLikeLive) {
|
||||
RegisterNpc(7426, 263, { NPC_HELMET, NPC_KNIFE });
|
||||
info.lot = 7426;
|
||||
Entity numbChuck(288300744895900001, info);
|
||||
auto* const inventory = numbChuck.AddComponent<InventoryComponent>(-1);
|
||||
auto items = Equipped(*inventory);
|
||||
ASSERT_EQ(items.size(), 2u);
|
||||
|
||||
RakNet::BitStream construction;
|
||||
inventory->Serialize(construction, true);
|
||||
RakNet::BitStream expected;
|
||||
expected.Write1();
|
||||
expected.Write<uint32_t>(2);
|
||||
WriteItem(expected, items[0].id, NPC_HELMET, 0, 5 /* Model */, true); // "hair" sorts before "special_r"
|
||||
WriteItem(expected, items[1].id, NPC_KNIFE, 0, 0, true);
|
||||
expected.Write1(); // equipped_model_transforms Some([])
|
||||
expected.Write<uint32_t>(0);
|
||||
ExpectSameBits(construction, expected);
|
||||
|
||||
RegisterNpc(13790, 516, { HOOD });
|
||||
info.lot = 13790;
|
||||
Entity cole(288300744895900002, info);
|
||||
auto* const coleInventory = cole.AddComponent<InventoryComponent>(-1);
|
||||
items = Equipped(*coleInventory);
|
||||
ASSERT_EQ(items.size(), 2u);
|
||||
|
||||
RakNet::BitStream coleConstruction;
|
||||
coleInventory->Serialize(coleConstruction, true);
|
||||
RakNet::BitStream coleExpected;
|
||||
coleExpected.Write1();
|
||||
coleExpected.Write<uint32_t>(2);
|
||||
WriteItem(coleExpected, items[0].id, HOOD_PROXY, 0, 4 /* TempEquip */, true); // "clavicle" before "hair"
|
||||
WriteItem(coleExpected, items[1].id, HOOD, 0, 0, true);
|
||||
coleExpected.Write1();
|
||||
coleExpected.Write<uint32_t>(0);
|
||||
ExpectSameBits(coleConstruction, coleExpected);
|
||||
|
||||
// A serialization with nothing changed writes neither list
|
||||
RakNet::BitStream serialization;
|
||||
coleInventory->Serialize(serialization, false);
|
||||
EXPECT_EQ(serialization.GetNumberOfBitsUsed(), 2u);
|
||||
}
|
||||
|
||||
// A player's items: is_bound is the item's bound state (live: bind-on-pickup and equipped bind-on-equip items true,
|
||||
// items that bind neither way false, 2,990 of 49,534), and proxies are TempEquip.
|
||||
TEST_F(InventoryConstructionTest, PlayerItemsLikeLive) {
|
||||
info.lot = 1;
|
||||
CDClientManager::GetEntriesMutable<CDComponentsRegistryTable>().insert_or_assign(static_cast<uint64_t>(info.lot), 0);
|
||||
Entity player(0x1000000000000001LL, info);
|
||||
auto* const inventory = player.AddComponent<InventoryComponent>(-1);
|
||||
Character character(1, nullptr);
|
||||
player.SetCharacter(&character);
|
||||
player.AddComponent<CharacterComponent>(-1, &character, UNASSIGNED_SYSTEM_ADDRESS)->InitializeStatisticsFromString("");
|
||||
|
||||
auto* const bag = inventory->GetInventory(eInventoryType::ITEMS);
|
||||
// A bind-on-pickup item is bound from the moment it is picked up
|
||||
auto* const hood = new Item(0x1000000000007000LL, HOOD, bag, 0, 1, true, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
|
||||
auto* const shirt = new Item(0x1000000000007001LL, PLAIN_SHIRT, bag, 1, 1, false, {}, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE);
|
||||
inventory->EquipItem(hood);
|
||||
inventory->EquipItem(shirt);
|
||||
|
||||
LWOOBJID proxyID = LWOOBJID_EMPTY;
|
||||
for (const auto& item : Equipped(*inventory)) if (item.lot == HOOD_PROXY) proxyID = item.id;
|
||||
ASSERT_NE(proxyID, LWOOBJID_EMPTY);
|
||||
|
||||
RakNet::BitStream construction;
|
||||
inventory->Serialize(construction, true);
|
||||
RakNet::BitStream expected;
|
||||
expected.Write1();
|
||||
expected.Write<uint32_t>(3);
|
||||
WriteItem(expected, shirt->GetId(), PLAIN_SHIRT, 1, 0, false); // "chest", "clavicle", "hair"
|
||||
WriteItem(expected, proxyID, HOOD_PROXY, 0, 4 /* TempEquip */, true);
|
||||
WriteItem(expected, hood->GetId(), HOOD, 0, 0, true);
|
||||
expected.Write1();
|
||||
expected.Write<uint32_t>(0);
|
||||
ExpectSameBits(construction, expected);
|
||||
|
||||
player.SetCharacter(nullptr);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user