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