From 20f70ac0de20376f57184896c31d7d81c11a545c Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 23:36:21 -0500 Subject: [PATCH] refactor: inventory and item game messages as structs Converts AddItemToInventoryClientSync, SetInventorySize, RemoveItemFromInventory, ConsumeClientItem, UseItemResult, UseItemRequirementsResponse, ResponseMoveItemBetweenInventoryTypes, NotifyNotEnoughInvSpace, UpdateInventoryUi, MarkInventoryItemAsActive and MoveInventoryBatch (11 old Send functions) and the received EquipInventory, UnEquipInventory, RemoveItemFromInventory, MoveItemInInventory, MoveItemBetweenInventoryTypes, RequestMoveItemBetweenInventoryTypes, PushEquippedItemsState, PopEquippedItemsState, ClientItemConsumed, UseNonEquipmentItem, SetConsumableItem, UpdateInventoryGroup and UpdateInventoryGroupContents (13 handlers) to NetGameMsgs in InventoryMessages.{h,cpp}. The received messages are registered in GameMessageHandler's map and handled by InventoryComponent (new On* methods holding the old handler logic verbatim); the old functions and switch cases are deleted. AddItemToInventoryClientSync::SetItem fills the fields that come from the Item. No wire change and no change in recipients. Kept as DLU has always sent them and documented: NotifyNotEnoughInvSpace goes out with the message ID of VehicleNotifyFinishedRace, and RemoveItemFromInventory always sets the flag of the fields DLU fills. The old SendMoveInventoryBatch was never called and wrote one flag bit fewer than the client reads (it had no moveSubkey); the struct follows the client's layout (1.10.64, LWOInventoryComponent_Common::msgMoveInventoryBatch at 00ce1310) and has no oracle. UnEquipInventory still ignores the trailing replacementObjectID the client sends, as before. Verified byte for byte against a frozen verbatim copy of the old functions over an input grid (AddItemToInventoryClientSync with real Items, extra info and bind flags), to one client and broadcast; received messages compared with the old handlers' read sequences and every truncated payload rejected; hand computed golden bytes; round trips; a deliberate width mutation made the tests fail. Co-Authored-By: Claude Opus 5.5 --- dGame/dComponents/InventoryComponent.cpp | 214 +++++- dGame/dComponents/InventoryComponent.h | 23 + dGame/dComponents/PetComponent.cpp | 20 +- dGame/dComponents/PossessorComponent.cpp | 8 +- dGame/dGameMessages/CMakeLists.txt | 1 + dGame/dGameMessages/GameMessageHandler.cpp | 75 +- dGame/dGameMessages/GameMessages.cpp | 681 +----------------- dGame/dGameMessages/GameMessages.h | 37 - dGame/dGameMessages/InventoryMessages.cpp | 458 ++++++++++++ dGame/dGameMessages/InventoryMessages.h | 314 ++++++++ dGame/dInventory/Inventory.cpp | 7 +- dGame/dInventory/Item.cpp | 53 +- .../dGameMessagesTests/CMakeLists.txt | 1 + .../InventoryMessagesTests.cpp | 519 +++++++++++++ .../Legacy/InventoryMessagesLegacy.h | 464 ++++++++++++ 15 files changed, 2092 insertions(+), 783 deletions(-) create mode 100644 dGame/dGameMessages/InventoryMessages.cpp create mode 100644 dGame/dGameMessages/InventoryMessages.h create mode 100644 tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp create mode 100644 tests/dGameTests/dGameMessagesTests/Legacy/InventoryMessagesLegacy.h diff --git a/dGame/dComponents/InventoryComponent.cpp b/dGame/dComponents/InventoryComponent.cpp index f3dc320a7..f5679a5a6 100644 --- a/dGame/dComponents/InventoryComponent.cpp +++ b/dGame/dComponents/InventoryComponent.cpp @@ -12,6 +12,8 @@ #include "ObjectIDManager.h" #include "MissionComponent.h" #include "GameMessages.h" +#include "InventoryMessages.h" +#include "Database.h" #include "SkillComponent.h" #include "Character.h" #include "EntityManager.h" @@ -324,7 +326,15 @@ ReceivedItem InventoryComponent::ReceiveItem(const LWOOBJID id, const LOT lot, c if (keepId) slot = options.preferredSlot != -1 && inventory->IsSlotEmpty(options.preferredSlot) ? options.preferredSlot : inventory->FindEmptySlot(); if (slot != -1) { auto* item = new Item(id, lot, inventory, slot, count, bound || Inventory::FindItemComponent(lot).isBOP, config, LWOOBJID_EMPTY, subKey, lootSourceType); - GameMessages::SendAddItemToInventoryClientSync(m_Parent, m_Parent->GetSystemAddress(), item, id, options.showFlyingLoot && !options.equip, static_cast(count), subKey, lootSourceType); + GameMessages::AddItemToInventoryClientSync addItem; + addItem.target = m_Parent->GetObjectID(); + addItem.SetItem(*item); + addItem.eLootTypeSource = lootSourceType; + addItem.iSubkey = subKey; + addItem.itemCount = static_cast(count); + addItem.newObjID = id; + addItem.showFlyingLoot = options.showFlyingLoot && !options.equip; + addItem.SendToClient(m_Parent->GetSystemAddress()); EconomyLedger::RecordItems(m_Parent, lot, count, static_cast(lootSourceType)); if (options.equip) { item->Equip(); @@ -578,7 +588,11 @@ bool InventoryComponent::HasSpaceForLoot(const Loot::Return& loot) { } if (slotsNeeded > 0) { - GameMessages::SendNotifyNotEnoughInvSpace(m_Parent->GetObjectID(), slotsNeeded, ITEMS, m_Parent->GetSystemAddress()); + GameMessages::NotifyNotEnoughInvSpace notEnoughSpace; + notEnoughSpace.target = m_Parent->GetObjectID(); + notEnoughSpace.freeSlotsNeeded = slotsNeeded; + notEnoughSpace.inventoryType = ITEMS; + notEnoughSpace.Send(m_Parent->GetSystemAddress()); return false; } @@ -1109,7 +1123,12 @@ void InventoryComponent::HandlePossession(Item* item) { Game::entityManager->ConstructEntity(mount); possessorComponent->Mount(mount); - GameMessages::SendMarkInventoryItemAsActive(m_Parent->GetObjectID(), true, eUnequippableActiveType::MOUNT, item->GetId(), m_Parent->GetSystemAddress()); + GameMessages::MarkInventoryItemAsActive markActive; + markActive.target = m_Parent->GetObjectID(); + markActive.bActive = true; + markActive.iType = eUnequippableActiveType::MOUNT; + markActive.itemID = item->GetId(); + markActive.Send(m_Parent->GetSystemAddress()); } void InventoryComponent::ApplyBuff(Item* item) const { @@ -1261,7 +1280,9 @@ void InventoryComponent::FixInvisibleItems() { for (int32_t i = 1; i < something + 1; i++) { // client loads 12 items every 1/8 seconds, we're adding a small hack to fix invisible inventory items due to closing the news screen too fast. m_Parent->AddCallbackTimer((arbitaryInventorySize / numberItemsLoadedPerFrame) * callbackTime * i, [this]() { - GameMessages::SendUpdateInventoryUi(m_Parent->GetObjectID(), m_Parent->GetSystemAddress()); + GameMessages::UpdateInventoryUi updateUi; + updateUi.target = m_Parent->GetObjectID(); + updateUi.SendToClient(m_Parent->GetSystemAddress()); }); } } @@ -1354,7 +1375,10 @@ void InventoryComponent::SpawnPet(Item* item) { auto destroyableComponent = m_Parent->GetComponent(); if (Game::config->GetValue("pets_take_imagination") == "1" && destroyableComponent && destroyableComponent->GetImagination() <= 0) { - GameMessages::SendUseItemRequirementsResponse(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), eUseItemResponse::NoImaginationForPet); + GameMessages::UseItemRequirementsResponse requirementsResponse; + requirementsResponse.target = m_Parent->GetObjectID(); + requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet; + requirementsResponse.SendToClient(m_Parent->GetSystemAddress()); return; } @@ -1982,3 +2006,183 @@ bool InventoryComponent::OnGetObjectReportInfo(GameMessages::GetObjectReportInfo return true; } + +void InventoryComponent::OnEquipInventory(const GameMessages::EquipInventory& msg) { + Item* item = FindItemById(msg.itemToEquip); + if (!item) return; + + item->Equip(); + + Game::entityManager->SerializeEntity(m_Parent); +} + +void InventoryComponent::OnUnEquipInventory(const GameMessages::UnEquipInventory& msg) { + auto* item = FindItemById(msg.itemToUnequip); + + if (!item) return; + + item->UnEquip(); + + Game::entityManager->SerializeEntity(m_Parent); +} + +void InventoryComponent::OnRemoveItemFromInventory(const GameMessages::RemoveItemFromInventory& msg) { + // this is used for a lot more than just inventory trashing (trades, vendors, etc.) but for now since it's just used for that, that's all im going to implement + auto* item = FindItemById(msg.iObjID); + + if (item == nullptr) { + return; + } + + const auto iStackCount = std::min(item->GetCount(), msg.iStackCount); + + if (msg.bConfirmed) { + const auto itemType = static_cast(item->GetInfo().itemType); + if (itemType == eItemType::MODEL || itemType == eItemType::LOOT_MODEL) { + item->DisassembleModel(iStackCount); + } else if (itemType == eItemType::VEHICLE) { + Database::Get()->DeleteUgcBuild(item->GetSubKey()); + } + auto lot = item->GetLot(); + item->SetCount(item->GetCount() - iStackCount, true); + Game::entityManager->SerializeEntity(m_Parent); + + auto* missionComponent = m_Parent->GetComponent(); + + if (missionComponent != nullptr) { + missionComponent->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -iStackCount); + } + } +} + +void InventoryComponent::OnMoveItemInInventory(const GameMessages::MoveItemInInventory& msg) { + auto* item = FindItemById(msg.iObjID); + + if (!item) { + return; + } + + MoveStack(item, static_cast(msg.destInvType), msg.slot); + Game::entityManager->SerializeEntity(m_Parent); +} + +void InventoryComponent::OnMoveItemBetweenInventoryTypes(const GameMessages::MoveItemBetweenInventoryTypes& msg) { + auto showFlyingLoot = msg.showFlyingLoot; + auto* item = FindItemById(msg.objectID); + + if (!item) { + // Attempt to find the item by lot in inventory A since A is the source inventory. + item = FindItemByLot(msg.templateID, static_cast(msg.inventoryTypeA)); + if (!item) { + // As a final resort, try to find the item in its default inventory based on type. + item = FindItemByLot(msg.templateID); + if (!item) { + return; + } + } + } + + if (m_Parent->GetCharacter()) { + if (m_Parent->GetCharacter()->GetBuildMode()) { + showFlyingLoot = false; + } + } + + MoveItemToInventory(item, msg.inventoryTypeB, msg.stackCount, showFlyingLoot); + Game::entityManager->SerializeEntity(m_Parent); +} + +void InventoryComponent::OnRequestMoveItemBetweenInventoryTypes(const GameMessages::RequestMoveItemBetweenInventoryTypes& msg, const SystemAddress& sysAddr) { + GameMessages::ResponseMoveItemBetweenInventoryTypes response; + response.target = m_Parent->GetObjectID(); + response.inventoryTypeDestination = msg.invTypeDst; + response.inventoryTypeSource = msg.invTypeSrc; + + if (msg.itemID != LWOOBJID_EMPTY) { + auto* item = FindItemById(msg.itemID); + + if (!item) { + response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_ITEM_NOT_FOUND; + response.SendToClient(sysAddr); + return; + } + + if (item->GetLot() == 6086) { // Thinking hat + response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_CANT_MOVE_THINKING_HAT; + response.SendToClient(sysAddr); + return; + } + + auto* destInv = GetInventory(msg.invTypeDst); + if (destInv && destInv->GetEmptySlots() == 0) { + response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_INV_FULL; + response.SendToClient(sysAddr); + return; + } + + // Despawn the pet if we are moving that pet to the vault. + auto* petComponent = PetComponent::GetActivePet(m_Parent->GetObjectID()); + if (petComponent && petComponent->GetDatabaseId() == item->GetSubKey()) { + DespawnPet(); + } + + MoveItemToInventory(item, msg.invTypeDst, msg.iStackCount, msg.showFlyingLoot, false, false, msg.destSlot); + response.response = eReponseMoveItemBetweenInventoryTypeCode::SUCCESS; + response.SendToClient(sysAddr); + } +} + +void InventoryComponent::OnClientItemConsumed(const GameMessages::ClientItemConsumed& msg) { + auto* item = FindItemById(msg.item); + if (item == nullptr) { + return; + } + LOT itemLot = item->GetLot(); + + item->Consume(); + + auto* missions = static_cast(m_Parent->GetComponent(eReplicaComponentType::MISSION)); + if (missions != nullptr) { + missions->Progress(eMissionTaskType::USE_ITEM, itemLot); + } +} + +void InventoryComponent::OnUseNonEquipmentItem(const GameMessages::UseNonEquipmentItem& msg) { + auto* item = FindItemById(msg.itemToUse); + + if (item) item->UseNonEquip(item); +} + +void InventoryComponent::OnUpdateInventoryGroup(const GameMessages::UpdateInventoryGroup& msg) { + GroupUpdate groupUpdate; + groupUpdate.groupId = msg.groupID; + groupUpdate.inventory = msg.inventoryType; + groupUpdate.groupName = GeneralUtils::UTF16ToWTF8(msg.groupName); + + if (msg.action == "ADD") groupUpdate.command = GroupUpdateCommand::ADD; + else if (msg.action == "MODIFY") groupUpdate.command = GroupUpdateCommand::MODIFY; + else if (msg.action == "REMOVE") groupUpdate.command = GroupUpdateCommand::REMOVE; + else { + LOG("Invalid action %s", msg.action.c_str()); + return; + } + + UpdateGroup(groupUpdate); +} + +void InventoryComponent::OnUpdateInventoryGroupContents(const GameMessages::UpdateInventoryGroupContents& msg) { + GroupUpdate groupUpdate; + + if (msg.action == "ADD") groupUpdate.command = GroupUpdateCommand::ADD_LOT; + else if (msg.action == "REMOVE") groupUpdate.command = GroupUpdateCommand::REMOVE_LOT; + else { + LOG("Invalid action %s", msg.action.c_str()); + return; + } + + groupUpdate.groupId = msg.groupID; + groupUpdate.inventory = msg.inventoryType; + groupUpdate.lot = msg.lot; + + UpdateGroup(groupUpdate); +} diff --git a/dGame/dComponents/InventoryComponent.h b/dGame/dComponents/InventoryComponent.h index da11607a9..f43d706c0 100644 --- a/dGame/dComponents/InventoryComponent.h +++ b/dGame/dComponents/InventoryComponent.h @@ -33,6 +33,16 @@ enum class eItemType : int32_t; namespace GameMessages { struct GetObjectReportInfo; + struct EquipInventory; + struct UnEquipInventory; + struct RemoveItemFromInventory; + struct MoveItemInInventory; + struct MoveItemBetweenInventoryTypes; + struct RequestMoveItemBetweenInventoryTypes; + struct ClientItemConsumed; + struct UseNonEquipmentItem; + struct UpdateInventoryGroup; + struct UpdateInventoryGroupContents; } /** @@ -446,6 +456,19 @@ public: bool OnGetObjectReportInfo(GameMessages::GetObjectReportInfo& reportInfo); + // Game messages received from the client (see InventoryMessages.h). + void OnEquipInventory(const GameMessages::EquipInventory& msg); + void OnUnEquipInventory(const GameMessages::UnEquipInventory& msg); + void OnRemoveItemFromInventory(const GameMessages::RemoveItemFromInventory& msg); + void OnMoveItemInInventory(const GameMessages::MoveItemInInventory& msg); + void OnMoveItemBetweenInventoryTypes(const GameMessages::MoveItemBetweenInventoryTypes& msg); + // Answers the client at sysAddr with ResponseMoveItemBetweenInventoryTypes. + void OnRequestMoveItemBetweenInventoryTypes(const GameMessages::RequestMoveItemBetweenInventoryTypes& msg, const SystemAddress& sysAddr); + void OnClientItemConsumed(const GameMessages::ClientItemConsumed& msg); + void OnUseNonEquipmentItem(const GameMessages::UseNonEquipmentItem& msg); + void OnUpdateInventoryGroup(const GameMessages::UpdateInventoryGroup& msg); + void OnUpdateInventoryGroupContents(const GameMessages::UpdateInventoryGroupContents& msg); + ~InventoryComponent() override; private: diff --git a/dGame/dComponents/PetComponent.cpp b/dGame/dComponents/PetComponent.cpp index 2181361c2..a504423ce 100644 --- a/dGame/dComponents/PetComponent.cpp +++ b/dGame/dComponents/PetComponent.cpp @@ -2,6 +2,7 @@ #include "DashboardNotify.h" #include "GameMessages.h" #include "PetMessages.h" +#include "InventoryMessages.h" #include "EffectsMessages.h" #include "BrickDatabase.h" #include "CDClientDatabase.h" @@ -960,7 +961,12 @@ void PetComponent::Activate(Item* item, bool registerPet, bool fromTaming) { } } - GameMessages::SendMarkInventoryItemAsActive(m_Owner, true, eUnequippableActiveType::PET, m_ItemId, GetOwner()->GetSystemAddress()); + GameMessages::MarkInventoryItemAsActive markActive; + markActive.target = m_Owner; + markActive.bActive = true; + markActive.iType = eUnequippableActiveType::PET; + markActive.itemID = m_ItemId; + markActive.Send(GetOwner()->GetSystemAddress()); activePets[m_Owner] = m_Parent->GetObjectID(); @@ -1029,7 +1035,10 @@ void PetComponent::AddDrainImaginationTimer(bool fromTaming) { auto playerEntity = playerDestroyableComponent->GetParent(); if (!playerEntity) return; - GameMessages::SendUseItemRequirementsResponse(playerEntity->GetObjectID(), playerEntity->GetSystemAddress(), eUseItemResponse::NoImaginationForPet); + GameMessages::UseItemRequirementsResponse requirementsResponse; + requirementsResponse.target = playerEntity->GetObjectID(); + requirementsResponse.eUseResponse = eUseItemResponse::NoImaginationForPet; + requirementsResponse.SendToClient(playerEntity->GetSystemAddress()); } this->AddDrainImaginationTimer(); @@ -1047,7 +1056,12 @@ void PetComponent::Deactivate() { if (owner == nullptr) return; - GameMessages::SendMarkInventoryItemAsActive(m_Owner, false, eUnequippableActiveType::PET, m_ItemId, owner->GetSystemAddress()); + GameMessages::MarkInventoryItemAsActive markActive; + markActive.target = m_Owner; + markActive.bActive = false; + markActive.iType = eUnequippableActiveType::PET; + markActive.itemID = m_ItemId; + markActive.Send(owner->GetSystemAddress()); { GameMessages::AddPetToPlayer msg; diff --git a/dGame/dComponents/PossessorComponent.cpp b/dGame/dComponents/PossessorComponent.cpp index d95bb79b3..f4d5fa428 100644 --- a/dGame/dComponents/PossessorComponent.cpp +++ b/dGame/dComponents/PossessorComponent.cpp @@ -6,6 +6,7 @@ #include "GameMessages.h" #include "RacingMessages.h" #include "eUnequippableActiveType.h" +#include "InventoryMessages.h" PossessorComponent::PossessorComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_Possessable = LWOOBJID_EMPTY; @@ -18,7 +19,12 @@ PossessorComponent::~PossessorComponent() { auto* possessable = mount->GetComponent(); if (possessable) { if (possessable->GetIsItemSpawned()) { - GameMessages::SendMarkInventoryItemAsActive(m_Parent->GetObjectID(), false, eUnequippableActiveType::MOUNT, GetMountItemID(), m_Parent->GetSystemAddress()); + GameMessages::MarkInventoryItemAsActive markActive; + markActive.target = m_Parent->GetObjectID(); + markActive.bActive = false; + markActive.iType = eUnequippableActiveType::MOUNT; + markActive.itemID = GetMountItemID(); + markActive.Send(m_Parent->GetSystemAddress()); } possessable->Dismount(); } diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index 0d728669b..9988cfb47 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -5,6 +5,7 @@ set(DGAME_DGAMEMESSAGES_SOURCES "GameMessageHandler.cpp" "EffectsMessages.cpp" "GameMessages.cpp" + "InventoryMessages.cpp" "MissionMessages.cpp" "PetMessages.cpp" "PropertyMessages.cpp" diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 3fe8a82d3..5fa6acc67 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -36,6 +36,7 @@ #include "RacingMessages.h" #include "MissionMessages.h" #include "EffectsMessages.h" +#include "InventoryMessages.h" #include "PetMessages.h" #include "PropertyMessages.h" #include "eMissionTaskType.h" @@ -83,6 +84,21 @@ namespace { { CHOICE_BOX_RESPOND, []() { return std::make_unique(); } }, { CINEMATIC_UPDATE, []() { return std::make_unique(); } }, + // Inventory and items + { EQUIP_INVENTORY, []() { return std::make_unique(); } }, + { UN_EQUIP_INVENTORY, []() { return std::make_unique(); } }, + { REMOVE_ITEM_FROM_INVENTORY, []() { return std::make_unique(); } }, + { MOVE_ITEM_IN_INVENTORY, []() { return std::make_unique(); } }, + { MOVE_ITEM_BETWEEN_INVENTORY_TYPES, []() { return std::make_unique(); } }, + { REQUEST_MOVE_ITEM_BETWEEN_INVENTORY_TYPES, []() { return std::make_unique(); } }, + { PUSH_EQUIPPED_ITEMS_STATE, []() { return std::make_unique(); } }, + { POP_EQUIPPED_ITEMS_STATE, []() { return std::make_unique(); } }, + { CLIENT_ITEM_CONSUMED, []() { return std::make_unique(); } }, + { USE_NON_EQUIPMENT_ITEM, []() { return std::make_unique(); } }, + { SET_CONSUMABLE_ITEM, []() { return std::make_unique(); } }, + { UPDATE_INVENTORY_GROUP, []() { return std::make_unique(); } }, + { UPDATE_INVENTORY_GROUP_CONTENTS, []() { return std::make_unique(); } }, + // Pets { PET_TAMING_TRY_BUILD, []() { return std::make_unique(); } }, { NOTIFY_TAMING_BUILD_SUCCESS, []() { return std::make_unique(); } }, @@ -173,24 +189,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System switch (messageID) { - case MessageType::Game::MOVE_ITEM_IN_INVENTORY: { - GameMessages::HandleMoveItemInInventory(inStream, entity); - break; - } - - case MessageType::Game::REMOVE_ITEM_FROM_INVENTORY: { - GameMessages::HandleRemoveItemFromInventory(inStream, entity, sysAddr); - break; - } - - case MessageType::Game::EQUIP_INVENTORY: - GameMessages::HandleEquipItem(inStream, entity); - break; - - case MessageType::Game::UN_EQUIP_INVENTORY: - GameMessages::HandleUnequipItem(inStream, entity); - break; - // Currently not actually used for our implementation, however its used right now to get around invisible inventory items in the client. case MessageType::Game::SELECT_SKILL: { auto var = entity->GetVar(u"dlu_first_time_load"); @@ -227,7 +225,11 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System // Fixes a bug where testmapping too fast causes large item inventories to become invisible. // Only affects item inventory - GameMessages::SendSetInventorySize(entity, eInventoryType::ITEMS, inv->GetInventory(eInventoryType::ITEMS)->GetSize()); + GameMessages::SetInventorySize setSize; + setSize.target = entity->GetObjectID(); + setSize.inventoryType = eInventoryType::ITEMS; + setSize.size = inv->GetInventory(eInventoryType::ITEMS)->GetSize(); + setSize.SendToClient(entity->GetSystemAddress()); } GameMessages::SendRestoreToPostLoadStats(entity, sysAddr); @@ -462,18 +464,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System entity->Smash(entity->GetObjectID()); break; - case MessageType::Game::MOVE_ITEM_BETWEEN_INVENTORY_TYPES: - GameMessages::HandleMoveItemBetweenInventoryTypes(inStream, entity, sysAddr); - break; - - case MessageType::Game::PUSH_EQUIPPED_ITEMS_STATE: - GameMessages::HandlePushEquippedItemsState(inStream, entity); - break; - - case MessageType::Game::POP_EQUIPPED_ITEMS_STATE: - GameMessages::HandlePopEquippedItemsState(inStream, entity); - break; - case MessageType::Game::BUY_FROM_VENDOR: GameMessages::HandleBuyFromVendor(inStream, entity, sysAddr); break; @@ -494,18 +484,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleMatchRequest(inStream, entity); break; - case MessageType::Game::USE_NON_EQUIPMENT_ITEM: - GameMessages::HandleUseNonEquipmentItem(inStream, entity); - break; - - case MessageType::Game::CLIENT_ITEM_CONSUMED: - GameMessages::HandleClientItemConsumed(inStream, entity); - break; - - case MessageType::Game::SET_CONSUMABLE_ITEM: - GameMessages::HandleSetConsumableItem(inStream, entity, sysAddr); - break; - case MessageType::Game::VERIFY_ACK: GameMessages::HandleVerifyAck(inStream, entity, sysAddr); break; @@ -538,10 +516,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; // NT - case MessageType::Game::REQUEST_MOVE_ITEM_BETWEEN_INVENTORY_TYPES: - GameMessages::HandleRequestMoveItemBetweenInventoryTypes(inStream, entity, sysAddr); - break; - case MessageType::Game::TOGGLE_GHOST_REFERENCE_OVERRIDE: GameMessages::HandleToggleGhostReferenceOverride(inStream, entity, sysAddr); break; @@ -606,13 +580,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System case MessageType::Game::REQUEST_VENDOR_STATUS_UPDATE: GameMessages::SendVendorStatusUpdate(entity, sysAddr, true); break; - case MessageType::Game::UPDATE_INVENTORY_GROUP: - GameMessages::HandleUpdateInventoryGroup(inStream, entity, sysAddr); - break; - case MessageType::Game::UPDATE_INVENTORY_GROUP_CONTENTS: - GameMessages::HandleUpdateInventoryGroupContents(inStream, entity, sysAddr); - break; - default: LOG_DEBUG("Received Unknown GM with ID: %4i, %s", messageID, StringifiedEnum::ToString(messageID).data()); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 8b930a1a1..bbf294ad3 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -1,5 +1,6 @@ #include "GameMessages.h" #include "EffectsMessages.h" +#include "InventoryMessages.h" #include "DashboardNotify.h" #include "PlayerReports.h" #include "EconomyLedger.h" @@ -348,65 +349,6 @@ void GameMessages::SendGMLevelBroadcast(const LWOOBJID& objectID, eGameMasterLev SEND_PACKET_BROADCAST; } -void GameMessages::SendAddItemToInventoryClientSync(Entity* entity, const SystemAddress& sysAddr, Item* item, const LWOOBJID& objectID, bool showFlyingLoot, int itemCount, LWOOBJID subKey, eLootSourceType lootSourceType) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC); - bitStream.Write(item->GetBound()); - bitStream.Write(item->GetInfo().isBOE); - bitStream.Write(item->GetInfo().isBOP); - - bitStream.Write(lootSourceType != eLootSourceType::NONE); // Loot source - if (lootSourceType != eLootSourceType::NONE) bitStream.Write(lootSourceType); - std::u16string extraInfo; - - const auto& config = item->GetConfig(); - - for (const auto& data : config.values | std::views::values) { - extraInfo += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; - } - - if (extraInfo.length() > 0) extraInfo.pop_back(); // remove the last comma - - bitStream.Write(extraInfo.size()); - if (extraInfo.size() > 0) { - for (uint32_t i = 0; i < extraInfo.size(); ++i) { - bitStream.Write(extraInfo[i]); - } - bitStream.Write(0x00); - } - - bitStream.Write(item->GetLot()); - - bitStream.Write(subKey != LWOOBJID_EMPTY); - if (subKey != LWOOBJID_EMPTY) bitStream.Write(subKey); - - auto* inventory = item->GetInventory(); - const auto inventoryType = inventory->GetType(); - - bitStream.Write(inventoryType != eInventoryType::ITEMS); - if (inventoryType != eInventoryType::ITEMS) bitStream.Write(inventoryType); - - bitStream.Write(itemCount != 1); - if (itemCount != 1) bitStream.Write(itemCount); - - const auto count = item->GetCount(); - - bitStream.Write(count != 0); //items total - if (count != 0) bitStream.Write(count); - - bitStream.Write(objectID); - bitStream.Write(0.0f); - bitStream.Write(0.0f); - bitStream.Write(0.0f); - bitStream.Write(showFlyingLoot); - bitStream.Write(item->GetSlot()); - - SEND_PACKET; -} - void GameMessages::SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -574,19 +516,6 @@ void GameMessages::SendDie(Entity* entity, const LWOOBJID& killerID, const LWOOB SEND_PACKET_BROADCAST; } -void GameMessages::SendSetInventorySize(Entity* entity, int invType, int size) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::SET_INVENTORY_SIZE); - bitStream.Write(invType); - bitStream.Write(size); - - SystemAddress sysAddr = entity->GetSystemAddress(); - SEND_PACKET; -} - void GameMessages::SendSetJetPackMode(Entity* entity, bool use, bool bypassChecks, bool doHover, int effectID, float airspeed, float maxAirspeed, float verticalVelocity, int warningEffectID) { /* historical jamesster jetpack values if (bIsJamessterPhysics) { @@ -828,141 +757,6 @@ void GameMessages::SendVendorTransactionResult(Entity* entity, const SystemAddre SEND_PACKET; } -void GameMessages::SendRemoveItemFromInventory(Entity* entity, const SystemAddress& sysAddr, LWOOBJID objectID, LOT templateID, int inventoryType, uint32_t stackCount, uint32_t stackRemaining) { - CBITSTREAM; - CMSGHEADER; - // this is used for a lot more than just inventory trashing (trades, vendors, etc.) but for now since it's just used for that, that's all im going to implement - bool bConfirmed = true; - bool bDeleteItem = true; - bool bOutSuccess = false; - int eInvType = inventoryType; - int eLootTypeSource = LOOTTYPE_NONE; - bool forceDeletion = true; - LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; - LWOOBJID iObjID = objectID; - LOT iObjTemplate = templateID; - LWOOBJID iRequestingObjID = LWOOBJID_EMPTY; - uint32_t iStackCount = stackCount; - uint32_t iStackRemaining = stackRemaining; - LWOOBJID iSubkey = LWOOBJID_EMPTY; - LWOOBJID iTradeID = LWOOBJID_EMPTY; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::REMOVE_ITEM_FROM_INVENTORY); - bitStream.Write(bConfirmed); - bitStream.Write(bDeleteItem); - bitStream.Write(bOutSuccess); - bitStream.Write1(); - bitStream.Write(eInvType); - bitStream.Write1(); - bitStream.Write(eLootTypeSource); - bitStream.Write(0); //extra info - //bitStream.Write(0); //extra info - bitStream.Write(forceDeletion); - bitStream.Write0(); - bitStream.Write1(); - bitStream.Write(iObjID); - bitStream.Write1(); - bitStream.Write(iObjTemplate); - bitStream.Write0(); - bitStream.Write1(); - bitStream.Write(iStackCount); - bitStream.Write1(); - bitStream.Write(iStackRemaining); - bitStream.Write0(); - bitStream.Write0(); - - SEND_PACKET; -} - -void GameMessages::SendConsumeClientItem(Entity* entity, bool bSuccess, LWOOBJID item) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::CONSUME_CLIENT_ITEM); - bitStream.Write(bSuccess); - bitStream.Write(item); - - SystemAddress sysAddr = entity->GetSystemAddress(); - SEND_PACKET; -} - -void GameMessages::SendUseItemResult(Entity* entity, LOT templateID, bool useItemResult) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::USE_ITEM_RESULT); - bitStream.Write(templateID); - bitStream.Write(useItemResult); - - SystemAddress sysAddr = entity->GetSystemAddress(); - SEND_PACKET; -} - -void GameMessages::SendUseItemRequirementsResponse(LWOOBJID objectID, const SystemAddress& sysAddr, eUseItemResponse itemResponse) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectID); - bitStream.Write(MessageType::Game::USE_ITEM_REQUIREMENTS_RESPONSE); - - bitStream.Write(itemResponse); - - SEND_PACKET; -} - -void GameMessages::SendMoveInventoryBatch(Entity* entity, uint32_t stackCount, int srcInv, int dstInv, const LWOOBJID& iObjID) { - CBITSTREAM; - CMSGHEADER; - - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - Item* itemStack = inv->FindItemById(iObjID); - if (!itemStack) return; - - bool bAllowPartial = false; - bool bOutSuccess = false; - uint32_t count = stackCount; - int dstBag = dstInv; - LOT moveLOT = itemStack->GetLot(); - //LWOOBJID moveSubkey = LWOOBJID_EMPTY; - bool showFlyingLoot = false; - int srcBag = srcInv; - LWOOBJID startObjectID = iObjID; - - bitStream.Write(bAllowPartial); - bitStream.Write(bOutSuccess); - if (count == 1) { - bitStream.Write0(); - } else { - bitStream.Write1(); - bitStream.Write(count); - } - if (dstBag == 0) { - bitStream.Write0(); - } else { - bitStream.Write1(); - bitStream.Write(dstBag); - } - bitStream.Write0(); - bitStream.Write(showFlyingLoot); - if (srcBag == 0) { - bitStream.Write0(); - } else { - bitStream.Write1(); - bitStream.Write(srcBag); - } - bitStream.Write1(); - bitStream.Write(startObjectID); - - auto sysAddr = entity->GetSystemAddress(); - - SEND_PACKET; -} - void GameMessages::SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response) { CBITSTREAM; CMSGHEADER; @@ -1398,18 +1192,6 @@ void GameMessages::SendUnSmash(Entity* entity, LWOOBJID builderID, float duratio SEND_PACKET_BROADCAST; } -void GameMessages::HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LOT lot; - - inStream.Read(lot); - - auto* inventory = entity->GetComponent(); - - if (inventory == nullptr) return; - - inventory->SetConsumable(lot); -} - void GameMessages::SendSetStunned(LWOOBJID objectId, eStateChangeType stateChangeType, const SystemAddress& sysAddr, LWOOBJID originator, bool bCantAttack, bool bCantEquip, bool bCantInteract, bool bCantJump, bool bCantMove, bool bCantTurn, @@ -1883,25 +1665,6 @@ void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* //Pets: -void GameMessages::SendMarkInventoryItemAsActive(LWOOBJID objectId, bool bActive, eUnequippableActiveType iType, LWOOBJID itemID, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::MARK_INVENTORY_ITEM_AS_ACTIVE); - - bitStream.Write(bActive); - - bitStream.Write(iType != eUnequippableActiveType::INVALID); - if (iType != eUnequippableActiveType::INVALID) bitStream.Write(iType); - - bitStream.Write(itemID != LWOOBJID_EMPTY); - if (itemID != LWOOBJID_EMPTY) bitStream.Write(itemID); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - void GameMessages::SendRemoveBuff(Entity* entity, bool fromUnEquip, bool removeImmunity, uint32_t buffId) { CBITSTREAM; CMSGHEADER; @@ -1935,21 +1698,6 @@ void GameMessages::SendDisplayZoneSummary(LWOOBJID objectId, const SystemAddress //UI -void GameMessages::SendNotifyNotEnoughInvSpace(LWOOBJID objectId, uint32_t freeSlotsNeeded, eInventoryType inventoryType, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::VEHICLE_NOTIFY_FINISHED_RACE); - - bitStream.Write(freeSlotsNeeded); - bitStream.Write(inventoryType != 0); - if (inventoryType != 0) bitStream.Write(inventoryType); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - // Mounts void GameMessages::SendSetMountInventoryID(Entity* entity, const LWOOBJID& objectID, const SystemAddress& sysAddr) { @@ -1991,7 +1739,14 @@ void GameMessages::HandleDismountComplete(RakNet::BitStream& inStream, Entity* e auto* character = entity->GetComponent(); if (character) { // If we had an active item turn it off - if (possessorComponent->GetMountItemID() != LWOOBJID_EMPTY) GameMessages::SendMarkInventoryItemAsActive(entity->GetObjectID(), false, eUnequippableActiveType::MOUNT, possessorComponent->GetMountItemID(), entity->GetSystemAddress()); + if (possessorComponent->GetMountItemID() != LWOOBJID_EMPTY) { + GameMessages::MarkInventoryItemAsActive markActive; + markActive.target = entity->GetObjectID(); + markActive.bActive = false; + markActive.iType = eUnequippableActiveType::MOUNT; + markActive.itemID = possessorComponent->GetMountItemID(); + markActive.Send(entity->GetSystemAddress()); + } possessorComponent->SetMountItemID(LWOOBJID_EMPTY); } @@ -2175,89 +1930,6 @@ void GameMessages::SendAddBuff(LWOOBJID& objectID, const LWOOBJID& casterID, uin // NT -void GameMessages::HandleRequestMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool bAllowPartial{}; - int32_t destSlot = -1; - int32_t iStackCount = 1; - eInventoryType invTypeDst = ITEMS; - eInventoryType invTypeSrc = ITEMS; - LWOOBJID itemID = LWOOBJID_EMPTY; - bool showFlyingLoot{}; - LWOOBJID subkey = LWOOBJID_EMPTY; - LOT itemLOT = 0; - - bAllowPartial = inStream.ReadBit(); - if (inStream.ReadBit()) inStream.Read(destSlot); - if (inStream.ReadBit()) inStream.Read(iStackCount); - if (inStream.ReadBit()) inStream.Read(invTypeDst); - if (inStream.ReadBit()) inStream.Read(invTypeSrc); - if (inStream.ReadBit()) inStream.Read(itemID); - showFlyingLoot = inStream.ReadBit(); - if (inStream.ReadBit()) inStream.Read(subkey); - if (inStream.ReadBit()) inStream.Read(itemLOT); - - if (invTypeDst == invTypeSrc) { - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC); - return; - } - - auto* inventoryComponent = entity->GetComponent(); - - if (inventoryComponent) { - if (itemID != LWOOBJID_EMPTY) { - auto* item = inventoryComponent->FindItemById(itemID); - - if (!item) { - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::FAIL_ITEM_NOT_FOUND); - return; - } - - if (item->GetLot() == 6086) { // Thinking hat - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::FAIL_CANT_MOVE_THINKING_HAT); - return; - } - - auto* destInv = inventoryComponent->GetInventory(invTypeDst); - if (destInv && destInv->GetEmptySlots() == 0) { - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::FAIL_INV_FULL); - return; - } - - // Despawn the pet if we are moving that pet to the vault. - auto* petComponent = PetComponent::GetActivePet(entity->GetObjectID()); - if (petComponent && petComponent->GetDatabaseId() == item->GetSubKey()) { - inventoryComponent->DespawnPet(); - } - - inventoryComponent->MoveItemToInventory(item, invTypeDst, iStackCount, showFlyingLoot, false, false, destSlot); - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::SUCCESS); - } - } else { - SendResponseMoveItemBetweenInventoryTypes(entity->GetObjectID(), sysAddr, invTypeDst, invTypeSrc, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC); - } -} - -void GameMessages::SendResponseMoveItemBetweenInventoryTypes(LWOOBJID objectId, const SystemAddress& sysAddr, eInventoryType inventoryTypeDestination, eInventoryType inventoryTypeSource, eReponseMoveItemBetweenInventoryTypeCode response) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::RESPONSE_MOVE_ITEM_BETWEEN_INVENTORY_TYPES); - - bitStream.Write(inventoryTypeDestination != eInventoryType::ITEMS); - if (inventoryTypeDestination != eInventoryType::ITEMS) bitStream.Write(inventoryTypeDestination); - - bitStream.Write(inventoryTypeSource != eInventoryType::ITEMS); - if (inventoryTypeSource != eInventoryType::ITEMS) bitStream.Write(inventoryTypeSource); - - bitStream.Write(response != eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC); - if (response != eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC) bitStream.Write(response); - - SEND_PACKET; -} - - - //----------------------------------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------- Handlers ------------------------------------------------------------------ //----------------------------------------------------------------------------------------------------------------------------------------------- @@ -2667,138 +2339,6 @@ void GameMessages::HandleRequestDie(RakNet::BitStream& inStream, Entity* entity) inStream.Read(killerID); } -void GameMessages::HandleEquipItem(RakNet::BitStream& inStream, Entity* entity) { - bool immediate; - LWOOBJID objectID; - inStream.Read(immediate); - inStream.Read(immediate); //twice? - inStream.Read(objectID); - - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - Item* item = inv->FindItemById(objectID); - if (!item) return; - - item->Equip(); - - Game::entityManager->SerializeEntity(entity); -} - -void GameMessages::HandleUnequipItem(RakNet::BitStream& inStream, Entity* entity) { - bool immediate; - LWOOBJID objectID; - inStream.Read(immediate); - inStream.Read(immediate); - inStream.Read(immediate); - inStream.Read(objectID); - - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - auto* item = inv->FindItemById(objectID); - - if (!item) return; - - item->UnEquip(); - - Game::entityManager->SerializeEntity(entity); -} - -void GameMessages::HandleRemoveItemFromInventory(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - // this is used for a lot more than just inventory trashing (trades, vendors, etc.) but for now since it's just used for that, that's all im going to implement - bool bConfirmed = false; - bool bDeleteItem = true; - bool bOutSuccess = false; - bool eInvTypeIsDefault = false; - int eInvType = INVENTORY_MAX; - bool eLootTypeSourceIsDefault = false; - int eLootTypeSource = LOOTTYPE_NONE; - int32_t extraInfoLength = 0; - std::u16string extraInfo; - bool forceDeletion = true; - bool iLootTypeSourceIsDefault = false; - LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; - bool iObjIDIsDefault = false; - LWOOBJID iObjID = LWOOBJID_EMPTY; - bool iObjTemplateIsDefault = false; - LOT iObjTemplate = LOT_NULL; - bool iRequestingObjIDIsDefault = false; - LWOOBJID iRequestingObjID = LWOOBJID_EMPTY; - bool iStackCountIsDefault = false; - uint32_t iStackCount = 1; - bool iStackRemainingIsDefault = false; - uint32_t iStackRemaining = 0; - bool iSubkeyIsDefault = false; - LWOOBJID iSubkey = LWOOBJID_EMPTY; - bool iTradeIDIsDefault = false; - LWOOBJID iTradeID = LWOOBJID_EMPTY; - - inStream.Read(bConfirmed); - inStream.Read(bDeleteItem); - inStream.Read(bOutSuccess); - inStream.Read(eInvTypeIsDefault); - if (eInvTypeIsDefault) inStream.Read(eInvType); - inStream.Read(eLootTypeSourceIsDefault); - if (eLootTypeSourceIsDefault) inStream.Read(eLootTypeSource); - inStream.Read(extraInfoLength); - if (extraInfoLength > 0) { - for (uint32_t i = 0; i < extraInfoLength; ++i) { - uint16_t character; - inStream.Read(character); - extraInfo.push_back(character); - } - uint16_t nullTerm; - inStream.Read(nullTerm); - } - inStream.Read(forceDeletion); - inStream.Read(iLootTypeSourceIsDefault); - if (iLootTypeSourceIsDefault) inStream.Read(iLootTypeSource); - inStream.Read(iObjIDIsDefault); - if (iObjIDIsDefault) inStream.Read(iObjID); - inStream.Read(iObjTemplateIsDefault); - if (iObjTemplateIsDefault) inStream.Read(iObjTemplate); - inStream.Read(iRequestingObjIDIsDefault); - if (iRequestingObjIDIsDefault) inStream.Read(iRequestingObjID); - inStream.Read(iStackCountIsDefault); - if (iStackCountIsDefault) inStream.Read(iStackCount); - inStream.Read(iStackRemainingIsDefault); - if (iStackRemainingIsDefault) inStream.Read(iStackRemaining); - inStream.Read(iSubkeyIsDefault); - if (iSubkeyIsDefault) inStream.Read(iSubkey); - inStream.Read(iTradeIDIsDefault); - if (iTradeIDIsDefault) inStream.Read(iTradeID); - - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - auto* item = inv->FindItemById(iObjID); - - if (item == nullptr) { - return; - } - - iStackCount = std::min(item->GetCount(), iStackCount); - - if (bConfirmed) { - const auto itemType = static_cast(item->GetInfo().itemType); - if (itemType == eItemType::MODEL || itemType == eItemType::LOOT_MODEL) { - item->DisassembleModel(iStackCount); - } else if (itemType == eItemType::VEHICLE) { - Database::Get()->DeleteUgcBuild(item->GetSubKey()); - } - auto lot = item->GetLot(); - item->SetCount(item->GetCount() - iStackCount, true); - Game::entityManager->SerializeEntity(entity); - - auto* missionComponent = entity->GetComponent(); - - if (missionComponent != nullptr) { - missionComponent->Progress(eMissionTaskType::GATHER, lot, LWOOBJID_EMPTY, "", -iStackCount); - } - } -} - void GameMessages::SendSetGravityScale(const LWOOBJID& target, const float effectScale, const SystemAddress& sysAddr) { CBITSTREAM; CMSGHEADER; @@ -2811,79 +2351,6 @@ void GameMessages::SendSetGravityScale(const LWOOBJID& target, const float effec SEND_PACKET; } -void GameMessages::HandleMoveItemInInventory(RakNet::BitStream& inStream, Entity* entity) { - bool destInvTypeIsDefault = false; - int32_t destInvType = eInventoryType::INVALID; - LWOOBJID iObjID; - int inventoryType; - int responseCode; - int slot; - inStream.Read(destInvTypeIsDefault); - if (destInvTypeIsDefault) { inStream.Read(destInvType); } - inStream.Read(iObjID); - inStream.Read(inventoryType); - inStream.Read(responseCode); - inStream.Read(slot); - - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - auto* item = inv->FindItemById(iObjID); - - if (!item) { - return; - } - - inv->MoveStack(item, static_cast(destInvType), slot); - Game::entityManager->SerializeEntity(entity); -} - -void GameMessages::HandleMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - eInventoryType inventoryTypeA; - eInventoryType inventoryTypeB; - LWOOBJID objectID; - bool showFlyingLoot = true; - bool stackCountIsDefault = false; - uint32_t stackCount = 1; - bool templateIDIsDefault = false; - LOT templateID = LOT_NULL; - - inStream.Read(inventoryTypeA); - inStream.Read(inventoryTypeB); - inStream.Read(objectID); - inStream.Read(showFlyingLoot); - inStream.Read(stackCountIsDefault); - if (stackCountIsDefault) inStream.Read(stackCount); - inStream.Read(templateIDIsDefault); - if (templateIDIsDefault) inStream.Read(templateID); - - auto inv = entity->GetComponent(); - if (!inv) return; - - auto* item = inv->FindItemById(objectID); - - if (!item) { - // Attempt to find the item by lot in inventory A since A is the source inventory. - item = inv->FindItemByLot(templateID, static_cast(inventoryTypeA)); - if (!item) { - // As a final resort, try to find the item in its default inventory based on type. - item = inv->FindItemByLot(templateID); - if (!item) { - return; - } - } - } - - if (entity->GetCharacter()) { - if (entity->GetCharacter()->GetBuildMode()) { - showFlyingLoot = false; - } - } - - inv->MoveItemToInventory(item, inventoryTypeB, stackCount, showFlyingLoot); - Game::entityManager->SerializeEntity(entity); -} - void GameMessages::HandleResurrect(RakNet::BitStream& inStream, Entity* entity) { bool immediate = inStream.ReadBit(); @@ -2900,60 +2367,6 @@ void GameMessages::HandleResurrect(RakNet::BitStream& inStream, Entity* entity) } } -void GameMessages::HandlePushEquippedItemsState(RakNet::BitStream& inStream, Entity* entity) { - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - inv->PushEquippedItems(); -} - -void GameMessages::HandlePopEquippedItemsState(RakNet::BitStream& inStream, Entity* entity) { - InventoryComponent* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - inv->PopEquippedItems(); - Game::entityManager->SerializeEntity(entity); // so it updates on client side -} - - -void GameMessages::HandleClientItemConsumed(RakNet::BitStream& inStream, Entity* entity) { - LWOOBJID itemConsumed; - - inStream.Read(itemConsumed); - - auto* inventory = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - - if (inventory == nullptr) { - return; - } - - auto* item = inventory->FindItemById(itemConsumed); - if (item == nullptr) { - return; - } - LOT itemLot = item->GetLot(); - - item->Consume(); - - auto* missions = static_cast(entity->GetComponent(eReplicaComponentType::MISSION)); - if (missions != nullptr) { - missions->Progress(eMissionTaskType::USE_ITEM, itemLot); - } -} - - -void GameMessages::HandleUseNonEquipmentItem(RakNet::BitStream& inStream, Entity* entity) { - LWOOBJID itemConsumed; - - inStream.Read(itemConsumed); - - auto* inv = static_cast(entity->GetComponent(eReplicaComponentType::INVENTORY)); - - if (!inv) return; - - auto* item = inv->FindItemById(itemConsumed); - - if (item) item->UseNonEquip(item); -} - void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entity) { LWOOBJID activator; uint32_t playerChoicesLen; @@ -3289,72 +2702,6 @@ void GameMessages::HandleCancelDonationOnPlayer(RakNet::BitStream& inStream, Ent characterComponent->SetCurrentInteracting(LWOOBJID_EMPTY); } -void GameMessages::HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - std::string action; - std::u16string groupName; - InventoryComponent::GroupUpdate groupUpdate; - bool locked{}; // All groups are locked by default - - uint32_t size{}; - if (!inStream.Read(size)) return; - if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize - action.resize(size); - if (!inStream.Read(action.data(), size)) return; - - if (!inStream.Read(size)) return; - if (size > MAX_MESSAGE_LENGTH) return; // Bounds check before resize - groupUpdate.groupId.resize(size); - if (!inStream.Read(groupUpdate.groupId.data(), size)) return; - - if (!inStream.Read(size)) return; - if (size > MAX_MESSAGE_LENGTH / 2) return; // Bounds check: size * 2 would overflow or exceed limit - groupName.resize(size); - if (!inStream.Read(reinterpret_cast(groupName.data()), size * 2)) return; - - if (!inStream.Read(groupUpdate.inventory)) return; - if (!inStream.Read(locked)) return; - - groupUpdate.groupName = GeneralUtils::UTF16ToWTF8(groupName); - - if (action == "ADD") groupUpdate.command = InventoryComponent::GroupUpdateCommand::ADD; - else if (action == "MODIFY") groupUpdate.command = InventoryComponent::GroupUpdateCommand::MODIFY; - else if (action == "REMOVE") groupUpdate.command = InventoryComponent::GroupUpdateCommand::REMOVE; - else { - LOG("Invalid action %s", action.c_str()); - return; - } - - auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent) inventoryComponent->UpdateGroup(groupUpdate); -} - -void GameMessages::HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - std::string action; - InventoryComponent::GroupUpdate groupUpdate; - - uint32_t size{}; - if (!inStream.Read(size)) return; - action.resize(size); - if (!inStream.Read(action.data(), size)) return; - - if (action == "ADD") groupUpdate.command = InventoryComponent::GroupUpdateCommand::ADD_LOT; - else if (action == "REMOVE") groupUpdate.command = InventoryComponent::GroupUpdateCommand::REMOVE_LOT; - else { - LOG("Invalid action %s", action.c_str()); - return; - } - - if (!inStream.Read(size)) return; - groupUpdate.groupId.resize(size); - if (!inStream.Read(groupUpdate.groupId.data(), size)) return; - - if (!inStream.Read(groupUpdate.inventory)) return; - if (!inStream.Read(groupUpdate.lot)) return; - - auto* inventoryComponent = entity->GetComponent(); - if (inventoryComponent) inventoryComponent->UpdateGroup(groupUpdate); -} - void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID) { CBITSTREAM; CMSGHEADER; @@ -3371,16 +2718,6 @@ void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling } -void GameMessages::SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::UPDATE_INVENTORY_UI); - - SEND_PACKET; -} - namespace GameMessages { bool GameMsg::Send() { return Game::entityManager->SendMessage(*this); diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 798909a29..dbc078b97 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -146,7 +146,6 @@ namespace GameMessages { void SendGMLevelBroadcast(const LWOOBJID& objectID, eGameMasterLevel level); void SendChatModeUpdate(const LWOOBJID& objectID, eGameMasterLevel level); - void SendAddItemToInventoryClientSync(Entity* entity, const SystemAddress& sysAddr, Item* item, const LWOOBJID& objectID, bool showFlyingLoot, int itemCount, LWOOBJID subKey = LWOOBJID_EMPTY, eLootSourceType lootSourceType = eLootSourceType::NONE); void SendChangeObjectWorldState(const LWOOBJID& objectID, eObjectWorldState state, const SystemAddress& sysAddr); void SendModifyLEGOScore(Entity* entity, const SystemAddress& sysAddr, int64_t score, eLootSourceType sourceType); @@ -161,8 +160,6 @@ namespace GameMessages { void SendDieNoImplCode(Entity* entity, const LWOOBJID& killerID, const LWOOBJID& lootOwnerID, eKillType killType, std::u16string deathType, float directionRelative_AngleY, float directionRelative_AngleXZ, float directionRelative_Force, bool bClientDeath, bool bSpawnLoot); void SendDie(Entity* entity, const LWOOBJID& killerID, const LWOOBJID& lootOwnerID, bool bDieAccepted, eKillType killType, std::u16string deathType, float directionRelative_AngleY, float directionRelative_AngleXZ, float directionRelative_Force, bool bClientDeath, bool bSpawnLoot, float coinSpawnTime); - void SendSetInventorySize(Entity* entity, int invType, int size); - void SendSetGravityScale(const LWOOBJID& target, const float effectScale, const SystemAddress& sysAddr); void SendSetJetPackMode(Entity* entity, bool use, bool bypassChecks = false, bool doHover = false, int effectID = -1, float airspeed = 10, float maxAirspeed = 15, float verticalVelocity = 1, int warningEffectID = -1); @@ -179,11 +176,6 @@ namespace GameMessages { void SendVendorStatusUpdate(Entity* entity, const SystemAddress& sysAddr, bool bUpdateOnly = false); void SendVendorTransactionResult(Entity* entity, const SystemAddress& sysAddr, eVendorTransactionResult result); - void SendRemoveItemFromInventory(Entity* entity, const SystemAddress& sysAddr, LWOOBJID iObjID, LOT templateID, int inventoryType, uint32_t stackCount, uint32_t stackRemaining); - void SendConsumeClientItem(Entity* entity, bool bSuccess, LWOOBJID item); - void SendUseItemResult(Entity* entity, LOT templateID, bool useItemResult); - void SendMoveInventoryBatch(Entity* entity, uint32_t stackCount, int srcInv, int dstInv, const LWOOBJID& iObjID); - void SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response); void SendMatchUpdate(Entity* entity, const SystemAddress& sysAddr, std::string data, eMatchUpdate type); @@ -240,8 +232,6 @@ namespace GameMessages { void SendLockNodeRotation(Entity* entity, std::string nodeName); - void HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void SendSetStunned(LWOOBJID objectId, eStateChangeType stateChangeType, const SystemAddress& sysAddr, LWOOBJID originator = LWOOBJID_EMPTY, bool bCantAttack = false, bool bCantEquip = false, bool bCantInteract = false, bool bCantJump = false, bool bCantMove = false, bool bCantTurn = false, @@ -314,13 +304,9 @@ namespace GameMessages { void HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); //Pets: - void SendMarkInventoryItemAsActive(LWOOBJID objectId, bool bActive, eUnequippableActiveType iType, LWOOBJID itemID, const SystemAddress& sysAddr); void SendDisplayZoneSummary(LWOOBJID objectId, const SystemAddress& sysAddr, bool isPropertyMap = false, bool isZoneStart = false, LWOOBJID sender = LWOOBJID_EMPTY); - //UI: - void SendNotifyNotEnoughInvSpace(LWOOBJID objectId, uint32_t freeSlotsNeeded, eInventoryType inventoryType, const SystemAddress& sysAddr); - // Mounts /** * @brief Set the Inventory LWOOBJID of the mount @@ -352,8 +338,6 @@ namespace GameMessages { //Racing: void HandleRequestDie(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void SendUseItemRequirementsResponse(LWOOBJID objectID, const SystemAddress& sysAddr, eUseItemResponse itemResponse); - // SG: void SendSetShootingGalleryParams(LWOOBJID objectId, const SystemAddress& sysAddr, @@ -391,9 +375,6 @@ namespace GameMessages { //NT: - void HandleRequestMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void SendResponseMoveItemBetweenInventoryTypes(LWOOBJID objectId, const SystemAddress& sysAddr, eInventoryType inventoryTypeDestination, eInventoryType inventoryTypeSource, eReponseMoveItemBetweenInventoryTypeCode response); - //Handlers: void HandleToggleGhostReferenceOverride(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); @@ -411,24 +392,11 @@ namespace GameMessages { void HandleNotifyServerLevelProcessingComplete(RakNet::BitStream& inStream, Entity* entity); void HandlePickupCurrency(RakNet::BitStream& inStream, Entity* entity); void HandleRequestDie(RakNet::BitStream& inStream, Entity* entity); - void HandleEquipItem(RakNet::BitStream& inStream, Entity* entity); - void HandleUnequipItem(RakNet::BitStream& inStream, Entity* entity); - void HandleRemoveItemFromInventory(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleMoveItemInInventory(RakNet::BitStream& inStream, Entity* entity); - void HandleMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandlePickupItem(RakNet::BitStream& inStream, Entity* entity); void HandleResurrect(RakNet::BitStream& inStream, Entity* entity); void HandleModifyPlayerZoneStatistic(RakNet::BitStream& inStream, Entity* entity); void HandleUpdatePlayerStatistic(RakNet::BitStream& inStream, Entity* entity); - void HandlePushEquippedItemsState(RakNet::BitStream& inStream, Entity* entity); - - void HandlePopEquippedItemsState(RakNet::BitStream& inStream, Entity* entity); - - void HandleClientItemConsumed(RakNet::BitStream& inStream, Entity* entity); - - void HandleUseNonEquipmentItem(RakNet::BitStream& inStream, Entity* entity); - void HandleMatchRequest(RakNet::BitStream& inStream, Entity* entity); void HandleReportBug(RakNet::BitStream& inStream, Entity* entity); @@ -452,13 +420,8 @@ namespace GameMessages { void HandleConfirmDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity); void HandleCancelDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity); - void HandleUpdateInventoryGroup(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleUpdateInventoryGroupContents(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID); - // This is a client gm however its default values are exactly what we need to get around the invisible inventory item issues. - void SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr); - struct DisplayTooltip : public NetGameMsg { DisplayTooltip() : NetGameMsg(MessageType::Game::DISPLAY_TOOLTIP) {} bool doOrDie{}; diff --git a/dGame/dGameMessages/InventoryMessages.cpp b/dGame/dGameMessages/InventoryMessages.cpp new file mode 100644 index 000000000..91e3636fc --- /dev/null +++ b/dGame/dGameMessages/InventoryMessages.cpp @@ -0,0 +1,458 @@ +#include "InventoryMessages.h" + +#include "BitStreamUtils.h" +#include "EntityManager.h" +#include "Game.h" +#include "Entity.h" +#include "GeneralUtils.h" +#include "Inventory.h" +#include "InventoryComponent.h" +#include "Item.h" + +#include + +namespace { + // Name value text (the client's LwoNameValue on the wire): u32 length, the characters, then a null character + // only if the text is not empty. + void WriteNameValue(RakNet::BitStream& bitStream, const std::u16string& text) { + BitStreamUtils::WriteLengthPrefixed(bitStream, text); + if (!text.empty()) bitStream.Write(0x00); + } + + bool ReadNameValue(RakNet::BitStream& bitStream, std::u16string& text) { + if (!BitStreamUtils::ReadLengthPrefixed(bitStream, text)) return false; + uint16_t nullTerminator{}; + return text.empty() || bitStream.Read(nullTerminator); + } + + // Writes the flag and value of an optional field whose flag DLU always sets. + template + void WriteAlways(RakNet::BitStream& bitStream, const T& value) { + bitStream.Write1(); + bitStream.Write(value); + } +} + +namespace GameMessages { + void AddItemToInventoryClientSync::SetItem(const Item& item) { + bBound = item.GetBound(); + bIsBOE = item.GetInfo().isBOE; + bIsBOP = item.GetInfo().isBOP; + + extraInfo.clear(); + for (const auto& data : item.GetConfig().values | std::views::values) { + extraInfo += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; + } + if (extraInfo.length() > 0) extraInfo.pop_back(); // remove the last comma + + iObjTemplate = item.GetLot(); + invType = item.GetInventory()->GetType(); + itemsTotal = item.GetCount(); + slotID = item.GetSlot(); + } + + void AddItemToInventoryClientSync::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bBound); + bitStream.Write(bIsBOE); + bitStream.Write(bIsBOP); + BitStreamUtils::WriteOptional(bitStream, eLootTypeSource, eLootSourceType::NONE); + WriteNameValue(bitStream, extraInfo); + bitStream.Write(iObjTemplate); + BitStreamUtils::WriteOptional(bitStream, iSubkey, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, invType, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, itemCount, 1); + BitStreamUtils::WriteOptional(bitStream, itemsTotal, 0); + bitStream.Write(newObjID); + bitStream.Write(ni3FlyingLootPosit.x); + bitStream.Write(ni3FlyingLootPosit.y); + bitStream.Write(ni3FlyingLootPosit.z); + bitStream.Write(showFlyingLoot); + bitStream.Write(slotID); + } + + bool AddItemToInventoryClientSync::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bBound)); + VALIDATE_READ(bitStream.Read(bIsBOE)); + VALIDATE_READ(bitStream.Read(bIsBOP)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, eLootTypeSource, eLootSourceType::NONE)); + VALIDATE_READ(ReadNameValue(bitStream, extraInfo)); + VALIDATE_READ(bitStream.Read(iObjTemplate)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iSubkey, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, invType, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemCount, 1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemsTotal, 0)); + VALIDATE_READ(bitStream.Read(newObjID)); + VALIDATE_READ(bitStream.Read(ni3FlyingLootPosit.x)); + VALIDATE_READ(bitStream.Read(ni3FlyingLootPosit.y)); + VALIDATE_READ(bitStream.Read(ni3FlyingLootPosit.z)); + VALIDATE_READ(bitStream.Read(showFlyingLoot)); + VALIDATE_READ(bitStream.Read(slotID)); + return true; + } + + void SetInventorySize::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(inventoryType); + bitStream.Write(size); + } + + bool SetInventorySize::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(inventoryType)); + VALIDATE_READ(bitStream.Read(size)); + return true; + } + + void RemoveItemFromInventory::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bConfirmed); + bitStream.Write(bDeleteItem); + bitStream.Write(bOutSuccess); + WriteAlways(bitStream, eInvType); + WriteAlways(bitStream, eLootTypeSource); + WriteNameValue(bitStream, extraInfo); + bitStream.Write(forceDeletion); + BitStreamUtils::WriteOptional(bitStream, iLootTypeSource, LWOOBJID_EMPTY); + WriteAlways(bitStream, iObjID); + WriteAlways(bitStream, iObjTemplate); + BitStreamUtils::WriteOptional(bitStream, iRequestingObjID, LWOOBJID_EMPTY); + WriteAlways(bitStream, iStackCount); + WriteAlways(bitStream, iStackRemaining); + BitStreamUtils::WriteOptional(bitStream, iSubkey, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, iTradeID, LWOOBJID_EMPTY); + } + + bool RemoveItemFromInventory::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bConfirmed)); + VALIDATE_READ(bitStream.Read(bDeleteItem)); + VALIDATE_READ(bitStream.Read(bOutSuccess)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, eInvType, INVENTORY_MAX)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, eLootTypeSource, LOOTTYPE_NONE)); + VALIDATE_READ(ReadNameValue(bitStream, extraInfo)); + VALIDATE_READ(bitStream.Read(forceDeletion)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iLootTypeSource, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iObjID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iObjTemplate, LOT_NULL)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iRequestingObjID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iStackCount, 1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iStackRemaining, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iSubkey, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iTradeID, LWOOBJID_EMPTY)); + return true; + } + + void RemoveItemFromInventory::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnRemoveItemFromInventory(*this); + } + + void EquipInventory::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bIgnoreCooldown); + bitStream.Write(bOutSuccess); + bitStream.Write(itemToEquip); + } + + bool EquipInventory::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bIgnoreCooldown)); + VALIDATE_READ(bitStream.Read(bOutSuccess)); + VALIDATE_READ(bitStream.Read(itemToEquip)); + return true; + } + + void EquipInventory::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnEquipInventory(*this); + } + + void UnEquipInventory::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bEvenIfDead); + bitStream.Write(bIgnoreCooldown); + bitStream.Write(bOutSuccess); + bitStream.Write(itemToUnequip); + } + + bool UnEquipInventory::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bEvenIfDead)); + VALIDATE_READ(bitStream.Read(bIgnoreCooldown)); + VALIDATE_READ(bitStream.Read(bOutSuccess)); + VALIDATE_READ(bitStream.Read(itemToUnequip)); + return true; + } + + void UnEquipInventory::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnUnEquipInventory(*this); + } + + void MoveItemInInventory::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, destInvType, eInventoryType::INVALID); + bitStream.Write(iObjID); + bitStream.Write(inventoryType); + bitStream.Write(responseCode); + bitStream.Write(slot); + } + + bool MoveItemInInventory::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, destInvType, eInventoryType::INVALID)); + VALIDATE_READ(bitStream.Read(iObjID)); + VALIDATE_READ(bitStream.Read(inventoryType)); + VALIDATE_READ(bitStream.Read(responseCode)); + VALIDATE_READ(bitStream.Read(slot)); + return true; + } + + void MoveItemInInventory::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnMoveItemInInventory(*this); + } + + void MoveItemBetweenInventoryTypes::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(inventoryTypeA); + bitStream.Write(inventoryTypeB); + bitStream.Write(objectID); + bitStream.Write(showFlyingLoot); + BitStreamUtils::WriteOptional(bitStream, stackCount, 1); + BitStreamUtils::WriteOptional(bitStream, templateID, LOT_NULL); + } + + bool MoveItemBetweenInventoryTypes::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(inventoryTypeA)); + VALIDATE_READ(bitStream.Read(inventoryTypeB)); + VALIDATE_READ(bitStream.Read(objectID)); + VALIDATE_READ(bitStream.Read(showFlyingLoot)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, stackCount, 1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, templateID, LOT_NULL)); + return true; + } + + void MoveItemBetweenInventoryTypes::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnMoveItemBetweenInventoryTypes(*this); + } + + void RequestMoveItemBetweenInventoryTypes::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bAllowPartial); + BitStreamUtils::WriteOptional(bitStream, destSlot, -1); + BitStreamUtils::WriteOptional(bitStream, iStackCount, 1); + BitStreamUtils::WriteOptional(bitStream, invTypeDst, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, invTypeSrc, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, itemID, LWOOBJID_EMPTY); + bitStream.Write(showFlyingLoot); + BitStreamUtils::WriteOptional(bitStream, subkey, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, itemLOT, LOT_NULL); + } + + bool RequestMoveItemBetweenInventoryTypes::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bAllowPartial)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, destSlot, -1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iStackCount, 1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, invTypeDst, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, invTypeSrc, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemID, LWOOBJID_EMPTY)); + VALIDATE_READ(bitStream.Read(showFlyingLoot)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, subkey, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemLOT, LOT_NULL)); + return true; + } + + void RequestMoveItemBetweenInventoryTypes::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (invTypeDst != invTypeSrc && inventoryComponent) { + inventoryComponent->OnRequestMoveItemBetweenInventoryTypes(*this, sysAddr); + return; + } + + ResponseMoveItemBetweenInventoryTypes response; + response.target = entity.GetObjectID(); + response.inventoryTypeDestination = invTypeDst; + response.inventoryTypeSource = invTypeSrc; + response.response = eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC; + response.SendToClient(sysAddr); + } + + void ResponseMoveItemBetweenInventoryTypes::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, inventoryTypeDestination, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, inventoryTypeSource, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, response, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC); + } + + bool ResponseMoveItemBetweenInventoryTypes::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, inventoryTypeDestination, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, inventoryTypeSource, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, response, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC)); + return true; + } + + void MoveInventoryBatch::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bAllowPartial); + bitStream.Write(bOutSuccess); + BitStreamUtils::WriteOptional(bitStream, count, 1); + BitStreamUtils::WriteOptional(bitStream, dstBag, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, moveLOT, LOT_NULL); + BitStreamUtils::WriteOptional(bitStream, moveSubkey, LWOOBJID_EMPTY); + bitStream.Write(showFlyingLoot); + BitStreamUtils::WriteOptional(bitStream, srcBag, eInventoryType::ITEMS); + BitStreamUtils::WriteOptional(bitStream, startObjectID, LWOOBJID_EMPTY); + } + + bool MoveInventoryBatch::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bAllowPartial)); + VALIDATE_READ(bitStream.Read(bOutSuccess)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, dstBag, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, moveLOT, LOT_NULL)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, moveSubkey, LWOOBJID_EMPTY)); + VALIDATE_READ(bitStream.Read(showFlyingLoot)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, srcBag, eInventoryType::ITEMS)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, startObjectID, LWOOBJID_EMPTY)); + return true; + } + + void NotifyNotEnoughInvSpace::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(freeSlotsNeeded); + BitStreamUtils::WriteOptional(bitStream, inventoryType, eInventoryType::ITEMS); + } + + bool NotifyNotEnoughInvSpace::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(freeSlotsNeeded)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, inventoryType, eInventoryType::ITEMS)); + return true; + } + + void MarkInventoryItemAsActive::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bActive); + BitStreamUtils::WriteOptional(bitStream, iType, eUnequippableActiveType::INVALID); + BitStreamUtils::WriteOptional(bitStream, itemID, LWOOBJID_EMPTY); + } + + bool MarkInventoryItemAsActive::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bActive)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, iType, eUnequippableActiveType::INVALID)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemID, LWOOBJID_EMPTY)); + return true; + } + + void ConsumeClientItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bSuccess); + bitStream.Write(item); + } + + bool ConsumeClientItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bSuccess)); + VALIDATE_READ(bitStream.Read(item)); + return true; + } + + void ClientItemConsumed::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(item); + } + + bool ClientItemConsumed::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(item)); + return true; + } + + void ClientItemConsumed::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnClientItemConsumed(*this); + } + + void UseNonEquipmentItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(itemToUse); + } + + bool UseNonEquipmentItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(itemToUse)); + return true; + } + + void UseNonEquipmentItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnUseNonEquipmentItem(*this); + } + + void UseItemResult::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(itemTemplateID); + bitStream.Write(useItemResult); + } + + bool UseItemResult::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(itemTemplateID)); + VALIDATE_READ(bitStream.Read(useItemResult)); + return true; + } + + void UseItemRequirementsResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(eUseResponse); + } + + bool UseItemRequirementsResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(eUseResponse)); + return true; + } + + void SetConsumableItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(itemTemplateID); + } + + bool SetConsumableItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(itemTemplateID)); + return true; + } + + void SetConsumableItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->SetConsumable(itemTemplateID); + } + + void PushEquippedItemsState::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->PushEquippedItems(); + } + + void PopEquippedItemsState::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (!inventoryComponent) return; + inventoryComponent->PopEquippedItems(); + Game::entityManager->SerializeEntity(&entity); // so it updates on client side + } + + void UpdateInventoryGroup::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, action); + BitStreamUtils::WriteLengthPrefixed(bitStream, groupID); + BitStreamUtils::WriteLengthPrefixed(bitStream, groupName); + bitStream.Write(inventoryType); + bitStream.Write(locked); + } + + bool UpdateInventoryGroup::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, action)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, groupID)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, groupName, MAX_MESSAGE_LENGTH / 2)); + VALIDATE_READ(bitStream.Read(inventoryType)); + VALIDATE_READ(bitStream.Read(locked)); + return true; + } + + void UpdateInventoryGroup::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnUpdateInventoryGroup(*this); + } + + void UpdateInventoryGroupContents::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, action); + BitStreamUtils::WriteLengthPrefixed(bitStream, groupID); + bitStream.Write(inventoryType); + bitStream.Write(lot); + } + + bool UpdateInventoryGroupContents::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, action)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, groupID)); + VALIDATE_READ(bitStream.Read(inventoryType)); + VALIDATE_READ(bitStream.Read(lot)); + return true; + } + + void UpdateInventoryGroupContents::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (inventoryComponent) inventoryComponent->OnUpdateInventoryGroupContents(*this); + } +} diff --git a/dGame/dGameMessages/InventoryMessages.h b/dGame/dGameMessages/InventoryMessages.h new file mode 100644 index 000000000..366948a12 --- /dev/null +++ b/dGame/dGameMessages/InventoryMessages.h @@ -0,0 +1,314 @@ +#ifndef INVENTORYMESSAGES_H +#define INVENTORYMESSAGES_H + +#include "GameMessages.h" +#include "eInventoryType.h" +#include "eLootSourceType.h" +#include "eReponseMoveItemBetweenInventoryTypeCode.h" +#include "eUnequippableActiveType.h" +#include "eUseItemResponse.h" +#include "NiPoint3.h" + +#include + +class Item; + +// Game messages for inventories and items: adding, removing, moving, equipping and using items, inventory sizes +// 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. +namespace GameMessages { + // Server -> client. + struct AddItemToInventoryClientSync : public NetGameMsg { + AddItemToInventoryClientSync() : NetGameMsg(MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + // Fills the fields that describe the item itself (bound flags, extra info, LOT, inventory, stack total and + // slot). The caller sets the rest. + void SetItem(const Item& item); + + bool bBound{}; + bool bIsBOE{}; + bool bIsBOP{}; + eLootSourceType eLootTypeSource{ eLootSourceType::NONE }; // optional + // Name value text: u32 length and the characters, then a null character if it is not empty. + std::u16string extraInfo{}; + LOT iObjTemplate{}; + LWOOBJID iSubkey{ LWOOBJID_EMPTY }; // optional + eInventoryType invType{ eInventoryType::ITEMS }; // optional + int32_t itemCount{ 1 }; // optional + uint32_t itemsTotal{ 0 }; // optional + LWOOBJID newObjID{}; + NiPoint3 ni3FlyingLootPosit{}; + bool showFlyingLoot{}; + uint32_t slotID{}; + }; + + // Server -> client. + struct SetInventorySize : public NetGameMsg { + SetInventorySize() : NetGameMsg(MessageType::Game::SET_INVENTORY_SIZE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + eInventoryType inventoryType{}; + int32_t size{}; + }; + + // Both directions: the client asks to delete (trash) an item, and the server tells the client an item left + // its inventory. DLU always sends the flag of the fields marked "always sent" (they are still read as optional). + struct RemoveItemFromInventory : public NetGameMsg { + RemoveItemFromInventory() : NetGameMsg(MessageType::Game::REMOVE_ITEM_FROM_INVENTORY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bConfirmed{}; + bool bDeleteItem{ true }; + bool bOutSuccess{}; + int32_t eInvType{ INVENTORY_MAX }; // optional, always sent + int32_t eLootTypeSource{ LOOTTYPE_NONE }; // optional, always sent + // Name value text: u32 length and the characters, then a null character if it is not empty. + std::u16string extraInfo{}; + bool forceDeletion{ true }; + LWOOBJID iLootTypeSource{ LWOOBJID_EMPTY }; // optional + LWOOBJID iObjID{ LWOOBJID_EMPTY }; // optional, always sent + LOT iObjTemplate{ LOT_NULL }; // optional, always sent + LWOOBJID iRequestingObjID{ LWOOBJID_EMPTY }; // optional + uint32_t iStackCount{ 1 }; // optional, always sent + uint32_t iStackRemaining{ 0 }; // optional, always sent + LWOOBJID iSubkey{ LWOOBJID_EMPTY }; // optional + LWOOBJID iTradeID{ LWOOBJID_EMPTY }; // optional + }; + + // Client -> server. + struct EquipInventory : public NetGameMsg { + EquipInventory() : NetGameMsg(MessageType::Game::EQUIP_INVENTORY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bIgnoreCooldown{}; + bool bOutSuccess{}; + LWOOBJID itemToEquip{}; + }; + + // Client -> server. The client also sends an optional replacementObjectID after itemToUnequip, which DLU + // has never read (and still does not). + struct UnEquipInventory : public NetGameMsg { + UnEquipInventory() : NetGameMsg(MessageType::Game::UN_EQUIP_INVENTORY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bEvenIfDead{}; + bool bIgnoreCooldown{}; + bool bOutSuccess{}; + LWOOBJID itemToUnequip{}; + }; + + // Client -> server. + struct MoveItemInInventory : public NetGameMsg { + MoveItemInInventory() : NetGameMsg(MessageType::Game::MOVE_ITEM_IN_INVENTORY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t destInvType{ eInventoryType::INVALID }; // optional + LWOOBJID iObjID{}; + int32_t inventoryType{}; + int32_t responseCode{}; + int32_t slot{}; + }; + + // Client -> server. + struct MoveItemBetweenInventoryTypes : public NetGameMsg { + MoveItemBetweenInventoryTypes() : NetGameMsg(MessageType::Game::MOVE_ITEM_BETWEEN_INVENTORY_TYPES) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + eInventoryType inventoryTypeA{}; + eInventoryType inventoryTypeB{}; + LWOOBJID objectID{}; + bool showFlyingLoot{ true }; + uint32_t stackCount{ 1 }; // optional + LOT templateID{ LOT_NULL }; // optional + }; + + // Client -> server. Answered with ResponseMoveItemBetweenInventoryTypes. + struct RequestMoveItemBetweenInventoryTypes : public NetGameMsg { + RequestMoveItemBetweenInventoryTypes() : NetGameMsg(MessageType::Game::REQUEST_MOVE_ITEM_BETWEEN_INVENTORY_TYPES) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bAllowPartial{}; + int32_t destSlot{ -1 }; // optional + int32_t iStackCount{ 1 }; // optional + eInventoryType invTypeDst{ eInventoryType::ITEMS }; // optional + eInventoryType invTypeSrc{ eInventoryType::ITEMS }; // optional + LWOOBJID itemID{ LWOOBJID_EMPTY }; // optional + bool showFlyingLoot{}; + LWOOBJID subkey{ LWOOBJID_EMPTY }; // optional + LOT itemLOT{ LOT_NULL }; // optional + }; + + // Server -> client. + struct ResponseMoveItemBetweenInventoryTypes : public NetGameMsg { + ResponseMoveItemBetweenInventoryTypes() : NetGameMsg(MessageType::Game::RESPONSE_MOVE_ITEM_BETWEEN_INVENTORY_TYPES) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + eInventoryType inventoryTypeDestination{ eInventoryType::ITEMS }; // optional + eInventoryType inventoryTypeSource{ eInventoryType::ITEMS }; // optional + eReponseMoveItemBetweenInventoryTypeCode response{ eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC }; // optional + }; + + // Server -> client. Moves count items out of srcBag (the client fills dstBag from a following + // AddItemToInventoryClientSync). Laid out as the client reads it; DLU does not send it. + struct MoveInventoryBatch : public NetGameMsg { + MoveInventoryBatch() : NetGameMsg(MessageType::Game::MOVE_INVENTORY_BATCH) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bAllowPartial{}; + bool bOutSuccess{}; + uint32_t count{ 1 }; // optional + eInventoryType dstBag{ eInventoryType::ITEMS }; // optional + LOT moveLOT{ LOT_NULL }; // optional + LWOOBJID moveSubkey{ LWOOBJID_EMPTY }; // optional + bool showFlyingLoot{}; + eInventoryType srcBag{ eInventoryType::ITEMS }; // optional + LWOOBJID startObjectID{ LWOOBJID_EMPTY }; // optional + }; + + // Server -> client. DLU sends this with the message ID of VehicleNotifyFinishedRace, not + // NOTIFY_NOT_ENOUGH_INV_SPACE; kept as it has always been sent (see docs/PacketArchitecture.md: wire fixes + // are separate changes). + struct NotifyNotEnoughInvSpace : public NetGameMsg { + NotifyNotEnoughInvSpace() : NetGameMsg(MessageType::Game::VEHICLE_NOTIFY_FINISHED_RACE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + uint32_t freeSlotsNeeded{}; + eInventoryType inventoryType{ eInventoryType::ITEMS }; // optional + }; + + // Server -> client. No payload. The client sends it to itself normally; DLU sends it to refresh the inventory + // UI (its defaults get around items that would otherwise stay invisible). + struct UpdateInventoryUi : public NetGameMsg { + UpdateInventoryUi() : NetGameMsg(MessageType::Game::UPDATE_INVENTORY_UI) {} + }; + + // Server -> client. Marks a pet or mount item as in use (or no longer). + struct MarkInventoryItemAsActive : public NetGameMsg { + MarkInventoryItemAsActive() : NetGameMsg(MessageType::Game::MARK_INVENTORY_ITEM_AS_ACTIVE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bActive{}; + eUnequippableActiveType iType{ eUnequippableActiveType::INVALID }; // optional + LWOOBJID itemID{ LWOOBJID_EMPTY }; // optional + }; + + // Server -> client. + struct ConsumeClientItem : public NetGameMsg { + ConsumeClientItem() : NetGameMsg(MessageType::Game::CONSUME_CLIENT_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bSuccess{}; + LWOOBJID item{}; + }; + + // Client -> server. + struct ClientItemConsumed : public NetGameMsg { + ClientItemConsumed() : NetGameMsg(MessageType::Game::CLIENT_ITEM_CONSUMED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID item{}; + }; + + // Client -> server. + struct UseNonEquipmentItem : public NetGameMsg { + UseNonEquipmentItem() : NetGameMsg(MessageType::Game::USE_NON_EQUIPMENT_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID itemToUse{}; + }; + + // Server -> client. + struct UseItemResult : public NetGameMsg { + UseItemResult() : NetGameMsg(MessageType::Game::USE_ITEM_RESULT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LOT itemTemplateID{}; + bool useItemResult{}; + }; + + // Server -> client. + struct UseItemRequirementsResponse : public NetGameMsg { + UseItemRequirementsResponse() : NetGameMsg(MessageType::Game::USE_ITEM_REQUIREMENTS_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + eUseItemResponse eUseResponse{}; + }; + + // Client -> server. + struct SetConsumableItem : public NetGameMsg { + SetConsumableItem() : NetGameMsg(MessageType::Game::SET_CONSUMABLE_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LOT itemTemplateID{}; + }; + + // Client -> server. No payload. + struct PushEquippedItemsState : public NetGameMsg { + PushEquippedItemsState() : NetGameMsg(MessageType::Game::PUSH_EQUIPPED_ITEMS_STATE) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Client -> server. No payload. + struct PopEquippedItemsState : public NetGameMsg { + PopEquippedItemsState() : NetGameMsg(MessageType::Game::POP_EQUIPPED_ITEMS_STATE) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Client -> server. Adds, modifies or removes an inventory group. + struct UpdateInventoryGroup : public NetGameMsg { + UpdateInventoryGroup() : NetGameMsg(MessageType::Game::UPDATE_INVENTORY_GROUP) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::string action{}; // ADD, MODIFY or REMOVE + std::string groupID{}; + std::u16string groupName{}; + eInventoryType inventoryType{}; + bool locked{}; + }; + + // Client -> server. Adds a LOT to or removes one from an inventory group. + struct UpdateInventoryGroupContents : public NetGameMsg { + UpdateInventoryGroupContents() : NetGameMsg(MessageType::Game::UPDATE_INVENTORY_GROUP_CONTENTS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::string action{}; // ADD or REMOVE + std::string groupID{}; + eInventoryType inventoryType{}; + LOT lot{}; + }; +}; + +#endif // INVENTORYMESSAGES_H diff --git a/dGame/dInventory/Inventory.cpp b/dGame/dInventory/Inventory.cpp index f5742165b..0d24f57d5 100644 --- a/dGame/dInventory/Inventory.cpp +++ b/dGame/dInventory/Inventory.cpp @@ -1,5 +1,6 @@ #include "Inventory.h" #include "GameMessages.h" +#include "InventoryMessages.h" #include "Game.h" #include "Item.h" #include "InventoryComponent.h" @@ -80,7 +81,11 @@ void Inventory::SetSize(const uint32_t value) { size = value; - GameMessages::SendSetInventorySize(component->GetParent(), type, static_cast(size)); + GameMessages::SetInventorySize setSize; + setSize.target = component->GetParent()->GetObjectID(); + setSize.inventoryType = type; + setSize.size = static_cast(size); + setSize.SendToClient(component->GetParent()->GetSystemAddress()); } int32_t Inventory::FindEmptySlot() { diff --git a/dGame/dInventory/Item.cpp b/dGame/dInventory/Item.cpp index e288057af..2c751cee8 100644 --- a/dGame/dInventory/Item.cpp +++ b/dGame/dInventory/Item.cpp @@ -6,6 +6,7 @@ #include "ObjectIDManager.h" #include "GeneralUtils.h" #include "GameMessages.h" +#include "InventoryMessages.h" #include "Entity.h" #include "Game.h" #include "Logger.h" @@ -107,7 +108,15 @@ Item::Item( auto* entity = inventoryComponent->GetParent(); if (parent == LWOOBJID_EMPTY) EconomyLedger::RecordItems(entity, lot, this->count, static_cast(lootSourceType)); - GameMessages::SendAddItemToInventoryClientSync(entity, entity->GetSystemAddress(), this, id, showFlyingLoot, static_cast(this->count), subKey, lootSourceType); + GameMessages::AddItemToInventoryClientSync addItem; + addItem.target = entity->GetObjectID(); + addItem.SetItem(*this); + addItem.eLootTypeSource = lootSourceType; + addItem.iSubkey = subKey; + addItem.itemCount = static_cast(this->count); + addItem.newObjID = id; + addItem.showFlyingLoot = showFlyingLoot; + addItem.SendToClient(entity->GetSystemAddress()); if (isModMoveAndEquip) { Equip(); @@ -187,9 +196,25 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem auto* entity = inventory->GetComponent()->GetParent(); if (value > count) { - GameMessages::SendAddItemToInventoryClientSync(entity, entity->GetSystemAddress(), this, id, showFlyingLoot, delta, LWOOBJID_EMPTY, lootSourceType); + GameMessages::AddItemToInventoryClientSync addItem; + addItem.target = entity->GetObjectID(); + addItem.SetItem(*this); + addItem.eLootTypeSource = lootSourceType; + addItem.itemCount = delta; + addItem.newObjID = id; + addItem.showFlyingLoot = showFlyingLoot; + addItem.SendToClient(entity->GetSystemAddress()); } else { - GameMessages::SendRemoveItemFromInventory(entity, entity->GetSystemAddress(), id, lot, inventory->GetType(), delta, value); + GameMessages::RemoveItemFromInventory removeItem; + removeItem.target = entity->GetObjectID(); + removeItem.bConfirmed = true; + removeItem.eInvType = inventory->GetType(); + removeItem.eLootTypeSource = LOOTTYPE_NONE; + removeItem.iObjID = id; + removeItem.iObjTemplate = lot; + removeItem.iStackCount = delta; + removeItem.iStackRemaining = value; + removeItem.SendToClient(entity->GetSystemAddress()); } } @@ -285,7 +310,12 @@ bool Item::Consume() { LOG_DEBUG("Consumed LOT (%i) itemID (%llu). Success=(%d)", lot, id, success); - GameMessages::SendUseItemResult(inventory->GetComponent()->GetParent(), lot, success); + auto* const owner = inventory->GetComponent()->GetParent(); + GameMessages::UseItemResult useItemResult; + useItemResult.target = owner->GetObjectID(); + useItemResult.itemTemplateID = lot; + useItemResult.useItemResult = success; + useItemResult.SendToClient(owner->GetSystemAddress()); const auto myLot = this->lot; if (success && inventory->GetComponent()->RemoveItem(lot, 1, eInventoryType::ALL)) { @@ -372,17 +402,20 @@ void Item::UseNonEquip(Item* item) { success = false; } } else { - GameMessages::SendUseItemRequirementsResponse( - playerInventoryComponent->GetParent()->GetObjectID(), - playerInventoryComponent->GetParent()->GetSystemAddress(), - eUseItemResponse::FailedPrecondition - ); + GameMessages::UseItemRequirementsResponse requirementsResponse; + requirementsResponse.target = playerInventoryComponent->GetParent()->GetObjectID(); + requirementsResponse.eUseResponse = eUseItemResponse::FailedPrecondition; + requirementsResponse.SendToClient(playerInventoryComponent->GetParent()->GetSystemAddress()); success = false; } } } LOG_DEBUG("Player %llu %s used item %i", playerEntity->GetObjectID(), success ? "successfully" : "unsuccessfully", thisLot); - GameMessages::SendUseItemResult(playerInventoryComponent->GetParent(), thisLot, success); + GameMessages::UseItemResult useItemResult; + useItemResult.target = playerInventoryComponent->GetParent()->GetObjectID(); + useItemResult.itemTemplateID = thisLot; + useItemResult.useItemResult = success; + useItemResult.SendToClient(playerInventoryComponent->GetParent()->GetSystemAddress()); } } diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index a49e21d7a..148f1e45f 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -4,6 +4,7 @@ SET(DGAMEMESSAGES_TESTS "GameMessageTests.cpp" "GameMsgSplitTests.cpp" "EffectsMessagesTests.cpp" + "InventoryMessagesTests.cpp" "MissionMessagesTests.cpp" "PetMessagesTests.cpp" "PropertyMessagesTests.cpp" diff --git a/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp new file mode 100644 index 000000000..878c45819 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/InventoryMessagesTests.cpp @@ -0,0 +1,519 @@ +#include "InventoryMessages.h" +#include "GameDependencies.h" +#include "GameMessageTestUtils.h" +#include "Legacy/InventoryMessagesLegacy.h" + +#include "CDComponentsRegistryTable.h" +#include "CDItemComponentTable.h" +#include "eReplicaComponentType.h" +#include "Inventory.h" +#include "Item.h" + +#include +#include +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + const std::vector g_Ints = { 0, 1, -1, 1727, std::numeric_limits::max(), std::numeric_limits::min() }; + const std::vector g_UInts = { 0, 1, 2, 999, std::numeric_limits::max() }; + const std::vector g_Ids = { LWOOBJID_EMPTY, 0x1000000000000001LL, -1 }; + const std::vector g_InventoryTypes = { eInventoryType::ITEMS, eInventoryType::VAULT_ITEMS, eInventoryType::MODELS, eInventoryType::INVALID }; + const std::vector g_WStrings = { u"", u"a", u"ma=1:6416+1:8092", u"été ☃", std::u16string(300, u'x') }; + + PacketBytes Payload(const GameMessages::NetGameMsg& msg) { + RakNet::BitStream bitStream; + msg.Serialize(bitStream); + return FromBitStream(bitStream); + } + + // Serializes msg, then reads it with the legacy read sequence; both must consume exactly the same bits. + template + Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function& read) { + RakNet::BitStream wire; + msg.Serialize(wire); + RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false); + auto result = read(legacyStream); + EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed()); + return result; + } + + // Makes lot a valid item (ItemComponent componentID) with the given bind flags, without a CDClient database. + void RegisterItemLot(const LOT lot, const uint32_t componentID, const bool isBOE, const bool isBOP) { + 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.isBOE = isBOE; + component.isBOP = isBOP; + CDClientManager::GetEntriesMutable().insert_or_assign(componentID, component); + } +} + +class InventoryMessagesTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +TEST_F(InventoryMessagesTests, AddItemToInventoryClientSyncMatchesLegacy) { + RegisterItemLot(1727, 90001, false, false); + RegisterItemLot(6416, 90002, true, false); + RegisterItemLot(8092, 90003, false, true); + + LwoNameValue emptyConfig; + LwoNameValue config; + config.Insert(u"assemblyPartLOTs", std::u16string(u"1:6416+1:8092")); + config.Insert(u"userModelID", 42); + + uint32_t slot = 0; + for (const auto inventoryType : { eInventoryType::ITEMS, eInventoryType::MODELS }) { + Inventory inventory(inventoryType, 400, {}, nullptr); + for (const LOT lot : { 1727, 6416, 8092 }) { + for (const uint32_t count : { 0u, 1u, 7u }) { + for (const bool bound : { false, true }) { + for (const auto* itemConfig : { &emptyConfig, &config }) { + // The inventory owns (and deletes) the item. + auto* item = new Item(0x5000 + slot, lot, &inventory, slot, count, bound, *itemConfig, LWOOBJID_EMPTY, LWOOBJID_EMPTY, eLootSourceType::NONE); + slot++; + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const LWOOBJID newID : { LWOOBJID_EMPTY, LWOOBJID{ 0x7777 } }) { + for (const bool showFlyingLoot : { false, true }) { + for (const int itemCount : { 1, 0, -1, 5 }) { + for (const LWOOBJID subKey : { LWOOBJID_EMPTY, LWOOBJID{ 0x99 } }) { + for (const auto lootSource : { eLootSourceType::NONE, eLootSourceType::QUICKBUILD }) { + GameMessages::AddItemToInventoryClientSync msg; + msg.target = target; + msg.SetItem(*item); + msg.eLootTypeSource = lootSource; + msg.iSubkey = subKey; + msg.itemCount = itemCount; + msg.newObjID = newID; + msg.showFlyingLoot = showFlyingLoot; + ExpectSameAsLegacy([&](const SystemAddress& a) { + LegacyGameMessages::SendAddItemToInventoryClientSync(&entity, a, item, newID, showFlyingLoot, itemCount, subKey, lootSource); + }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.extraInfo, msg.extraInfo); + EXPECT_EQ(copy.iObjTemplate, lot); + EXPECT_EQ(copy.invType, inventoryType); + EXPECT_EQ(copy.itemsTotal, count); + EXPECT_EQ(copy.itemCount, itemCount); + EXPECT_EQ(copy.iSubkey, subKey); + } + } + } + } + } + } + } + } + } + } + } +} + +TEST_F(InventoryMessagesTests, SmallMessagesMatchLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + const auto toEntity = [&](const std::function& legacy, const GameMessages::NetGameMsg& msg) { + // The legacy functions sent to entity->GetSystemAddress() only. + const auto legacyPackets = Capture(legacy); + const auto ours = Capture([&] { msg.SendToClient(entity.GetSystemAddress()); }); + ASSERT_EQ(legacyPackets.size(), 1); + ASSERT_EQ(ours.size(), 1); + EXPECT_PACKET_EQ(FromCapture(legacyPackets[0]), FromCapture(ours[0])); + EXPECT_EQ(legacyPackets[0].sysAddr, ours[0].sysAddr); + EXPECT_EQ(legacyPackets[0].broadcast, ours[0].broadcast); + }; + + for (const auto type : g_InventoryTypes) { + for (const auto size : g_Ints) { + GameMessages::SetInventorySize msg; + msg.target = target; + msg.inventoryType = type; + msg.size = size; + toEntity([&] { LegacyGameMessages::SendSetInventorySize(&entity, type, size); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.inventoryType, type); + EXPECT_EQ(copy.size, size); + } + } + + for (const auto item : g_Ids) { + for (const bool success : { false, true }) { + GameMessages::ConsumeClientItem consume; + consume.target = target; + consume.bSuccess = success; + consume.item = item; + toEntity([&] { LegacyGameMessages::SendConsumeClientItem(&entity, success, item); }, consume); + const auto copy = RoundTrip(consume); + EXPECT_EQ(copy.bSuccess, success); + EXPECT_EQ(copy.item, item); + } + } + + for (const auto lot : g_Ints) { + for (const bool success : { false, true }) { + GameMessages::UseItemResult result; + result.target = target; + result.itemTemplateID = lot; + result.useItemResult = success; + toEntity([&] { LegacyGameMessages::SendUseItemResult(&entity, lot, success); }, result); + const auto copy = RoundTrip(result); + EXPECT_EQ(copy.itemTemplateID, lot); + EXPECT_EQ(copy.useItemResult, success); + } + } + + for (const auto response : { eUseItemResponse::NoImaginationForPet, eUseItemResponse::FailedPrecondition, static_cast(0) }) { + GameMessages::UseItemRequirementsResponse msg; + msg.target = target; + msg.eUseResponse = response; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUseItemRequirementsResponse(target, a, response); }, msg, SendMode::SendToClient); + EXPECT_EQ(RoundTrip(msg).eUseResponse, response); + } + + for (const auto dst : g_InventoryTypes) { + for (const auto src : g_InventoryTypes) { + for (const auto code : { eReponseMoveItemBetweenInventoryTypeCode::SUCCESS, eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC, eReponseMoveItemBetweenInventoryTypeCode::FAIL_INV_FULL, eReponseMoveItemBetweenInventoryTypeCode::FAIL_CANT_MOVE_THINKING_HAT }) { + GameMessages::ResponseMoveItemBetweenInventoryTypes msg; + msg.target = target; + msg.inventoryTypeDestination = dst; + msg.inventoryTypeSource = src; + msg.response = code; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendResponseMoveItemBetweenInventoryTypes(target, a, dst, src, code); }, msg, SendMode::SendToClient); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.inventoryTypeDestination, dst); + EXPECT_EQ(copy.inventoryTypeSource, src); + EXPECT_EQ(copy.response, code); + } + } + } + + for (const auto slots : g_UInts) { + for (const auto type : g_InventoryTypes) { + GameMessages::NotifyNotEnoughInvSpace msg; + msg.target = target; + msg.freeSlotsNeeded = slots; + msg.inventoryType = type; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendNotifyNotEnoughInvSpace(target, slots, type, a); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.freeSlotsNeeded, slots); + EXPECT_EQ(copy.inventoryType, type); + } + } + + for (const bool active : { false, true }) { + for (const auto type : { eUnequippableActiveType::INVALID, eUnequippableActiveType::PET, eUnequippableActiveType::MOUNT }) { + for (const auto item : g_Ids) { + GameMessages::MarkInventoryItemAsActive msg; + msg.target = target; + msg.bActive = active; + msg.iType = type; + msg.itemID = item; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendMarkInventoryItemAsActive(target, active, type, item, a); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.bActive, active); + EXPECT_EQ(copy.iType, type); + EXPECT_EQ(copy.itemID, item); + } + } + } + + GameMessages::UpdateInventoryUi updateUi; + updateUi.target = target; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUpdateInventoryUi(target, a); }, updateUi, SendMode::SendToClient); + } +} + +TEST_F(InventoryMessagesTests, RemoveItemFromInventoryMatchesLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto id : g_Ids) { + for (const LOT lot : { LOT_NULL, LOT{ 1727 } }) { + for (const auto type : g_InventoryTypes) { + for (const auto stackCount : g_UInts) { + for (const auto stackRemaining : g_UInts) { + GameMessages::RemoveItemFromInventory msg; + msg.target = target; + msg.bConfirmed = true; + msg.eInvType = type; + msg.eLootTypeSource = LOOTTYPE_NONE; + msg.iObjID = id; + msg.iObjTemplate = lot; + msg.iStackCount = stackCount; + msg.iStackRemaining = stackRemaining; + ExpectSameAsLegacy([&](const SystemAddress& a) { + LegacyGameMessages::SendRemoveItemFromInventory(&entity, a, id, lot, type, stackCount, stackRemaining); + }, msg, SendMode::SendToClient); + + // What the server sends reads back the same with the old handler's read sequence. + const auto legacy = ReadWithLegacy(msg, LegacyGameMessages::ReadRemoveItemFromInventory); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.eInvType, legacy.eInvType); + EXPECT_EQ(copy.iObjID, legacy.iObjID); + EXPECT_EQ(copy.iObjTemplate, legacy.iObjTemplate); + EXPECT_EQ(copy.iStackCount, legacy.iStackCount); + EXPECT_EQ(copy.iStackRemaining, legacy.iStackRemaining); + } + } + } + } + } + } +} + +TEST_F(InventoryMessagesTests, InboundReadsLikeLegacy) { + // RemoveItemFromInventory as the client sends it (every optional field both ways is covered by the + // optional reads; the "always sent" flags only matter for what the server writes). + for (const bool flag : { false, true }) { + for (const auto& extraInfo : g_WStrings) { + for (const auto id : g_Ids) { + GameMessages::RemoveItemFromInventory msg; + msg.bConfirmed = flag; + msg.bDeleteItem = !flag; + msg.bOutSuccess = flag; + msg.extraInfo = extraInfo; + msg.forceDeletion = flag; + msg.iLootTypeSource = id; + msg.iObjID = id; + msg.iRequestingObjID = id; + msg.iSubkey = id; + msg.iTradeID = id; + const auto legacy = ReadWithLegacy(msg, LegacyGameMessages::ReadRemoveItemFromInventory); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.bConfirmed, legacy.bConfirmed); + EXPECT_EQ(copy.bDeleteItem, legacy.bDeleteItem); + EXPECT_EQ(copy.bOutSuccess, legacy.bOutSuccess); + EXPECT_EQ(copy.extraInfo, legacy.extraInfo); + EXPECT_EQ(copy.forceDeletion, legacy.forceDeletion); + EXPECT_EQ(copy.iLootTypeSource, legacy.iLootTypeSource); + EXPECT_EQ(copy.iRequestingObjID, legacy.iRequestingObjID); + EXPECT_EQ(copy.iSubkey, legacy.iSubkey); + EXPECT_EQ(copy.iTradeID, legacy.iTradeID); + ExpectTruncatedFails(msg); + } + } + } + // The same message with every optional flag clear, as a client that relies on the defaults would send it. + { + RakNet::BitStream wire; + wire.Write1(); // bConfirmed + wire.Write1(); // bDeleteItem + wire.Write0(); // bOutSuccess + wire.Write0(); // eInvType + wire.Write0(); // eLootTypeSource + wire.Write(0); // extraInfo + wire.Write1(); // forceDeletion + for (int i = 0; i < 8; i++) wire.Write0(); // the eight optional ids and counts + RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false); + const auto legacy = LegacyGameMessages::ReadRemoveItemFromInventory(legacyStream); + GameMessages::RemoveItemFromInventory msg; + ASSERT_TRUE(msg.Deserialize(wire)); + EXPECT_EQ(msg.eInvType, legacy.eInvType); + EXPECT_EQ(msg.eLootTypeSource, legacy.eLootTypeSource); + EXPECT_EQ(msg.iObjTemplate, legacy.iObjTemplate); + EXPECT_EQ(msg.iStackCount, legacy.iStackCount); + EXPECT_EQ(msg.iStackRemaining, legacy.iStackRemaining); + } + + for (const auto id : g_Ids) { + for (const bool flag : { false, true }) { + GameMessages::EquipInventory equip; + equip.bIgnoreCooldown = !flag; + equip.bOutSuccess = flag; + equip.itemToEquip = id; + const auto legacyEquip = ReadWithLegacy(equip, LegacyGameMessages::ReadEquipItem); + EXPECT_EQ(RoundTrip(equip).itemToEquip, legacyEquip.objectID); + ExpectTruncatedFails(equip); + + GameMessages::UnEquipInventory unequip; + unequip.bEvenIfDead = flag; + unequip.bIgnoreCooldown = !flag; + unequip.bOutSuccess = flag; + unequip.itemToUnequip = id; + const auto legacyUnequip = ReadWithLegacy(unequip, LegacyGameMessages::ReadUnequipItem); + EXPECT_EQ(RoundTrip(unequip).itemToUnequip, legacyUnequip.objectID); + ExpectTruncatedFails(unequip); + + for (const auto value : g_Ints) { + GameMessages::MoveItemInInventory move; + move.destInvType = flag ? value : static_cast(eInventoryType::INVALID); + move.iObjID = id; + move.inventoryType = value; + move.responseCode = -value; + move.slot = value / 2; + const auto legacyMove = ReadWithLegacy(move, LegacyGameMessages::ReadMoveItemInInventory); + const auto moveCopy = RoundTrip(move); + EXPECT_EQ(moveCopy.destInvType, legacyMove.destInvType); + EXPECT_EQ(moveCopy.iObjID, legacyMove.iObjID); + EXPECT_EQ(moveCopy.inventoryType, legacyMove.inventoryType); + EXPECT_EQ(moveCopy.responseCode, legacyMove.responseCode); + EXPECT_EQ(moveCopy.slot, legacyMove.slot); + ExpectTruncatedFails(move); + + GameMessages::RequestMoveItemBetweenInventoryTypes request; + request.bAllowPartial = flag; + request.destSlot = value; + request.iStackCount = -value; + request.invTypeDst = flag ? eInventoryType::VAULT_ITEMS : eInventoryType::ITEMS; + request.invTypeSrc = flag ? eInventoryType::ITEMS : eInventoryType::MODELS; + request.itemID = id; + request.showFlyingLoot = !flag; + request.subkey = id; + request.itemLOT = value; + const auto legacyRequest = ReadWithLegacy(request, LegacyGameMessages::ReadRequestMoveItemBetweenInventoryTypes); + const auto requestCopy = RoundTrip(request); + EXPECT_EQ(requestCopy.bAllowPartial, legacyRequest.bAllowPartial); + EXPECT_EQ(requestCopy.destSlot, legacyRequest.destSlot); + EXPECT_EQ(requestCopy.iStackCount, legacyRequest.iStackCount); + EXPECT_EQ(requestCopy.invTypeDst, legacyRequest.invTypeDst); + EXPECT_EQ(requestCopy.invTypeSrc, legacyRequest.invTypeSrc); + EXPECT_EQ(requestCopy.itemID, legacyRequest.itemID); + EXPECT_EQ(requestCopy.showFlyingLoot, legacyRequest.showFlyingLoot); + EXPECT_EQ(requestCopy.subkey, legacyRequest.subkey); + if (value != LOT_NULL) EXPECT_EQ(requestCopy.itemLOT, legacyRequest.itemLOT); // unused; the old default was 0 + ExpectTruncatedFails(request); + } + + for (const auto count : g_UInts) { + for (const LOT lot : { LOT_NULL, LOT{ 1727 } }) { + GameMessages::MoveItemBetweenInventoryTypes move; + move.inventoryTypeA = eInventoryType::MODELS; + move.inventoryTypeB = flag ? eInventoryType::VAULT_MODELS : eInventoryType::TEMP_MODELS; + move.objectID = id; + move.showFlyingLoot = flag; + move.stackCount = count; + move.templateID = lot; + const auto legacy = ReadWithLegacy(move, LegacyGameMessages::ReadMoveItemBetweenInventoryTypes); + const auto copy = RoundTrip(move); + EXPECT_EQ(copy.inventoryTypeA, legacy.inventoryTypeA); + EXPECT_EQ(copy.inventoryTypeB, legacy.inventoryTypeB); + EXPECT_EQ(copy.objectID, legacy.objectID); + EXPECT_EQ(copy.showFlyingLoot, legacy.showFlyingLoot); + EXPECT_EQ(copy.stackCount, legacy.stackCount); + EXPECT_EQ(copy.templateID, legacy.templateID); + ExpectTruncatedFails(move); + } + } + } + + GameMessages::ClientItemConsumed consumed; + consumed.item = id; + EXPECT_EQ(ReadWithLegacy(consumed, LegacyGameMessages::ReadSingle), id); + EXPECT_EQ(RoundTrip(consumed).item, id); + ExpectTruncatedFails(consumed); + + GameMessages::UseNonEquipmentItem use; + use.itemToUse = id; + EXPECT_EQ(ReadWithLegacy(use, LegacyGameMessages::ReadSingle), id); + EXPECT_EQ(RoundTrip(use).itemToUse, id); + ExpectTruncatedFails(use); + } + + for (const auto lot : g_Ints) { + GameMessages::SetConsumableItem consumable; + consumable.itemTemplateID = lot; + EXPECT_EQ(ReadWithLegacy(consumable, LegacyGameMessages::ReadSingle), lot); + EXPECT_EQ(RoundTrip(consumable).itemTemplateID, lot); + ExpectTruncatedFails(consumable); + } + + for (const std::string action : std::vector{ "ADD", "MODIFY", "REMOVE", "", "bogus" }) { + for (const std::string& groupID : std::vector{ "", "user_group1", std::string(200, 'g') }) { + for (const auto& groupName : g_WStrings) { + for (const auto type : { eInventoryType::ITEMS, eInventoryType::BRICKS, eInventoryType::MODELS }) { + for (const bool locked : { false, true }) { + GameMessages::UpdateInventoryGroup group; + group.action = action; + group.groupID = groupID; + group.groupName = groupName; + group.inventoryType = type; + group.locked = locked; + const auto legacy = ReadWithLegacy(group, LegacyGameMessages::ReadUpdateInventoryGroup); + ASSERT_TRUE(legacy.ok); + const auto copy = RoundTrip(group); + EXPECT_EQ(copy.action, legacy.action); + EXPECT_EQ(copy.groupID, legacy.groupId); + EXPECT_EQ(copy.groupName, legacy.groupName); + EXPECT_EQ(copy.inventoryType, legacy.inventory); + EXPECT_EQ(copy.locked, legacy.locked); + ExpectTruncatedFails(group); + } + } + for (const auto lot : g_Ints) { + GameMessages::UpdateInventoryGroupContents contents; + contents.action = action; + contents.groupID = groupID; + contents.inventoryType = eInventoryType::BRICKS; + contents.lot = lot; + const auto legacy = ReadWithLegacy(contents, LegacyGameMessages::ReadUpdateInventoryGroupContents); + ASSERT_TRUE(legacy.ok); + const auto copy = RoundTrip(contents); + EXPECT_EQ(copy.action, legacy.action); + EXPECT_EQ(copy.groupID, legacy.groupId); + EXPECT_EQ(copy.inventoryType, legacy.inventory); + EXPECT_EQ(copy.lot, legacy.lot); + ExpectTruncatedFails(contents); + } + } + } + } + + // A group name longer than the old handler allowed is rejected. + RakNet::BitStream tooLong; + tooLong.Write(0); + tooLong.Write(0); + tooLong.Write(MAX_MESSAGE_LENGTH / 2 + 1); + GameMessages::UpdateInventoryGroup group; + EXPECT_FALSE(group.Deserialize(tooLong)); +} + +// MoveInventoryBatch had no oracle (the old function was never called); it follows the client's layout +// (legouniverse.exe 1.10.64, LWOInventoryComponent_Common::msgMoveInventoryBatch reads moveLOT and moveSubKey). +TEST_F(InventoryMessagesTests, MoveInventoryBatchRoundTrips) { + GameMessages::MoveInventoryBatch batch; + EXPECT_PACKET_EQ(FromHex("00 00", 9), Payload(batch)); + batch.bAllowPartial = true; + batch.count = 3; + batch.dstBag = eInventoryType::VAULT_ITEMS; + batch.moveLOT = 1727; + batch.moveSubkey = 0x22; + batch.showFlyingLoot = true; + batch.srcBag = eInventoryType::MODELS; + batch.startObjectID = 0x33; + const auto copy = RoundTrip(batch); + EXPECT_EQ(copy.count, 3); + EXPECT_EQ(copy.dstBag, eInventoryType::VAULT_ITEMS); + EXPECT_EQ(copy.moveLOT, 1727); + EXPECT_EQ(copy.moveSubkey, 0x22); + EXPECT_EQ(copy.srcBag, eInventoryType::MODELS); + EXPECT_EQ(copy.startObjectID, 0x33); + ExpectTruncatedFails(batch); +} + +// Independent of the legacy code: hand computed payloads (RakNet writes MSB first). +TEST_F(InventoryMessagesTests, GoldenBytes) { + GameMessages::EquipInventory equip; + equip.bOutSuccess = true; + equip.itemToEquip = 0x11; + // 0, 1, then 11 00 00 00 00 00 00 00 + EXPECT_PACKET_EQ(FromHex("44 40 00 00 00 00 00 00 00", 66), Payload(equip)); + + GameMessages::ResponseMoveItemBetweenInventoryTypes response; + response.inventoryTypeDestination = eInventoryType::VAULT_ITEMS; // 1 + response.response = eReponseMoveItemBetweenInventoryTypeCode::SUCCESS; // 0 + // 1 + 01 00 00 00, 0, 1 + 00 00 00 00 + EXPECT_PACKET_EQ(FromHex("80 80 00 00 20 00 00 00 00", 67), Payload(response)); + + GameMessages::SetInventorySize size; + size.inventoryType = eInventoryType::ITEMS; + size.size = 20; + EXPECT_PACKET_EQ(FromHex("00 00 00 00 14 00 00 00"), Payload(size)); +} diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/InventoryMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/InventoryMessagesLegacy.h new file mode 100644 index 000000000..4ea461aed --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/InventoryMessagesLegacy.h @@ -0,0 +1,464 @@ +#ifndef INVENTORYMESSAGESLEGACY_H +#define INVENTORYMESSAGESLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written GameMessages functions that InventoryMessages.h replaced +// (dGame/dGameMessages/GameMessages.cpp, branched from origin/main 129199e4). Only the namespace changed. +// The Read* functions are the read sequences of the replaced GameMessages::Handle* functions. +// SendMoveInventoryBatch is not here: nothing called it, and MoveInventoryBatch follows the client's layout. + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "eInventoryType.h" +#include "eLootSourceType.h" +#include "eReponseMoveItemBetweenInventoryTypeCode.h" +#include "eUnequippableActiveType.h" +#include "eUseItemResponse.h" +#include "Entity.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "Inventory.h" +#include "Item.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "ServiceType.h" + +#include +#include + +namespace LegacyGameMessages { + inline void SendAddItemToInventoryClientSync(Entity* entity, const SystemAddress& sysAddr, Item* item, const LWOOBJID& objectID, bool showFlyingLoot, int itemCount, LWOOBJID subKey, eLootSourceType lootSourceType) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::ADD_ITEM_TO_INVENTORY_CLIENT_SYNC); + bitStream.Write(item->GetBound()); + bitStream.Write(item->GetInfo().isBOE); + bitStream.Write(item->GetInfo().isBOP); + + bitStream.Write(lootSourceType != eLootSourceType::NONE); // Loot source + if (lootSourceType != eLootSourceType::NONE) bitStream.Write(lootSourceType); + std::u16string extraInfo; + + const auto& config = item->GetConfig(); + + for (const auto& data : config.values | std::views::values) { + extraInfo += GeneralUtils::ASCIIToUTF16(data->GetString()) + u","; + } + + if (extraInfo.length() > 0) extraInfo.pop_back(); // remove the last comma + + bitStream.Write(extraInfo.size()); + if (extraInfo.size() > 0) { + for (uint32_t i = 0; i < extraInfo.size(); ++i) { + bitStream.Write(extraInfo[i]); + } + bitStream.Write(0x00); + } + + bitStream.Write(item->GetLot()); + + bitStream.Write(subKey != LWOOBJID_EMPTY); + if (subKey != LWOOBJID_EMPTY) bitStream.Write(subKey); + + auto* inventory = item->GetInventory(); + const auto inventoryType = inventory->GetType(); + + bitStream.Write(inventoryType != eInventoryType::ITEMS); + if (inventoryType != eInventoryType::ITEMS) bitStream.Write(inventoryType); + + bitStream.Write(itemCount != 1); + if (itemCount != 1) bitStream.Write(itemCount); + + const auto count = item->GetCount(); + + bitStream.Write(count != 0); //items total + if (count != 0) bitStream.Write(count); + + bitStream.Write(objectID); + bitStream.Write(0.0f); + bitStream.Write(0.0f); + bitStream.Write(0.0f); + bitStream.Write(showFlyingLoot); + bitStream.Write(item->GetSlot()); + + SEND_PACKET; + } + + inline void SendSetInventorySize(Entity* entity, int invType, int size) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::SET_INVENTORY_SIZE); + bitStream.Write(invType); + bitStream.Write(size); + + SystemAddress sysAddr = entity->GetSystemAddress(); + SEND_PACKET; + } + + inline void SendRemoveItemFromInventory(Entity* entity, const SystemAddress& sysAddr, LWOOBJID objectID, LOT templateID, int inventoryType, uint32_t stackCount, uint32_t stackRemaining) { + CBITSTREAM; + CMSGHEADER; + // this is used for a lot more than just inventory trashing (trades, vendors, etc.) but for now since it's just used for that, that's all im going to implement + bool bConfirmed = true; + bool bDeleteItem = true; + bool bOutSuccess = false; + int eInvType = inventoryType; + int eLootTypeSource = LOOTTYPE_NONE; + bool forceDeletion = true; + LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; + LWOOBJID iObjID = objectID; + LOT iObjTemplate = templateID; + LWOOBJID iRequestingObjID = LWOOBJID_EMPTY; + uint32_t iStackCount = stackCount; + uint32_t iStackRemaining = stackRemaining; + LWOOBJID iSubkey = LWOOBJID_EMPTY; + LWOOBJID iTradeID = LWOOBJID_EMPTY; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::REMOVE_ITEM_FROM_INVENTORY); + bitStream.Write(bConfirmed); + bitStream.Write(bDeleteItem); + bitStream.Write(bOutSuccess); + bitStream.Write1(); + bitStream.Write(eInvType); + bitStream.Write1(); + bitStream.Write(eLootTypeSource); + bitStream.Write(0); //extra info + //bitStream.Write(0); //extra info + bitStream.Write(forceDeletion); + bitStream.Write0(); + bitStream.Write1(); + bitStream.Write(iObjID); + bitStream.Write1(); + bitStream.Write(iObjTemplate); + bitStream.Write0(); + bitStream.Write1(); + bitStream.Write(iStackCount); + bitStream.Write1(); + bitStream.Write(iStackRemaining); + bitStream.Write0(); + bitStream.Write0(); + + SEND_PACKET; + } + + inline void SendConsumeClientItem(Entity* entity, bool bSuccess, LWOOBJID item) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::CONSUME_CLIENT_ITEM); + bitStream.Write(bSuccess); + bitStream.Write(item); + + SystemAddress sysAddr = entity->GetSystemAddress(); + SEND_PACKET; + } + + inline void SendUseItemResult(Entity* entity, LOT templateID, bool useItemResult) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::USE_ITEM_RESULT); + bitStream.Write(templateID); + bitStream.Write(useItemResult); + + SystemAddress sysAddr = entity->GetSystemAddress(); + SEND_PACKET; + } + + inline void SendUseItemRequirementsResponse(LWOOBJID objectID, const SystemAddress& sysAddr, eUseItemResponse itemResponse) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectID); + bitStream.Write(MessageType::Game::USE_ITEM_REQUIREMENTS_RESPONSE); + + bitStream.Write(itemResponse); + + SEND_PACKET; + } + + inline void SendResponseMoveItemBetweenInventoryTypes(LWOOBJID objectId, const SystemAddress& sysAddr, eInventoryType inventoryTypeDestination, eInventoryType inventoryTypeSource, eReponseMoveItemBetweenInventoryTypeCode response) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::RESPONSE_MOVE_ITEM_BETWEEN_INVENTORY_TYPES); + + bitStream.Write(inventoryTypeDestination != eInventoryType::ITEMS); + if (inventoryTypeDestination != eInventoryType::ITEMS) bitStream.Write(inventoryTypeDestination); + + bitStream.Write(inventoryTypeSource != eInventoryType::ITEMS); + if (inventoryTypeSource != eInventoryType::ITEMS) bitStream.Write(inventoryTypeSource); + + bitStream.Write(response != eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC); + if (response != eReponseMoveItemBetweenInventoryTypeCode::FAIL_GENERIC) bitStream.Write(response); + + SEND_PACKET; + } + + inline void SendNotifyNotEnoughInvSpace(LWOOBJID objectId, uint32_t freeSlotsNeeded, eInventoryType inventoryType, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::VEHICLE_NOTIFY_FINISHED_RACE); + + bitStream.Write(freeSlotsNeeded); + bitStream.Write(inventoryType != 0); + if (inventoryType != 0) bitStream.Write(inventoryType); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendUpdateInventoryUi(LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::UPDATE_INVENTORY_UI); + + SEND_PACKET; + } + + + inline void SendMarkInventoryItemAsActive(LWOOBJID objectId, bool bActive, eUnequippableActiveType iType, LWOOBJID itemID, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::MARK_INVENTORY_ITEM_AS_ACTIVE); + + bitStream.Write(bActive); + + bitStream.Write(iType != eUnequippableActiveType::INVALID); + if (iType != eUnequippableActiveType::INVALID) bitStream.Write(iType); + + bitStream.Write(itemID != LWOOBJID_EMPTY); + if (itemID != LWOOBJID_EMPTY) bitStream.Write(itemID); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + + struct LegacyEquip { bool immediate{}; LWOOBJID objectID{}; }; + inline LegacyEquip ReadEquipItem(RakNet::BitStream& inStream) { + bool immediate; + LWOOBJID objectID; + inStream.Read(immediate); + inStream.Read(immediate); //twice? + inStream.Read(objectID); + return { immediate, objectID }; + } + + inline LegacyEquip ReadUnequipItem(RakNet::BitStream& inStream) { + bool immediate; + LWOOBJID objectID; + inStream.Read(immediate); + inStream.Read(immediate); + inStream.Read(immediate); + inStream.Read(objectID); + return { immediate, objectID }; + } + + struct LegacyRemoveItem { + bool bConfirmed; bool bDeleteItem; bool bOutSuccess; int eInvType; int eLootTypeSource; std::u16string extraInfo; bool forceDeletion; + LWOOBJID iLootTypeSource; LWOOBJID iObjID; LOT iObjTemplate; LWOOBJID iRequestingObjID; uint32_t iStackCount; uint32_t iStackRemaining; LWOOBJID iSubkey; LWOOBJID iTradeID; + }; + inline LegacyRemoveItem ReadRemoveItemFromInventory(RakNet::BitStream& inStream) { + bool bConfirmed = false; + bool bDeleteItem = true; + bool bOutSuccess = false; + bool eInvTypeIsDefault = false; + int eInvType = INVENTORY_MAX; + bool eLootTypeSourceIsDefault = false; + int eLootTypeSource = LOOTTYPE_NONE; + int32_t extraInfoLength = 0; + std::u16string extraInfo; + bool forceDeletion = true; + bool iLootTypeSourceIsDefault = false; + LWOOBJID iLootTypeSource = LWOOBJID_EMPTY; + bool iObjIDIsDefault = false; + LWOOBJID iObjID = LWOOBJID_EMPTY; + bool iObjTemplateIsDefault = false; + LOT iObjTemplate = LOT_NULL; + bool iRequestingObjIDIsDefault = false; + LWOOBJID iRequestingObjID = LWOOBJID_EMPTY; + bool iStackCountIsDefault = false; + uint32_t iStackCount = 1; + bool iStackRemainingIsDefault = false; + uint32_t iStackRemaining = 0; + bool iSubkeyIsDefault = false; + LWOOBJID iSubkey = LWOOBJID_EMPTY; + bool iTradeIDIsDefault = false; + LWOOBJID iTradeID = LWOOBJID_EMPTY; + + inStream.Read(bConfirmed); + inStream.Read(bDeleteItem); + inStream.Read(bOutSuccess); + inStream.Read(eInvTypeIsDefault); + if (eInvTypeIsDefault) inStream.Read(eInvType); + inStream.Read(eLootTypeSourceIsDefault); + if (eLootTypeSourceIsDefault) inStream.Read(eLootTypeSource); + inStream.Read(extraInfoLength); + if (extraInfoLength > 0) { + for (uint32_t i = 0; i < extraInfoLength; ++i) { + uint16_t character; + inStream.Read(character); + extraInfo.push_back(character); + } + uint16_t nullTerm; + inStream.Read(nullTerm); + } + inStream.Read(forceDeletion); + inStream.Read(iLootTypeSourceIsDefault); + if (iLootTypeSourceIsDefault) inStream.Read(iLootTypeSource); + inStream.Read(iObjIDIsDefault); + if (iObjIDIsDefault) inStream.Read(iObjID); + inStream.Read(iObjTemplateIsDefault); + if (iObjTemplateIsDefault) inStream.Read(iObjTemplate); + inStream.Read(iRequestingObjIDIsDefault); + if (iRequestingObjIDIsDefault) inStream.Read(iRequestingObjID); + inStream.Read(iStackCountIsDefault); + if (iStackCountIsDefault) inStream.Read(iStackCount); + inStream.Read(iStackRemainingIsDefault); + if (iStackRemainingIsDefault) inStream.Read(iStackRemaining); + inStream.Read(iSubkeyIsDefault); + if (iSubkeyIsDefault) inStream.Read(iSubkey); + inStream.Read(iTradeIDIsDefault); + if (iTradeIDIsDefault) inStream.Read(iTradeID); + return { bConfirmed, bDeleteItem, bOutSuccess, eInvType, eLootTypeSource, extraInfo, forceDeletion, iLootTypeSource, iObjID, iObjTemplate, iRequestingObjID, iStackCount, iStackRemaining, iSubkey, iTradeID }; + } + + struct LegacyMoveItemInInventory { int32_t destInvType; LWOOBJID iObjID; int inventoryType; int responseCode; int slot; }; + inline LegacyMoveItemInInventory ReadMoveItemInInventory(RakNet::BitStream& inStream) { + bool destInvTypeIsDefault = false; + int32_t destInvType = eInventoryType::INVALID; + LWOOBJID iObjID; + int inventoryType; + int responseCode; + int slot; + inStream.Read(destInvTypeIsDefault); + if (destInvTypeIsDefault) { inStream.Read(destInvType); } + inStream.Read(iObjID); + inStream.Read(inventoryType); + inStream.Read(responseCode); + inStream.Read(slot); + return { destInvType, iObjID, inventoryType, responseCode, slot }; + } + + struct LegacyMoveItemBetweenInventoryTypes { eInventoryType inventoryTypeA; eInventoryType inventoryTypeB; LWOOBJID objectID; bool showFlyingLoot; uint32_t stackCount; LOT templateID; }; + inline LegacyMoveItemBetweenInventoryTypes ReadMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream) { + eInventoryType inventoryTypeA; + eInventoryType inventoryTypeB; + LWOOBJID objectID; + bool showFlyingLoot = true; + bool stackCountIsDefault = false; + uint32_t stackCount = 1; + bool templateIDIsDefault = false; + LOT templateID = LOT_NULL; + + inStream.Read(inventoryTypeA); + inStream.Read(inventoryTypeB); + inStream.Read(objectID); + inStream.Read(showFlyingLoot); + inStream.Read(stackCountIsDefault); + if (stackCountIsDefault) inStream.Read(stackCount); + inStream.Read(templateIDIsDefault); + if (templateIDIsDefault) inStream.Read(templateID); + return { inventoryTypeA, inventoryTypeB, objectID, showFlyingLoot, stackCount, templateID }; + } + + struct LegacyRequestMove { bool bAllowPartial; int32_t destSlot; int32_t iStackCount; eInventoryType invTypeDst; eInventoryType invTypeSrc; LWOOBJID itemID; bool showFlyingLoot; LWOOBJID subkey; LOT itemLOT; }; + inline LegacyRequestMove ReadRequestMoveItemBetweenInventoryTypes(RakNet::BitStream& inStream) { + bool bAllowPartial{}; + int32_t destSlot = -1; + int32_t iStackCount = 1; + eInventoryType invTypeDst = ITEMS; + eInventoryType invTypeSrc = ITEMS; + LWOOBJID itemID = LWOOBJID_EMPTY; + bool showFlyingLoot{}; + LWOOBJID subkey = LWOOBJID_EMPTY; + LOT itemLOT = 0; + + bAllowPartial = inStream.ReadBit(); + if (inStream.ReadBit()) inStream.Read(destSlot); + if (inStream.ReadBit()) inStream.Read(iStackCount); + if (inStream.ReadBit()) inStream.Read(invTypeDst); + if (inStream.ReadBit()) inStream.Read(invTypeSrc); + if (inStream.ReadBit()) inStream.Read(itemID); + showFlyingLoot = inStream.ReadBit(); + if (inStream.ReadBit()) inStream.Read(subkey); + if (inStream.ReadBit()) inStream.Read(itemLOT); + return { bAllowPartial, destSlot, iStackCount, invTypeDst, invTypeSrc, itemID, showFlyingLoot, subkey, itemLOT }; + } + + // HandleClientItemConsumed, HandleUseNonEquipmentItem and HandleSetConsumableItem each read one value. + template + inline T ReadSingle(RakNet::BitStream& inStream) { + T value; + inStream.Read(value); + return value; + } + + // The two group handlers read into an InventoryComponent::GroupUpdate; its fields are locals here. + struct LegacyGroupUpdate { bool ok; std::string action; std::string groupId; std::u16string groupName; eInventoryType inventory; bool locked; LOT lot; }; + inline LegacyGroupUpdate ReadUpdateInventoryGroup(RakNet::BitStream& inStream) { + std::string action; + std::u16string groupName; + std::string groupId; + eInventoryType inventory{}; + bool locked{}; // All groups are locked by default + + uint32_t size{}; + if (!inStream.Read(size)) return {}; + if (size > MAX_MESSAGE_LENGTH) return {}; // Bounds check before resize + action.resize(size); + if (!inStream.Read(action.data(), size)) return {}; + + if (!inStream.Read(size)) return {}; + if (size > MAX_MESSAGE_LENGTH) return {}; // Bounds check before resize + groupId.resize(size); + if (!inStream.Read(groupId.data(), size)) return {}; + + if (!inStream.Read(size)) return {}; + if (size > MAX_MESSAGE_LENGTH / 2) return {}; // Bounds check: size * 2 would overflow or exceed limit + groupName.resize(size); + if (!inStream.Read(reinterpret_cast(groupName.data()), size * 2)) return {}; + + if (!inStream.Read(inventory)) return {}; + if (!inStream.Read(locked)) return {}; + return { true, action, groupId, groupName, inventory, locked, 0 }; + } + + inline LegacyGroupUpdate ReadUpdateInventoryGroupContents(RakNet::BitStream& inStream) { + std::string action; + std::string groupId; + eInventoryType inventory{}; + LOT lot{}; + + uint32_t size{}; + if (!inStream.Read(size)) return {}; + action.resize(size); + if (!inStream.Read(action.data(), size)) return {}; + + if (!inStream.Read(size)) return {}; + groupId.resize(size); + if (!inStream.Read(groupId.data(), size)) return {}; + + if (!inStream.Read(inventory)) return {}; + if (!inStream.Read(lot)) return {}; + return { true, action, groupId, u"", inventory, false, lot }; + } +} + +#endif // INVENTORYMESSAGESLEGACY_H