diff --git a/dGame/TradingManager.cpp b/dGame/TradingManager.cpp index 24b081f59..37885dbc4 100644 --- a/dGame/TradingManager.cpp +++ b/dGame/TradingManager.cpp @@ -3,6 +3,7 @@ #include "dServer.h" #include "EntityManager.h" #include "GameMessages.h" +#include "TradeMessages.h" #include "InventoryComponent.h" #include "ObjectIDManager.h" #include "Game.h" @@ -79,7 +80,10 @@ void Trade::SetAccepted(LWOOBJID participant, bool value) { auto* entityB = GetParticipantBEntity(); if (entityB != nullptr) { - GameMessages::SendServerTradeAccept(m_ParticipantB, value, entityB->GetSystemAddress()); + GameMessages::ServerTradeAccept accept; + accept.target = m_ParticipantB; + accept.bFirst = value; + accept.Send(entityB->GetSystemAddress()); } } else if (participant == m_ParticipantB) { m_AcceptedB = !value; @@ -89,7 +93,10 @@ void Trade::SetAccepted(LWOOBJID participant, bool value) { auto* entityA = GetParticipantAEntity(); if (entityA != nullptr) { - GameMessages::SendServerTradeAccept(m_ParticipantA, value, entityA->GetSystemAddress()); + GameMessages::ServerTradeAccept accept; + accept.target = m_ParticipantA; + accept.bFirst = value; + accept.Send(entityA->GetSystemAddress()); } } @@ -97,7 +104,10 @@ void Trade::SetAccepted(LWOOBJID participant, bool value) { auto* entityB = GetParticipantBEntity(); if (entityB != nullptr) { - GameMessages::SendServerTradeAccept(m_ParticipantB, false, entityB->GetSystemAddress()); + GameMessages::ServerTradeAccept accept; + accept.target = m_ParticipantB; + accept.bFirst = false; + accept.Send(entityB->GetSystemAddress()); } else { return; } @@ -105,7 +115,10 @@ void Trade::SetAccepted(LWOOBJID participant, bool value) { auto* entityA = GetParticipantAEntity(); if (entityA != nullptr) { - GameMessages::SendServerTradeAccept(m_ParticipantA, false, entityA->GetSystemAddress()); + GameMessages::ServerTradeAccept accept; + accept.target = m_ParticipantA; + accept.bFirst = false; + accept.Send(entityA->GetSystemAddress()); } else { return; } @@ -214,8 +227,16 @@ void Trade::Cancel(const LWOOBJID canceller) { if (entityA == nullptr || entityB == nullptr) return; - if (entityA->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) GameMessages::SendServerTradeCancel(entityA->GetObjectID(), entityA->GetSystemAddress()); - if (entityB->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) GameMessages::SendServerTradeCancel(entityB->GetObjectID(), entityB->GetSystemAddress()); + if (entityA->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) { + GameMessages::ServerTradeCancel cancel; + cancel.target = entityA->GetObjectID(); + cancel.Send(entityA->GetSystemAddress()); + } + if (entityB->GetObjectID() != canceller || canceller == LWOOBJID_EMPTY) { + GameMessages::ServerTradeCancel cancel; + cancel.target = entityB->GetObjectID(); + cancel.Send(entityB->GetSystemAddress()); + } } void Trade::SendUpdateToOther(LWOOBJID participant) { @@ -260,7 +281,18 @@ void Trade::SendUpdateToOther(LWOOBJID participant) { LOG("Sending trade update"); - GameMessages::SendServerTradeUpdate(other->GetObjectID(), coins, items, other->GetSystemAddress()); + GameMessages::ServerTradeUpdate update; + update.target = other->GetObjectID(); + update.i64Currency = coins; + for (const auto& item : items) { + GameMessages::TradeItemEntry entry; + entry.key = item.itemId; + entry.itemID = item.itemId; + entry.templateID = item.itemLot; + entry.count = item.itemCount; // the count is always written, even when it is 1 + update.inventoryMap.push_back(entry); + } + update.Send(other->GetSystemAddress()); } const std::unique_ptr& TradingManager::GetTrade(LWOOBJID tradeId) const { diff --git a/dGame/dComponents/AchievementVendorComponent.cpp b/dGame/dComponents/AchievementVendorComponent.cpp index 950d178da..ab1faa4fa 100644 --- a/dGame/dComponents/AchievementVendorComponent.cpp +++ b/dGame/dComponents/AchievementVendorComponent.cpp @@ -43,13 +43,13 @@ void AchievementVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { if (costLOT == -1 || !SellsItem(buyer, lot)) { auto* user = UserManager::Instance()->GetUser(buyer->GetSystemAddress()); CheatDetection::ReportCheat(user, buyer->GetSystemAddress(), "Attempted to buy item %i from achievement vendor %i that is not purchasable", lot, m_Parent->GetLOT()); - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } auto* inventoryComponent = buyer->GetComponent(); if (!inventoryComponent) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } @@ -65,12 +65,12 @@ void AchievementVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { const uint32_t altCurrencyCost = itemComp.commendationCost * count; if (inventoryComponent->GetLotCount(costLOT) < altCurrencyCost || !inventoryComponent->RemoveItem(costLOT, altCurrencyCost, eInventoryType::ALL)) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR); - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS); } diff --git a/dGame/dComponents/DonationVendorComponent.cpp b/dGame/dComponents/DonationVendorComponent.cpp index 9436631e4..323db8859 100644 --- a/dGame/dComponents/DonationVendorComponent.cpp +++ b/dGame/dComponents/DonationVendorComponent.cpp @@ -1,5 +1,18 @@ #include "DonationVendorComponent.h" #include "Database.h" +#include "CharacterComponent.h" +#include "EconomyLedger.h" +#include "eInventoryType.h" +#include "eMissionTaskType.h" +#include "EntityManager.h" +#include "Game.h" +#include "InventoryComponent.h" +#include "Item.h" +#include "LeaderboardManager.h" +#include "Logger.h" +#include "MissionComponent.h" +#include "User.h" +#include "UserManager.h" DonationVendorComponent::DonationVendorComponent(Entity* parent, const int32_t componentID) : VendorComponent(parent, componentID) { //LoadConfigData @@ -46,3 +59,49 @@ void DonationVendorComponent::Serialize(RakNet::BitStream& outBitStream, bool bI if (!bIsInitialUpdate) m_DirtyDonationVendor = false; } } + +void DonationVendorComponent::AddDonationItem(const SystemAddress& sysAddr, LWOOBJID itemObjID, uint32_t count) { + if (GetActivityID() == 0) { + LOG("WARNING: Trying to dontate to a vendor with no activity"); + return; + } + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return; + Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); + if (!player) return; + auto* characterComponent = player->GetComponent(); + if (!characterComponent) return; + auto* inventoryComponent = player->GetComponent(); + if (!inventoryComponent) return; + Item* item = inventoryComponent->FindItemById(itemObjID); + if (!item) return; + if (item->GetCount() < count) return; + characterComponent->SetCurrentInteracting(m_Parent->GetObjectID()); + inventoryComponent->MoveItemToInventory(item, eInventoryType::DONATION, count, true, false, true); +} + +void DonationVendorComponent::ConfirmDonation(Entity& player) { + const auto [inventoryComponent, missionComponent, characterComponent] = player.GetComponentsMut(); + if (!inventoryComponent || !missionComponent || !characterComponent || !characterComponent->GetCurrentInteracting()) return; + + if (GetActivityID() == 0) { + LOG("WARNING: Trying to dontate to a vendor with no activity"); + return; + } + auto* inventory = inventoryComponent->GetInventory(eInventoryType::DONATION); + if (!inventory) return; + auto items = inventory->GetItems(); + if (!items.empty()) { + uint32_t count = 0; + for (auto& [itemID, item] : items) { + count += item->GetCount(); + EconomyLedger::RecordItemsUnsuppressed(&player, item->GetLot(), -static_cast(item->GetCount()), EconomyLedger::DONATION_SOURCE); + item->RemoveFromInventory(); + } + missionComponent->Progress(eMissionTaskType::DONATION, 0, LWOOBJID_EMPTY, "", count); + LeaderboardManager::SaveScore(player.GetObjectID(), GetActivityID(), count); + SubmitDonation(count); + Game::entityManager->SerializeEntity(m_Parent); + } + characterComponent->SetCurrentInteracting(LWOOBJID_EMPTY); +} diff --git a/dGame/dComponents/DonationVendorComponent.h b/dGame/dComponents/DonationVendorComponent.h index 5fef2e419..c2979a2a4 100644 --- a/dGame/dComponents/DonationVendorComponent.h +++ b/dGame/dComponents/DonationVendorComponent.h @@ -14,6 +14,12 @@ public: uint32_t GetActivityID() {return m_ActivityId;}; void SubmitDonation(uint32_t count); + // AddDonationItem from the player at sysAddr: moves count of the item into their donation inventory. + void AddDonationItem(const SystemAddress& sysAddr, LWOOBJID itemObjID, uint32_t count); + + // ConfirmDonationOnPlayer: the player donates everything in their donation inventory to this vendor. + void ConfirmDonation(Entity& player); + private: bool m_DirtyDonationVendor = false; float m_PercentComplete = 0.0; diff --git a/dGame/dComponents/VendorComponent.cpp b/dGame/dComponents/VendorComponent.cpp index 55808e73f..44496c1a6 100644 --- a/dGame/dComponents/VendorComponent.cpp +++ b/dGame/dComponents/VendorComponent.cpp @@ -13,6 +13,8 @@ #include "eVendorTransactionResult.h" #include "UserManager.h" #include "CheatDetection.h" +#include "VendorMessages.h" +#include "Item.h" VendorComponent::VendorComponent(Entity* parent, const int32_t componentID) : Component(parent, componentID) { m_HasStandardCostItems = false; @@ -31,8 +33,13 @@ void VendorComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitial } void VendorComponent::OnUse(Entity* originator) { - GameMessages::SendVendorOpenWindow(m_Parent, originator->GetSystemAddress()); - GameMessages::SendVendorStatusUpdate(m_Parent, originator->GetSystemAddress()); + GameMessages::VendorOpenWindow openWindow; + openWindow.target = m_Parent->GetObjectID(); + openWindow.SendToClient(originator->GetSystemAddress()); + + // Only the entity's VENDOR component sends its stock (achievement and donation vendors never did). + auto* vendor = static_cast(m_Parent->GetComponent(eReplicaComponentType::VENDOR)); + if (vendor) vendor->SendStatusUpdate(originator->GetSystemAddress()); } void VendorComponent::RefreshInventory(bool isCreation) { @@ -90,7 +97,9 @@ void VendorComponent::RefreshInventory(bool isCreation) { }); } Game::entityManager->SerializeEntity(m_Parent); - GameMessages::SendVendorStatusUpdate(m_Parent, UNASSIGNED_SYSTEM_ADDRESS); + // Only the entity's VENDOR component sends its stock (achievement and donation vendors never did). + auto* vendor = static_cast(m_Parent->GetComponent(eReplicaComponentType::VENDOR)); + if (vendor) vendor->SendStatusUpdate(UNASSIGNED_SYSTEM_ADDRESS); } void VendorComponent::SetupConstants() { @@ -142,13 +151,13 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { if (!SellsItem(lot)) { auto* user = UserManager::Instance()->GetUser(buyer->GetSystemAddress()); CheatDetection::ReportCheat(user, buyer->GetSystemAddress(), "Attempted to buy item %i from achievement vendor %i that is not purchasable", lot, m_Parent->GetLOT()); - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } auto* inventoryComponent = buyer->GetComponent(); if (!inventoryComponent) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); @@ -160,7 +169,7 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { auto craftingCurrencies = CDItemComponentTable::ParseCraftingCurrencies(itemComp); for (const auto& [crafintCurrencyLOT, crafintCurrencyCount]: craftingCurrencies) { if (inventoryComponent->GetLotCount(crafintCurrencyLOT) < (crafintCurrencyCount * count)) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } } @@ -172,7 +181,7 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { } if (!success) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } @@ -181,14 +190,14 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { Character* character = buyer->GetCharacter(); if (!character || character->GetCoins() < coinCost) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } if (Inventory::IsValidItem(itemComp.currencyLOT)) { const uint32_t altCurrencyCost = std::floor(itemComp.altCurrencyCost * buyScalar) * count; if (inventoryComponent->GetLotCount(itemComp.currencyLOT) < altCurrencyCost) { - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL); return; } inventoryComponent->RemoveItem(itemComp.currencyLOT, altCurrencyCost, eInventoryType::ALL); @@ -196,7 +205,7 @@ void VendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) { character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR); inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR); - GameMessages::SendVendorTransactionResult(buyer, buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS); + SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS); } bool VendorComponent::SetupItem(LOT item) { @@ -219,3 +228,90 @@ bool VendorComponent::SetupItem(LOT item) { return true; } + +void VendorComponent::SendStatusUpdate(const SystemAddress& sysAddr, bool bUpdateOnly) { + GameMessages::VendorStatusUpdate statusUpdate; + statusUpdate.target = m_Parent->GetObjectID(); + statusUpdate.bUpdateOnly = bUpdateOnly; + statusUpdate.inventoryList.reserve(m_Inventory.size()); + for (const auto& item : m_Inventory) statusUpdate.inventoryList.push_back({ item.lot, item.sortPriority }); + statusUpdate.Send(sysAddr); +} + +void VendorComponent::SendTransactionResult(LWOOBJID target, const SystemAddress& sysAddr, eVendorTransactionResult result) { + GameMessages::VendorTransactionResult transactionResult; + transactionResult.target = target; + transactionResult.iResult = result; + transactionResult.SendToClient(sysAddr); +} + +void VendorComponent::SellToVendor(Entity& player, const SystemAddress& sysAddr, LWOOBJID itemObjID, int32_t count) { + Character* character = player.GetCharacter(); + if (!character) return; + InventoryComponent* inv = static_cast(player.GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + Item* item = inv->FindItemById(itemObjID); + if (!item) return; + + CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); + CDItemComponentTable* itemComponentTable = CDClientManager::GetTable(); + + int itemCompID = compRegistryTable->GetByIDAndType(item->GetLot(), eReplicaComponentType::ITEM); + CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID); + + // Items with a base value of 0 or max int are special items that should not be sold if they're not sub items + if (itemComp.baseValue == 0 || itemComp.baseValue == UINT_MAX) { + SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::SELL_FAIL); + return; + } + + float sellScalar = GetSellScalar(); + if (Inventory::IsValidItem(itemComp.currencyLOT)) { + const auto altCurrency = static_cast(itemComp.altCurrencyCost * sellScalar) * count; + inv->AddItem(itemComp.currencyLOT, std::floor(altCurrency), eLootSourceType::VENDOR); // Return alt currencies like faction tokens. + } + + inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true); + character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR); + SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::SELL_SUCCESS); +} + +void VendorComponent::BuybackFromVendor(Entity& player, const SystemAddress& sysAddr, LWOOBJID itemObjID, int32_t count) { + Character* character = player.GetCharacter(); + if (!character) return; + InventoryComponent* inv = static_cast(player.GetComponent(eReplicaComponentType::INVENTORY)); + if (!inv) return; + + Item* item = inv->FindItemById(itemObjID); + if (!item) return; + + CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); + CDItemComponentTable* itemComponentTable = CDClientManager::GetTable(); + + int itemCompID = compRegistryTable->GetByIDAndType(item->GetLot(), eReplicaComponentType::ITEM); + CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID); + + float sellScalar = GetSellScalar(); + + const auto cost = static_cast(std::floor(((itemComp.baseValue * sellScalar) * count))); + + if (character->GetCoins() < cost) { + SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::PURCHASE_FAIL); + return; + } + + if (Inventory::IsValidItem(itemComp.currencyLOT)) { + const uint32_t altCurrencyCost = std::floor(itemComp.altCurrencyCost * sellScalar) * count; + if (inv->GetLotCount(itemComp.currencyLOT) < altCurrencyCost || !inv->RemoveItem(itemComp.currencyLOT, altCurrencyCost, eInventoryType::ALL)) { + SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::PURCHASE_FAIL); + return; + } + } + + //inv->RemoveItem(count, -1, iObjID); + inv->MoveItemToInventory(item, Inventory::FindInventoryTypeForLot(item->GetLot()), count, true, false); + character->SetCoins(character->GetCoins() - cost, eLootSourceType::VENDOR); + //Game::entityManager->SerializeEntity(player); // so inventory updates + SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::PURCHASE_SUCCESS); +} diff --git a/dGame/dComponents/VendorComponent.h b/dGame/dComponents/VendorComponent.h index 733ab3f04..f2d271c09 100644 --- a/dGame/dComponents/VendorComponent.h +++ b/dGame/dComponents/VendorComponent.h @@ -9,6 +9,8 @@ #include "RakNetTypes.h" #include "eReplicaComponentType.h" +enum class eVendorTransactionResult : uint32_t; + struct SoldItem { SoldItem(const LOT lot, const int32_t sortPriority) { this->lot = lot; @@ -49,6 +51,16 @@ public: void Buy(Entity* buyer, LOT lot, uint32_t count); + // SellToVendor / BuybackFromVendor from player (sent from sysAddr), for the item with object ID itemObjID. + void SellToVendor(Entity& player, const SystemAddress& sysAddr, LWOOBJID itemObjID, int32_t count); + void BuybackFromVendor(Entity& player, const SystemAddress& sysAddr, LWOOBJID itemObjID, int32_t count); + + // Sends this vendor's stock (VendorStatusUpdate) to sysAddr; UNASSIGNED_SYSTEM_ADDRESS broadcasts. + void SendStatusUpdate(const SystemAddress& sysAddr, bool bUpdateOnly = false); + + // Sends a VendorTransactionResult targeting target to sysAddr only. + static void SendTransactionResult(LWOOBJID target, const SystemAddress& sysAddr, eVendorTransactionResult result); + private: void HandleMrReeCameras(); bool SetupItem(LOT item); diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index 9988cfb47..23a4c08e0 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -1,15 +1,17 @@ set(DGAME_DGAMEMESSAGES_SOURCES "ActivityMessages.cpp" "BuildingMessages.cpp" + "EffectsMessages.cpp" "GameMessageDecoder.cpp" "GameMessageHandler.cpp" - "EffectsMessages.cpp" "GameMessages.cpp" "InventoryMessages.cpp" "MissionMessages.cpp" "PetMessages.cpp" "PropertyMessages.cpp" - "RacingMessages.cpp") + "RacingMessages.cpp" + "TradeMessages.cpp" + "VendorMessages.cpp") add_library(dGameMessages OBJECT ${DGAME_DGAMEMESSAGES_SOURCES}) target_link_libraries(dGameMessages diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 5fa6acc67..77bf73da1 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -35,6 +35,8 @@ #include "BuildingMessages.h" #include "RacingMessages.h" #include "MissionMessages.h" +#include "TradeMessages.h" +#include "VendorMessages.h" #include "EffectsMessages.h" #include "InventoryMessages.h" #include "PetMessages.h" @@ -139,6 +141,22 @@ namespace { { UN_USE_BBB_MODEL, []() { return std::make_unique(); } }, { BBB_LOAD_ITEM_REQUEST, []() { return std::make_unique(); } }, { BBB_SAVE_REQUEST, []() { return std::make_unique(); } }, + + // Vendors and donation vendors + { REQUEST_VENDOR_STATUS_UPDATE, []() { return std::make_unique(); } }, + { BUY_FROM_VENDOR, []() { return std::make_unique(); } }, + { SELL_TO_VENDOR, []() { return std::make_unique(); } }, + { BUYBACK_FROM_VENDOR, []() { return std::make_unique(); } }, + { ADD_DONATION_ITEM, []() { return std::make_unique(); } }, + { REMOVE_DONATION_ITEM, []() { return std::make_unique(); } }, + { CONFIRM_DONATION_ON_PLAYER, []() { return std::make_unique(); } }, + { CANCEL_DONATION_ON_PLAYER, []() { return std::make_unique(); } }, + + // Trading + { CLIENT_TRADE_REQUEST, []() { return std::make_unique(); } }, + { CLIENT_TRADE_CANCEL, []() { return std::make_unique(); } }, + { CLIENT_TRADE_ACCEPT, []() { return std::make_unique(); } }, + { CLIENT_TRADE_UPDATE, []() { return std::make_unique(); } }, }; }; @@ -464,18 +482,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System entity->Smash(entity->GetObjectID()); break; - case MessageType::Game::BUY_FROM_VENDOR: - GameMessages::HandleBuyFromVendor(inStream, entity, sysAddr); - break; - - case MessageType::Game::SELL_TO_VENDOR: - GameMessages::HandleSellToVendor(inStream, entity, sysAddr); - break; - - case MessageType::Game::BUYBACK_FROM_VENDOR: - GameMessages::HandleBuybackFromVendor(inStream, entity, sysAddr); - break; - case MessageType::Game::REBUILD_CANCEL: GameMessages::HandleQuickBuildCancel(inStream, entity); break; @@ -489,20 +495,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System break; // Trading - case MessageType::Game::CLIENT_TRADE_REQUEST: - GameMessages::HandleClientTradeRequest(inStream, entity, sysAddr); - break; - case MessageType::Game::CLIENT_TRADE_CANCEL: - GameMessages::HandleClientTradeCancel(inStream, entity, sysAddr); - break; - case MessageType::Game::CLIENT_TRADE_ACCEPT: - GameMessages::HandleClientTradeAccept(inStream, entity, sysAddr); - break; - case MessageType::Game::CLIENT_TRADE_UPDATE: - GameMessages::HandleClientTradeUpdate(inStream, entity, sysAddr); - break; - - // Racing: most racing messages are registered in g_MessageHandlers case MessageType::Game::ACKNOWLEDGE_POSSESSION: GameMessages::HandleAcknowledgePossession(inStream, entity, sysAddr); break; @@ -565,21 +557,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System case MessageType::Game::ZONE_SUMMARY_DISMISSED: GameMessages::HandleZoneSummaryDismissed(inStream, entity); break; - case MessageType::Game::ADD_DONATION_ITEM: - GameMessages::HandleAddDonationItem(inStream, entity, sysAddr); - break; - case MessageType::Game::REMOVE_DONATION_ITEM: - GameMessages::HandleRemoveDonationItem(inStream, entity, sysAddr); - break; - case MessageType::Game::CONFIRM_DONATION_ON_PLAYER: - GameMessages::HandleConfirmDonationOnPlayer(inStream, entity); - break; - case MessageType::Game::CANCEL_DONATION_ON_PLAYER: - GameMessages::HandleCancelDonationOnPlayer(inStream, entity); - break; - case MessageType::Game::REQUEST_VENDOR_STATUS_UPDATE: - GameMessages::SendVendorStatusUpdate(entity, sysAddr, true); - 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 bbf294ad3..9d06d0c1f 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -710,53 +710,6 @@ void GameMessages::SendRemoveSkill(Entity* entity, TSkillID skillID) { SEND_PACKET; } -void GameMessages::SendVendorOpenWindow(Entity* entity, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::VENDOR_OPEN_WINDOW); - - SEND_PACKET; -} - -void GameMessages::SendVendorStatusUpdate(Entity* entity, const SystemAddress& sysAddr, bool bUpdateOnly) { - CBITSTREAM; - CMSGHEADER; - - VendorComponent* vendor = static_cast(entity->GetComponent(eReplicaComponentType::VENDOR)); - if (!vendor) return; - - auto vendorItems = vendor->GetInventory(); - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::VENDOR_STATUS_UPDATE); - - bitStream.Write(bUpdateOnly); - bitStream.Write(vendorItems.size()); - - - for (const auto& item : vendorItems) { - bitStream.Write(item.lot); - bitStream.Write(item.sortPriority); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendVendorTransactionResult(Entity* entity, const SystemAddress& sysAddr, eVendorTransactionResult result) { - CBITSTREAM; - CMSGHEADER; - - - bitStream.Write(entity->GetObjectID()); - bitStream.Write(MessageType::Game::VENDOR_TRANSACTION_RESULT); - bitStream.Write(result); - - SEND_PACKET; -} - void GameMessages::SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response) { CBITSTREAM; CMSGHEADER; @@ -1405,264 +1358,6 @@ void GameMessages::SendTeamPickupItem(LWOOBJID objectId, LWOOBJID lootID, LWOOBJ SEND_PACKET; } -//Trading: - -void GameMessages::SendServerTradeInvite(LWOOBJID objectId, bool bNeedInvitePopUp, LWOOBJID i64Requestor, std::u16string wsName, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_INVITE); - - bitStream.Write(bNeedInvitePopUp); - bitStream.Write(i64Requestor); - bitStream.Write(wsName.size()); - for (const auto character : wsName) { - bitStream.Write(character); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendServerTradeInitialReply(LWOOBJID objectId, LWOOBJID i64Invitee, int32_t resultType, std::u16string wsName, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_INITIAL_REPLY); - - bitStream.Write(i64Invitee); - bitStream.Write(resultType); - bitStream.Write(wsName.size()); - for (const auto character : wsName) { - bitStream.Write(character); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendServerTradeFinalReply(LWOOBJID objectId, bool bResult, LWOOBJID i64Invitee, std::u16string wsName, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_FINAL_REPLY); - - bitStream.Write(bResult); - bitStream.Write(i64Invitee); - bitStream.Write(wsName.size()); - for (const auto character : wsName) { - bitStream.Write(character); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendServerTradeAccept(LWOOBJID objectId, bool bFirst, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_ACCEPT); - - bitStream.Write(bFirst); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendServerTradeCancel(LWOOBJID objectId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_CANCEL); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendServerTradeUpdate(LWOOBJID objectId, uint64_t coins, const std::vector& items, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::SERVER_TRADE_UPDATE); - - bitStream.Write(false); - bitStream.Write(coins); - bitStream.Write(items.size()); - - for (const auto& item : items) { - bitStream.Write(item.itemId); - bitStream.Write(item.itemId); - - bitStream.Write(item.itemLot); - bitStream.Write0(); - bitStream.Write1(); - bitStream.Write(item.itemCount); - bitStream.Write0(); - bitStream.Write0(); - bitStream.Write0(); - bitStream.Write0(); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - // Check if the player has restricted trade access - auto* character = entity->GetCharacter(); - const bool restrictTradeOnMute = UserManager::Instance()->GetMuteRestrictTrade(); - - if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) { - // Send a message to the player - ChatPackets::SendSystemMessage( - sysAddr, - u"Your character has restricted trade access." - ); - - return; - } - - bool bNeedInvitePopUp = inStream.ReadBit(); - LWOOBJID i64Invitee; - - inStream.Read(i64Invitee); - - auto* invitee = Game::entityManager->GetEntity(i64Invitee); - - if (invitee != nullptr && invitee->IsPlayer()) { - character = invitee->GetCharacter(); - - if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) { - // Send a message to the player - ChatPackets::SendSystemMessage( - sysAddr, - u"The character you are trying to trade with has restricted trade access." - ); - - return; - } - - LOG("Trade request to (%llu)", i64Invitee); - - const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); - - if (trade != nullptr) { - if (!trade->IsParticipant(i64Invitee)) { - TradingManager::Instance()->CancelTrade(entity->GetObjectID(), trade->GetTradeId()); - - TradingManager::Instance()->NewTrade(entity->GetObjectID(), i64Invitee); - } - } else { - TradingManager::Instance()->NewTrade(entity->GetObjectID(), i64Invitee); - } - - SendServerTradeInvite( - i64Invitee, - bNeedInvitePopUp, - entity->GetObjectID(), - GeneralUtils::UTF8ToUTF16(entity->GetCharacter()->GetName()), - invitee->GetSystemAddress() - ); - } -} - -void GameMessages::HandleClientTradeCancel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); - - if (trade == nullptr) return; - - LOG("Trade canceled from (%llu)", entity->GetObjectID()); - - TradingManager::Instance()->CancelTrade(entity->GetObjectID(), trade->GetTradeId()); -} - -void GameMessages::HandleClientTradeAccept(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool bFirst = inStream.ReadBit(); - - LOG("Trade accepted from (%llu) -> (%d)", entity->GetObjectID(), bFirst); - - const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); - - if (trade == nullptr) return; - - trade->SetAccepted(entity->GetObjectID(), bFirst); -} - -void GameMessages::HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - uint64_t currency; - uint32_t itemCount; - - inStream.Read(currency); - inStream.Read(itemCount); - if (itemCount > MAX_MESSAGE_LENGTH) return; - - LOG("Trade update from (%llu) -> (%llu), (%i)", entity->GetObjectID(), currency, itemCount); - - std::vector items{}; - - for (size_t i = 0; i < itemCount; i++) { - LWOOBJID itemId; - LWOOBJID itemId2; - - inStream.Read(itemId); - inStream.Read(itemId2); - - LOT lot = 0; - LWOOBJID unknown1 = 0; - uint32_t unknown2 = 0; - uint16_t slot = 0; - uint32_t unknown3 = 0; - uint32_t ldfSize = 0; - bool unknown4; - - inStream.Read(lot); - if (inStream.ReadBit()) { - inStream.Read(unknown1); - } - if (inStream.ReadBit()) { - inStream.Read(unknown2); - } - if (inStream.ReadBit()) { - inStream.Read(slot); - } - if (inStream.ReadBit()) { - inStream.Read(unknown3); - } - if (inStream.ReadBit()) // No - { - inStream.Read(ldfSize); - bool compressed = inStream.ReadBit(); - if (compressed) { - uint32_t ldfCompressedSize = 0; - inStream.Read(ldfCompressedSize); - inStream.IgnoreBytes(ldfCompressedSize); - } else { - inStream.IgnoreBytes(ldfSize); - } - } - unknown4 = inStream.ReadBit(); - - items.push_back({ itemId, lot, unknown2 }); - - LOG("Trade item from (%llu) -> (%llu)/(%llu), (%i), (%llu), (%i), (%i)", entity->GetObjectID(), itemId, itemId2, lot, unknown1, unknown2, unknown3); - } - - const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity->GetObjectID()); - - if (trade == nullptr) return; - - trade->SetCoins(entity->GetObjectID(), currency); - trade->SetItems(entity->GetObjectID(), items); - trade->SendUpdateToOther(entity->GetObjectID()); -} - //Pets: void GameMessages::SendRemoveBuff(Entity* entity, bool fromUnEquip, bool removeImmunity, uint32_t buffId) { @@ -1966,149 +1661,6 @@ void GameMessages::HandleSetGhostReferencePosition(RakNet::BitStream& inStream, } -void GameMessages::HandleBuyFromVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool bConfirmed{}; // This doesn't appear to do anything. Further research is needed. - bool countIsDefault{}; - int count = 1; - LOT item; - - inStream.Read(bConfirmed); - inStream.Read(countIsDefault); - if (countIsDefault) inStream.Read(count); - inStream.Read(item); - - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!player) return; - - // handle buying normal items - auto* vendorComponent = entity->GetComponent(); - if (vendorComponent) { - vendorComponent->Buy(player, item, count); - return; - } - - // handle buying achievement items - auto* achievementVendorComponent = entity->GetComponent(); - if (achievementVendorComponent) { - achievementVendorComponent->Buy(player, item, count); - return; - } - - // Handle buying properties - auto* propertyVendorComponent = entity->GetComponent(); - if (propertyVendorComponent) { - propertyVendorComponent->OnBuyFromVendor(player, bConfirmed, item, count); - return; - } -} - -void GameMessages::HandleSellToVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool countIsDefault{}; - int count = 1; - LWOOBJID iObjID; - - inStream.Read(countIsDefault); - if (countIsDefault) inStream.Read(count); - inStream.Read(iObjID); - - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!player) return; - Character* character = player->GetCharacter(); - if (!character) return; - InventoryComponent* inv = static_cast(player->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - VendorComponent* vend = static_cast(entity->GetComponent(eReplicaComponentType::VENDOR)); - if (!vend) return; - - Item* item = inv->FindItemById(iObjID); - if (!item) return; - - CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); - CDItemComponentTable* itemComponentTable = CDClientManager::GetTable(); - - int itemCompID = compRegistryTable->GetByIDAndType(item->GetLot(), eReplicaComponentType::ITEM); - CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID); - - // Items with a base value of 0 or max int are special items that should not be sold if they're not sub items - if (itemComp.baseValue == 0 || itemComp.baseValue == UINT_MAX) { - GameMessages::SendVendorTransactionResult(entity, sysAddr, eVendorTransactionResult::SELL_FAIL); - return; - } - - float sellScalar = vend->GetSellScalar(); - if (Inventory::IsValidItem(itemComp.currencyLOT)) { - const auto altCurrency = static_cast(itemComp.altCurrencyCost * sellScalar) * count; - inv->AddItem(itemComp.currencyLOT, std::floor(altCurrency), eLootSourceType::VENDOR); // Return alt currencies like faction tokens. - } - - inv->MoveItemToInventory(item, eInventoryType::VENDOR_BUYBACK, count, true, false, true); - character->SetCoins(std::floor(character->GetCoins() + (static_cast(itemComp.baseValue * sellScalar) * count)), eLootSourceType::VENDOR); - GameMessages::SendVendorTransactionResult(entity, sysAddr, eVendorTransactionResult::SELL_SUCCESS); -} - -void GameMessages::HandleBuybackFromVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool confirmed = false; - bool countIsDefault{}; - int count = 1; - LWOOBJID iObjID; - - inStream.Read(confirmed); - inStream.Read(countIsDefault); - if (countIsDefault) inStream.Read(count); - inStream.Read(iObjID); - - //if (!confirmed) return; they always built in this confirmed garbage... but never used it? - - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!player) return; - Character* character = player->GetCharacter(); - if (!character) return; - InventoryComponent* inv = static_cast(player->GetComponent(eReplicaComponentType::INVENTORY)); - if (!inv) return; - - VendorComponent* vend = static_cast(entity->GetComponent(eReplicaComponentType::VENDOR)); - if (!vend) return; - - Item* item = inv->FindItemById(iObjID); - if (!item) return; - - CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable(); - CDItemComponentTable* itemComponentTable = CDClientManager::GetTable(); - - int itemCompID = compRegistryTable->GetByIDAndType(item->GetLot(), eReplicaComponentType::ITEM); - CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID); - - float sellScalar = vend->GetSellScalar(); - - const auto cost = static_cast(std::floor(((itemComp.baseValue * sellScalar) * count))); - - if (character->GetCoins() < cost) { - GameMessages::SendVendorTransactionResult(entity, sysAddr, eVendorTransactionResult::PURCHASE_FAIL); - return; - } - - if (Inventory::IsValidItem(itemComp.currencyLOT)) { - const uint32_t altCurrencyCost = std::floor(itemComp.altCurrencyCost * sellScalar) * count; - if (inv->GetLotCount(itemComp.currencyLOT) < altCurrencyCost || !inv->RemoveItem(itemComp.currencyLOT, altCurrencyCost, eInventoryType::ALL)) { - GameMessages::SendVendorTransactionResult(entity, sysAddr, eVendorTransactionResult::PURCHASE_FAIL); - return; - } - } - - //inv->RemoveItem(count, -1, iObjID); - inv->MoveItemToInventory(item, Inventory::FindInventoryTypeForLot(item->GetLot()), count, true, false); - character->SetCoins(character->GetCoins() - cost, eLootSourceType::VENDOR); - //Game::entityManager->SerializeEntity(player); // so inventory updates - GameMessages::SendVendorTransactionResult(entity, sysAddr, eVendorTransactionResult::PURCHASE_SUCCESS); -} - void GameMessages::HandleParseChatMessage(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { std::u16string wsString; int iClientState; @@ -2603,105 +2155,6 @@ void GameMessages::HandleZoneSummaryDismissed(RakNet::BitStream& inStream, Entit entity->TriggerEvent(eTriggerEventType::ZONE_SUMMARY_DISMISSED, target); }; -void GameMessages::HandleAddDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - uint32_t count = 1; - bool hasCount = false; - inStream.Read(hasCount); - if (hasCount) inStream.Read(count); - LWOOBJID itemId = LWOOBJID_EMPTY; - inStream.Read(itemId); - if (!itemId) return; - - auto* donationVendorComponent = entity->GetComponent(); - if (!donationVendorComponent) return; - if (donationVendorComponent->GetActivityID() == 0) { - LOG("WARNING: Trying to dontate to a vendor with no activity"); - return; - } - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!player) return; - auto* characterComponent = player->GetComponent(); - if (!characterComponent) return; - auto* inventoryComponent = player->GetComponent(); - if (!inventoryComponent) return; - Item* item = inventoryComponent->FindItemById(itemId); - if (!item) return; - if (item->GetCount() < count) return; - characterComponent->SetCurrentInteracting(entity->GetObjectID()); - inventoryComponent->MoveItemToInventory(item, eInventoryType::DONATION, count, true, false, true); -} - -void GameMessages::HandleRemoveDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool confirmed = false; - inStream.Read(confirmed); - uint32_t count = 1; - bool hasCount = false; - inStream.Read(hasCount); - if (hasCount) inStream.Read(count); - LWOOBJID itemId = LWOOBJID_EMPTY; - inStream.Read(itemId); - if (!itemId) return; - - User* user = UserManager::Instance()->GetUser(sysAddr); - if (!user) return; - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - if (!player) return; - - auto* inventoryComponent = player->GetComponent(); - if (!inventoryComponent) return; - - Item* item = inventoryComponent->FindItemById(itemId); - if (!item) return; - if (item->GetCount() < count) return; - inventoryComponent->MoveItemToInventory(item, eInventoryType::BRICKS, count, true, false, true); -} - -void GameMessages::HandleConfirmDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity) { - const auto [inventoryComponent, missionComponent, characterComponent] = entity->GetComponentsMut(); - if (!inventoryComponent || !missionComponent || !characterComponent || !characterComponent->GetCurrentInteracting()) return; - - auto* donationEntity = Game::entityManager->GetEntity(characterComponent->GetCurrentInteracting()); - if (!donationEntity) return; - auto* donationVendorComponent = donationEntity->GetComponent(); - if (!donationVendorComponent) return; - if (donationVendorComponent->GetActivityID() == 0) { - LOG("WARNING: Trying to dontate to a vendor with no activity"); - return; - } - auto* inventory = inventoryComponent->GetInventory(eInventoryType::DONATION); - if (!inventory) return; - auto items = inventory->GetItems(); - if (!items.empty()) { - uint32_t count = 0; - for (auto& [itemID, item] : items) { - count += item->GetCount(); - EconomyLedger::RecordItemsUnsuppressed(entity, item->GetLot(), -static_cast(item->GetCount()), EconomyLedger::DONATION_SOURCE); - item->RemoveFromInventory(); - } - missionComponent->Progress(eMissionTaskType::DONATION, 0, LWOOBJID_EMPTY, "", count); - LeaderboardManager::SaveScore(entity->GetObjectID(), donationVendorComponent->GetActivityID(), count); - donationVendorComponent->SubmitDonation(count); - Game::entityManager->SerializeEntity(donationEntity); - } - characterComponent->SetCurrentInteracting(LWOOBJID_EMPTY); -} - -void GameMessages::HandleCancelDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity) { - auto* inventoryComponent = entity->GetComponent(); - if (!inventoryComponent) return; - auto* inventory = inventoryComponent->GetInventory(eInventoryType::DONATION); - if (!inventory) return; - auto items = inventory->GetItems(); - for (auto& [itemID, item] : items) { - inventoryComponent->MoveItemToInventory(item, eInventoryType::BRICKS, item->GetCount(), false, false, true); - } - auto* characterComponent = entity->GetComponent(); - if (!characterComponent) return; - characterComponent->SetCurrentInteracting(LWOOBJID_EMPTY); -} - void GameMessages::SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID) { CBITSTREAM; CMSGHEADER; diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index dbc078b97..8d0653c5d 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -172,14 +172,9 @@ namespace GameMessages { void SendAddSkill(Entity* entity, TSkillID skillID, BehaviorSlot slotID); void SendRemoveSkill(Entity* entity, TSkillID skillID); - void SendVendorOpenWindow(Entity* entity, const SystemAddress& sysAddr); - void SendVendorStatusUpdate(Entity* entity, const SystemAddress& sysAddr, bool bUpdateOnly = false); - void SendVendorTransactionResult(Entity* entity, const SystemAddress& sysAddr, eVendorTransactionResult result); - void SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response); void SendMatchUpdate(Entity* entity, const SystemAddress& sysAddr, std::string data, eMatchUpdate type); - // https://lcdruniverse.org/lu_packets/lu_packets/world/gm/client/struct.SetResurrectRestoreValues.html void SendSetResurrectRestoreValues(Entity* targetEntity, int32_t armorRestore, int32_t healthRestore, int32_t imaginationRestore); /** @@ -282,27 +277,6 @@ namespace GameMessages { void SendTeamPickupItem(LWOOBJID objectId, LWOOBJID lootID, LWOOBJID lootOwnerID, const SystemAddress& sysAddr); - //Trading: - void SendServerTradeInvite(LWOOBJID objectId, bool bNeedInvitePopUp, LWOOBJID i64Requestor, std::u16string wsName, const SystemAddress& sysAddr); - - void SendServerTradeInitialReply(LWOOBJID objectId, LWOOBJID i64Invitee, int32_t resultType, std::u16string wsName, const SystemAddress& sysAddr); - - void SendServerTradeFinalReply(LWOOBJID objectId, bool bResult, LWOOBJID i64Invitee, std::u16string wsName, const SystemAddress& sysAddr); - - void SendServerTradeAccept(LWOOBJID objectId, bool bFirst, const SystemAddress& sysAddr); - - void SendServerTradeCancel(LWOOBJID objectId, const SystemAddress& sysAddr); - - void SendServerTradeUpdate(LWOOBJID objectId, uint64_t coins, const std::vector& items, const SystemAddress& sysAddr); - - void HandleClientTradeRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleClientTradeCancel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleClientTradeAccept(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleClientTradeUpdate(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - //Pets: void SendDisplayZoneSummary(LWOOBJID objectId, const SystemAddress& sysAddr, bool isPropertyMap = false, bool isZoneStart = false, LWOOBJID sender = LWOOBJID_EMPTY); @@ -380,9 +354,6 @@ namespace GameMessages { void HandleToggleGhostReferenceOverride(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleSetGhostReferencePosition(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleBuyFromVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleSellToVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleBuybackFromVendor(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleParseChatMessage(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleToggleGhostReffrenceOverride(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleSetGhostReffrenceOverride(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); @@ -414,12 +385,6 @@ namespace GameMessages { void HandleZoneSummaryDismissed(RakNet::BitStream& inStream, Entity* entity); - // Donation vendor - void HandleAddDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleRemoveDonationItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleConfirmDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity); - void HandleCancelDonationOnPlayer(RakNet::BitStream& inStream, Entity* entity); - void SendForceCameraTargetCycle(Entity* entity, bool bForceCycling, eCameraTargetCyclingMode cyclingMode, LWOOBJID optionalTargetID); struct DisplayTooltip : public NetGameMsg { diff --git a/dGame/dGameMessages/TradeMessages.cpp b/dGame/dGameMessages/TradeMessages.cpp new file mode 100644 index 000000000..344fa8a71 --- /dev/null +++ b/dGame/dGameMessages/TradeMessages.cpp @@ -0,0 +1,303 @@ +#include "TradeMessages.h" + +#include "BitStreamUtils.h" +#include "Character.h" +#include "ChatPackets.h" +#include "dCommonVars.h" +#include "Entity.h" +#include "EntityManager.h" +#include "ePermissionMap.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "Logger.h" +#include "TradingManager.h" +#include "User.h" +#include "UserManager.h" + +namespace { + template + void WriteFlagged(RakNet::BitStream& bitStream, const std::optional& value) { + bitStream.Write(value.has_value()); + if (value) bitStream.Write(*value); + } + + template + bool ReadFlagged(RakNet::BitStream& bitStream, std::optional& value) { + bool present{}; + if (!bitStream.Read(present)) return false; + if (!present) { + value.reset(); + return true; + } + T read{}; + if (!bitStream.Read(read)) return false; + value = read; + return true; + } + + void WriteEntries(RakNet::BitStream& bitStream, const std::vector& entries) { + bitStream.Write(entries.size()); + for (const auto& entry : entries) entry.Serialize(bitStream); + } + + bool ReadEntries(RakNet::BitStream& bitStream, std::vector& entries) { + uint32_t count{}; + if (!bitStream.Read(count)) return false; + if (count > MAX_MESSAGE_LENGTH) return false; + entries.clear(); + for (uint32_t i = 0; i < count; i++) { + GameMessages::TradeItemEntry entry; + if (!entry.Deserialize(bitStream)) return false; + entries.push_back(std::move(entry)); + } + return true; + } +} + +namespace GameMessages { + void TradeItemEntry::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(key); + bitStream.Write(itemID); + bitStream.Write(templateID); + WriteFlagged(bitStream, subkey); + WriteFlagged(bitStream, count); + WriteFlagged(bitStream, slot); + WriteFlagged(bitStream, inventoryType); + bitStream.Write(config.has_value()); + if (config) { + bitStream.Write(config->size); + bitStream.Write(config->compressed); + if (config->compressed) bitStream.Write(config->compressedSize); + bitStream.WriteBits(config->data.data(), BYTES_TO_BITS(config->data.size())); + } + bitStream.Write(unknownFlag); + } + + bool TradeItemEntry::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(key)); + VALIDATE_READ(bitStream.Read(itemID)); + VALIDATE_READ(bitStream.Read(templateID)); + VALIDATE_READ(ReadFlagged(bitStream, subkey)); + VALIDATE_READ(ReadFlagged(bitStream, count)); + VALIDATE_READ(ReadFlagged(bitStream, slot)); + VALIDATE_READ(ReadFlagged(bitStream, inventoryType)); + bool hasConfig{}; + VALIDATE_READ(bitStream.Read(hasConfig)); + if (hasConfig) { + Config readConfig; + VALIDATE_READ(bitStream.Read(readConfig.size)); + VALIDATE_READ(bitStream.Read(readConfig.compressed)); + if (readConfig.compressed) VALIDATE_READ(bitStream.Read(readConfig.compressedSize)); + const uint32_t dataSize = readConfig.compressed ? readConfig.compressedSize : readConfig.size; + if (BYTES_TO_BITS(static_cast(dataSize)) > bitStream.GetNumberOfUnreadBits()) return false; + readConfig.data.resize(dataSize); + if (dataSize > 0) VALIDATE_READ(bitStream.ReadBits(readConfig.data.data(), BYTES_TO_BITS(dataSize), true)); + config = std::move(readConfig); + } else { + config.reset(); + } + VALIDATE_READ(bitStream.Read(unknownFlag)); + return true; + } + + void ClientTradeRequest::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bNeedInvitePopUp); + bitStream.Write(i64Invitee); + } + + bool ClientTradeRequest::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bNeedInvitePopUp)); + VALIDATE_READ(bitStream.Read(i64Invitee)); + return true; + } + + void ClientTradeRequest::Handle(Entity& entity, const SystemAddress& sysAddr) { + // Check if the player has restricted trade access + auto* character = entity.GetCharacter(); + const bool restrictTradeOnMute = UserManager::Instance()->GetMuteRestrictTrade(); + + if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) { + // Send a message to the player + ChatPackets::SendSystemMessage( + sysAddr, + u"Your character has restricted trade access." + ); + + return; + } + + auto* invitee = Game::entityManager->GetEntity(i64Invitee); + + if (invitee != nullptr && invitee->IsPlayer()) { + character = invitee->GetCharacter(); + + if (character->HasPermission(ePermissionMap::RestrictedTradeAccess) || (restrictTradeOnMute && character->GetParentUser()->GetIsMuted())) { + // Send a message to the player + ChatPackets::SendSystemMessage( + sysAddr, + u"The character you are trying to trade with has restricted trade access." + ); + + return; + } + + LOG("Trade request to (%llu)", i64Invitee); + + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity.GetObjectID()); + + if (trade != nullptr) { + if (!trade->IsParticipant(i64Invitee)) { + TradingManager::Instance()->CancelTrade(entity.GetObjectID(), trade->GetTradeId()); + + TradingManager::Instance()->NewTrade(entity.GetObjectID(), i64Invitee); + } + } else { + TradingManager::Instance()->NewTrade(entity.GetObjectID(), i64Invitee); + } + + ServerTradeInvite invite; + invite.target = i64Invitee; + invite.bNeedInvitePopUp = bNeedInvitePopUp; + invite.i64Requestor = entity.GetObjectID(); + invite.wsName = GeneralUtils::UTF8ToUTF16(entity.GetCharacter()->GetName()); + invite.Send(invitee->GetSystemAddress()); + } + } + + void ServerTradeInvite::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bNeedInvitePopUp); + bitStream.Write(i64Requestor); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsName); + } + + bool ServerTradeInvite::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bNeedInvitePopUp)); + VALIDATE_READ(bitStream.Read(i64Requestor)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsName)); + return true; + } + + void ServerTradeInitialReply::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(i64Invitee); + bitStream.Write(resultType); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsName); + } + + bool ServerTradeInitialReply::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(i64Invitee)); + VALIDATE_READ(bitStream.Read(resultType)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsName)); + return true; + } + + void ServerTradeFinalReply::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bResult); + bitStream.Write(i64Invitee); + BitStreamUtils::WriteLengthPrefixed(bitStream, wsName); + } + + bool ServerTradeFinalReply::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bResult)); + VALIDATE_READ(bitStream.Read(i64Invitee)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, wsName)); + return true; + } + + void ClientTradeUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(i64Currency); + WriteEntries(bitStream, inventoryMap); + } + + bool ClientTradeUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(i64Currency)); + VALIDATE_READ(ReadEntries(bitStream, inventoryMap)); + return true; + } + + void ClientTradeUpdate::Handle(Entity& entity, const SystemAddress& sysAddr) { + LOG("Trade update from (%llu) -> (%llu), (%i)", entity.GetObjectID(), i64Currency, static_cast(inventoryMap.size())); + + std::vector items{}; + + for (const auto& entry : inventoryMap) { + const auto unknown1 = entry.subkey.value_or(0); + const auto unknown2 = entry.count.value_or(0); + const auto unknown3 = entry.inventoryType.value_or(0); + + items.push_back({ entry.key, entry.templateID, unknown2 }); + + LOG("Trade item from (%llu) -> (%llu)/(%llu), (%i), (%llu), (%i), (%i)", entity.GetObjectID(), entry.key, entry.itemID, entry.templateID, unknown1, unknown2, unknown3); + } + + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity.GetObjectID()); + + if (trade == nullptr) return; + + trade->SetCoins(entity.GetObjectID(), i64Currency); + trade->SetItems(entity.GetObjectID(), items); + trade->SendUpdateToOther(entity.GetObjectID()); + } + + void ServerTradeUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bAboutToPerform); + bitStream.Write(i64Currency); + WriteEntries(bitStream, inventoryMap); + } + + bool ServerTradeUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bAboutToPerform)); + VALIDATE_READ(bitStream.Read(i64Currency)); + VALIDATE_READ(ReadEntries(bitStream, inventoryMap)); + return true; + } + + void ClientTradeCancel::Serialize(RakNet::BitStream& bitStream) const {} + + bool ClientTradeCancel::Deserialize(RakNet::BitStream& bitStream) { + return true; + } + + void ClientTradeCancel::Handle(Entity& entity, const SystemAddress& sysAddr) { + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity.GetObjectID()); + + if (trade == nullptr) return; + + LOG("Trade canceled from (%llu)", entity.GetObjectID()); + + TradingManager::Instance()->CancelTrade(entity.GetObjectID(), trade->GetTradeId()); + } + + void ClientTradeAccept::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFirst); + } + + bool ClientTradeAccept::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bFirst)); + return true; + } + + void ClientTradeAccept::Handle(Entity& entity, const SystemAddress& sysAddr) { + LOG("Trade accepted from (%llu) -> (%d)", entity.GetObjectID(), bFirst); + + const auto& trade = TradingManager::Instance()->GetPlayerTrade(entity.GetObjectID()); + + if (trade == nullptr) return; + + trade->SetAccepted(entity.GetObjectID(), bFirst); + } + + void ServerTradeCancel::Serialize(RakNet::BitStream& bitStream) const {} + + bool ServerTradeCancel::Deserialize(RakNet::BitStream& bitStream) { + return true; + } + + void ServerTradeAccept::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bFirst); + } + + bool ServerTradeAccept::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bFirst)); + return true; + } +} diff --git a/dGame/dGameMessages/TradeMessages.h b/dGame/dGameMessages/TradeMessages.h new file mode 100644 index 000000000..00a2fff2b --- /dev/null +++ b/dGame/dGameMessages/TradeMessages.h @@ -0,0 +1,143 @@ +#ifndef TRADEMESSAGES_H +#define TRADEMESSAGES_H + +#include "GameMessages.h" + +#include +#include + +// Game messages for player to player trading. +// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +// The server -> client messages are sent with Send (UNASSIGNED_SYSTEM_ADDRESS broadcasts), as before. +namespace GameMessages { + // One entry of a trade offer: the object ID it is keyed by, then the client's inventory item layout. + // Optional fields are written as a flag bit followed by the value when the flag is set. + struct TradeItemEntry { + void Serialize(RakNet::BitStream& bitStream) const; + bool Deserialize(RakNet::BitStream& bitStream); + bool operator==(const TradeItemEntry&) const = default; + + LWOOBJID key{}; // the map key, the item's object ID + LWOOBJID itemID{}; + LOT templateID{}; + std::optional subkey{}; + std::optional count{}; + std::optional slot{}; + std::optional inventoryType{}; + + // Optional config (LDF) block, kept as bytes: u32 uncompressed size, compressed bit, then either the raw + // data (uncompressed size bytes) or a u32 compressed size and the compressed data. + struct Config { + uint32_t size{}; + bool compressed{}; + uint32_t compressedSize{}; + std::vector data{}; + bool operator==(const Config&) const = default; + }; + std::optional config{}; + + bool unknownFlag{}; // trailing bit, not used by DLU + }; + + // Client -> server. + struct ClientTradeRequest : public NetGameMsg { + ClientTradeRequest() : NetGameMsg(MessageType::Game::CLIENT_TRADE_REQUEST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bNeedInvitePopUp{}; + LWOOBJID i64Invitee{}; + }; + + // Server -> client. + struct ServerTradeInvite : public NetGameMsg { + ServerTradeInvite() : NetGameMsg(MessageType::Game::SERVER_TRADE_INVITE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bNeedInvitePopUp{}; + LWOOBJID i64Requestor{}; + std::u16string wsName{}; // u32 length prefixed + }; + + // Server -> client. + struct ServerTradeInitialReply : public NetGameMsg { + ServerTradeInitialReply() : NetGameMsg(MessageType::Game::SERVER_TRADE_INITIAL_REPLY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LWOOBJID i64Invitee{}; + int32_t resultType{}; + std::u16string wsName{}; // u32 length prefixed + }; + + // Server -> client. + struct ServerTradeFinalReply : public NetGameMsg { + ServerTradeFinalReply() : NetGameMsg(MessageType::Game::SERVER_TRADE_FINAL_REPLY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bResult{}; + LWOOBJID i64Invitee{}; + std::u16string wsName{}; // u32 length prefixed + }; + + // Client -> server. + struct ClientTradeUpdate : public NetGameMsg { + ClientTradeUpdate() : NetGameMsg(MessageType::Game::CLIENT_TRADE_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + uint64_t i64Currency{}; + std::vector inventoryMap{}; // u32 count, then the entries + }; + + // Server -> client. + struct ServerTradeUpdate : public NetGameMsg { + ServerTradeUpdate() : NetGameMsg(MessageType::Game::SERVER_TRADE_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bAboutToPerform{}; + uint64_t i64Currency{}; + std::vector inventoryMap{}; // u32 count, then the entries + }; + + // Client -> server. No payload. + struct ClientTradeCancel : public NetGameMsg { + ClientTradeCancel() : NetGameMsg(MessageType::Game::CLIENT_TRADE_CANCEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Client -> server. + struct ClientTradeAccept : public NetGameMsg { + ClientTradeAccept() : NetGameMsg(MessageType::Game::CLIENT_TRADE_ACCEPT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool bFirst{}; + }; + + // Server -> client. No payload. + struct ServerTradeCancel : public NetGameMsg { + ServerTradeCancel() : NetGameMsg(MessageType::Game::SERVER_TRADE_CANCEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Server -> client. + struct ServerTradeAccept : public NetGameMsg { + ServerTradeAccept() : NetGameMsg(MessageType::Game::SERVER_TRADE_ACCEPT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + bool bFirst{}; + }; +}; + +#endif // TRADEMESSAGES_H diff --git a/dGame/dGameMessages/VendorMessages.cpp b/dGame/dGameMessages/VendorMessages.cpp new file mode 100644 index 000000000..d1515d32f --- /dev/null +++ b/dGame/dGameMessages/VendorMessages.cpp @@ -0,0 +1,252 @@ +#include "VendorMessages.h" + +#include "AchievementVendorComponent.h" +#include "BitStreamUtils.h" +#include "CharacterComponent.h" +#include "DonationVendorComponent.h" +#include "eInventoryType.h" +#include "eReplicaComponentType.h" +#include "eVendorTransactionResult.h" +#include "Entity.h" +#include "EntityManager.h" +#include "Game.h" +#include "InventoryComponent.h" +#include "Item.h" +#include "PropertyVendorComponent.h" +#include "User.h" +#include "UserManager.h" +#include "VendorComponent.h" + +namespace { + // The player that sent a message, found from the sender's address as the old handlers did. + Entity* GetSendingPlayer(const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + if (!user) return nullptr; + return Game::entityManager->GetEntity(user->GetLoggedInChar()); + } +} + +namespace GameMessages { + void VendorOpenWindow::Serialize(RakNet::BitStream& bitStream) const {} + + bool VendorOpenWindow::Deserialize(RakNet::BitStream& bitStream) { + return true; + } + + void RequestVendorStatusUpdate::Serialize(RakNet::BitStream& bitStream) const {} + + bool RequestVendorStatusUpdate::Deserialize(RakNet::BitStream& bitStream) { + return true; + } + + void RequestVendorStatusUpdate::Handle(Entity& entity, const SystemAddress& sysAddr) { + // Only the VENDOR component answers, as before (not achievement or donation vendors). + auto* vendor = static_cast(entity.GetComponent(eReplicaComponentType::VENDOR)); + if (vendor) vendor->SendStatusUpdate(sysAddr, true); + } + + void VendorStatusUpdate::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(bUpdateOnly); + bitStream.Write(inventoryList.size()); + for (const auto& entry : inventoryList) { + bitStream.Write(entry.lot); + bitStream.Write(entry.sortPriority); + } + } + + bool VendorStatusUpdate::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(bUpdateOnly)); + uint32_t count{}; + VALIDATE_READ(bitStream.Read(count)); + // Each entry is 8 bytes; refuse counts the stream cannot hold before allocating. + if (static_cast(count) * 64 > bitStream.GetNumberOfUnreadBits()) return false; + inventoryList.resize(count); + for (auto& entry : inventoryList) { + VALIDATE_READ(bitStream.Read(entry.lot)); + VALIDATE_READ(bitStream.Read(entry.sortPriority)); + } + return true; + } + + void VendorTransactionResult::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(iResult); + } + + bool VendorTransactionResult::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(iResult)); + return true; + } + + void BuyFromVendor::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(confirmed); + BitStreamUtils::WriteOptional(bitStream, count, 1); + bitStream.Write(item); + } + + bool BuyFromVendor::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(confirmed)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1)); + VALIDATE_READ(bitStream.Read(item)); + return true; + } + + void BuyFromVendor::Handle(Entity& entity, const SystemAddress& sysAddr) { + Entity* player = GetSendingPlayer(sysAddr); + if (!player) return; + + // handle buying normal items + auto* vendorComponent = entity.GetComponent(); + if (vendorComponent) { + vendorComponent->Buy(player, item, count); + return; + } + + // handle buying achievement items + auto* achievementVendorComponent = entity.GetComponent(); + if (achievementVendorComponent) { + achievementVendorComponent->Buy(player, item, count); + return; + } + + // Handle buying properties + auto* propertyVendorComponent = entity.GetComponent(); + if (propertyVendorComponent) { + propertyVendorComponent->OnBuyFromVendor(player, confirmed, item, count); + return; + } + } + + void SellToVendor::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, count, 1); + bitStream.Write(itemObjID); + } + + bool SellToVendor::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1)); + VALIDATE_READ(bitStream.Read(itemObjID)); + return true; + } + + void SellToVendor::Handle(Entity& entity, const SystemAddress& sysAddr) { + Entity* player = GetSendingPlayer(sysAddr); + if (!player) return; + + auto* vendor = static_cast(entity.GetComponent(eReplicaComponentType::VENDOR)); + if (!vendor) return; + + vendor->SellToVendor(*player, sysAddr, itemObjID, count); + } + + void BuybackFromVendor::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(confirmed); + BitStreamUtils::WriteOptional(bitStream, count, 1); + bitStream.Write(item); + } + + bool BuybackFromVendor::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(confirmed)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1)); + VALIDATE_READ(bitStream.Read(item)); + return true; + } + + void BuybackFromVendor::Handle(Entity& entity, const SystemAddress& sysAddr) { + //if (!confirmed) return; they always built in this confirmed garbage... but never used it? + + Entity* player = GetSendingPlayer(sysAddr); + if (!player) return; + + auto* vendor = static_cast(entity.GetComponent(eReplicaComponentType::VENDOR)); + if (!vendor) return; + + vendor->BuybackFromVendor(*player, sysAddr, item, count); + } + + void AddDonationItem::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, count, 1u); + bitStream.Write(itemObjID); + } + + bool AddDonationItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1u)); + VALIDATE_READ(bitStream.Read(itemObjID)); + return true; + } + + void AddDonationItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + if (!itemObjID) return; + + auto* donationVendorComponent = entity.GetComponent(); + if (!donationVendorComponent) return; + donationVendorComponent->AddDonationItem(sysAddr, itemObjID, count); + } + + void RemoveDonationItem::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(confirmed); + BitStreamUtils::WriteOptional(bitStream, count, 1u); + bitStream.Write(itemObjID); + } + + bool RemoveDonationItem::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(confirmed)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, count, 1u)); + VALIDATE_READ(bitStream.Read(itemObjID)); + return true; + } + + void RemoveDonationItem::Handle(Entity& entity, const SystemAddress& sysAddr) { + if (!itemObjID) return; + + Entity* player = GetSendingPlayer(sysAddr); + if (!player) return; + + auto* inventoryComponent = player->GetComponent(); + if (!inventoryComponent) return; + + Item* item = inventoryComponent->FindItemById(itemObjID); + if (!item) return; + if (item->GetCount() < count) return; + inventoryComponent->MoveItemToInventory(item, eInventoryType::BRICKS, count, true, false, true); + } + + void ConfirmDonationOnPlayer::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(vendorID); + } + + bool ConfirmDonationOnPlayer::Deserialize(RakNet::BitStream& bitStream) { + // The old handler read nothing; keep accepting the message even if vendorID is missing. + if (!bitStream.Read(vendorID)) vendorID = LWOOBJID_EMPTY; + return true; + } + + void ConfirmDonationOnPlayer::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* characterComponent = entity.GetComponent(); + if (!characterComponent || !characterComponent->GetCurrentInteracting()) return; + + auto* donationEntity = Game::entityManager->GetEntity(characterComponent->GetCurrentInteracting()); + if (!donationEntity) return; + auto* donationVendorComponent = donationEntity->GetComponent(); + if (!donationVendorComponent) return; + donationVendorComponent->ConfirmDonation(entity); + } + + void CancelDonationOnPlayer::Serialize(RakNet::BitStream& bitStream) const {} + + bool CancelDonationOnPlayer::Deserialize(RakNet::BitStream& bitStream) { + return true; + } + + void CancelDonationOnPlayer::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* inventoryComponent = entity.GetComponent(); + if (!inventoryComponent) return; + auto* inventory = inventoryComponent->GetInventory(eInventoryType::DONATION); + if (!inventory) return; + auto items = inventory->GetItems(); + for (auto& [itemID, item] : items) { + inventoryComponent->MoveItemToInventory(item, eInventoryType::BRICKS, item->GetCount(), false, false, true); + } + auto* characterComponent = entity.GetComponent(); + if (!characterComponent) return; + characterComponent->SetCurrentInteracting(LWOOBJID_EMPTY); + } +} diff --git a/dGame/dGameMessages/VendorMessages.h b/dGame/dGameMessages/VendorMessages.h new file mode 100644 index 000000000..525c510f8 --- /dev/null +++ b/dGame/dGameMessages/VendorMessages.h @@ -0,0 +1,132 @@ +#ifndef VENDORMESSAGES_H +#define VENDORMESSAGES_H + +#include "GameMessages.h" + +#include + +enum class eVendorTransactionResult : uint32_t; + +// Game messages for vendors (buy, sell, buyback, the vendor window and its stock) and donation vendors. +// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +namespace GameMessages { + // Server -> client. Sent with SendToClient: the old function never broadcast. + struct VendorOpenWindow : public NetGameMsg { + VendorOpenWindow() : NetGameMsg(MessageType::Game::VENDOR_OPEN_WINDOW) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + + // Client -> server. Asks the vendor (the target) to resend its stock. No payload. + struct RequestVendorStatusUpdate : public NetGameMsg { + RequestVendorStatusUpdate() : NetGameMsg(MessageType::Game::REQUEST_VENDOR_STATUS_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Server -> client. The vendor's stock. UNASSIGNED_SYSTEM_ADDRESS broadcasts. + struct VendorStatusUpdate : public NetGameMsg { + VendorStatusUpdate() : NetGameMsg(MessageType::Game::VENDOR_STATUS_UPDATE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + struct Entry { + LOT lot{}; + int32_t sortPriority{}; + bool operator==(const Entry&) const = default; + }; + + bool bUpdateOnly{}; + std::vector inventoryList{}; // u32 count, then the entries + }; + + // Server -> client. Sent with SendToClient: the old function never broadcast. + struct VendorTransactionResult : public NetGameMsg { + VendorTransactionResult() : NetGameMsg(MessageType::Game::VENDOR_TRANSACTION_RESULT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + eVendorTransactionResult iResult{}; + }; + + // Client -> server. Target is the vendor. + struct BuyFromVendor : public NetGameMsg { + BuyFromVendor() : NetGameMsg(MessageType::Game::BUY_FROM_VENDOR) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool confirmed{}; + int32_t count{ 1 }; // optional + LOT item{}; + }; + + // Client -> server. Target is the vendor. + struct SellToVendor : public NetGameMsg { + SellToVendor() : NetGameMsg(MessageType::Game::SELL_TO_VENDOR) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t count{ 1 }; // optional + LWOOBJID itemObjID{}; + }; + + // Client -> server. Target is the vendor. + struct BuybackFromVendor : public NetGameMsg { + BuybackFromVendor() : NetGameMsg(MessageType::Game::BUYBACK_FROM_VENDOR) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool confirmed{}; + int32_t count{ 1 }; // optional + LWOOBJID item{}; + }; + + // Client -> server. Target is the donation vendor. + struct AddDonationItem : public NetGameMsg { + AddDonationItem() : NetGameMsg(MessageType::Game::ADD_DONATION_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + uint32_t count{ 1 }; // optional + LWOOBJID itemObjID{}; + }; + + // Client -> server. + struct RemoveDonationItem : public NetGameMsg { + RemoveDonationItem() : NetGameMsg(MessageType::Game::REMOVE_DONATION_ITEM) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool confirmed{}; + uint32_t count{ 1 }; // optional + LWOOBJID itemObjID{}; + }; + + // Client -> server. Target is the player. The vendor comes from the player's current interaction, + // not from vendorID. + struct ConfirmDonationOnPlayer : public NetGameMsg { + ConfirmDonationOnPlayer() : NetGameMsg(MessageType::Game::CONFIRM_DONATION_ON_PLAYER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + // Not read by DLU: the old handler ignored the payload, so a message without it is still accepted. + LWOOBJID vendorID{}; + }; + + // Client -> server. Target is the player. No payload. + struct CancelDonationOnPlayer : public NetGameMsg { + CancelDonationOnPlayer() : NetGameMsg(MessageType::Game::CANCEL_DONATION_ON_PLAYER) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; +}; + +#endif // VENDORMESSAGES_H diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 148f1e45f..098fa353c 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -10,6 +10,7 @@ SET(DGAMEMESSAGES_TESTS "PropertyMessagesTests.cpp" "RacingMessagesTests.cpp" "UnSmashTests.cpp" + "VendorMessagesTests.cpp" "LegacyGameMessageTests.cpp" "GameMessageDecoderTests.cpp" "HotPropertyMessageTests.cpp") diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/VendorMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/VendorMessagesLegacy.h new file mode 100644 index 000000000..bbab54e54 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/VendorMessagesLegacy.h @@ -0,0 +1,328 @@ +#ifndef VENDORMESSAGESLEGACY_H +#define VENDORMESSAGESLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written GameMessages::Send* functions that VendorMessages.h and TradeMessages.h +// replaced (dGame/dGameMessages/GameMessages.cpp, branched from origin/main 129199e4). Only the namespace changed, +// with one exception: SendVendorStatusUpdate takes the vendor's stock as a parameter instead of reading it from +// the entity's VendorComponent (a VendorComponent cannot be stocked without CDClient data in the tests); the +// rest of its body is unchanged. +// The Read* functions are the read sequences of the replaced GameMessages::Handle* functions, verbatim up to +// the point where the handler starts using what it read. + +#include "BitStreamUtils.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "Entity.h" +#include "eVendorTransactionResult.h" +#include "Game.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "ServiceType.h" +#include "TradingManager.h" +#include "VendorComponent.h" + +#include +#include + +namespace LegacyGameMessages { + inline void SendVendorOpenWindow(Entity* entity, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::VENDOR_OPEN_WINDOW); + + SEND_PACKET; + } + + inline void SendVendorStatusUpdate(Entity* entity, const SystemAddress& sysAddr, const std::vector& vendorItems, bool bUpdateOnly = false) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::VENDOR_STATUS_UPDATE); + + bitStream.Write(bUpdateOnly); + bitStream.Write(vendorItems.size()); + + + for (const auto& item : vendorItems) { + bitStream.Write(item.lot); + bitStream.Write(item.sortPriority); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendVendorTransactionResult(Entity* entity, const SystemAddress& sysAddr, eVendorTransactionResult result) { + CBITSTREAM; + CMSGHEADER; + + + bitStream.Write(entity->GetObjectID()); + bitStream.Write(MessageType::Game::VENDOR_TRANSACTION_RESULT); + bitStream.Write(result); + + SEND_PACKET; + } + + inline void SendServerTradeInvite(LWOOBJID objectId, bool bNeedInvitePopUp, LWOOBJID i64Requestor, std::u16string wsName, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_INVITE); + + bitStream.Write(bNeedInvitePopUp); + bitStream.Write(i64Requestor); + bitStream.Write(wsName.size()); + for (const auto character : wsName) { + bitStream.Write(character); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendServerTradeInitialReply(LWOOBJID objectId, LWOOBJID i64Invitee, int32_t resultType, std::u16string wsName, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_INITIAL_REPLY); + + bitStream.Write(i64Invitee); + bitStream.Write(resultType); + bitStream.Write(wsName.size()); + for (const auto character : wsName) { + bitStream.Write(character); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendServerTradeFinalReply(LWOOBJID objectId, bool bResult, LWOOBJID i64Invitee, std::u16string wsName, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_FINAL_REPLY); + + bitStream.Write(bResult); + bitStream.Write(i64Invitee); + bitStream.Write(wsName.size()); + for (const auto character : wsName) { + bitStream.Write(character); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendServerTradeAccept(LWOOBJID objectId, bool bFirst, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_ACCEPT); + + bitStream.Write(bFirst); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendServerTradeCancel(LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_CANCEL); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendServerTradeUpdate(LWOOBJID objectId, uint64_t coins, const std::vector& items, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::SERVER_TRADE_UPDATE); + + bitStream.Write(false); + bitStream.Write(coins); + bitStream.Write(items.size()); + + for (const auto& item : items) { + bitStream.Write(item.itemId); + bitStream.Write(item.itemId); + + bitStream.Write(item.itemLot); + bitStream.Write0(); + bitStream.Write1(); + bitStream.Write(item.itemCount); + bitStream.Write0(); + bitStream.Write0(); + bitStream.Write0(); + bitStream.Write0(); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + // GameMessages::HandleBuyFromVendor + struct LegacyBuyFromVendor { bool bConfirmed{}; int count{}; LOT item{}; }; + inline LegacyBuyFromVendor ReadBuyFromVendor(RakNet::BitStream& inStream) { + bool bConfirmed{}; // This doesn't appear to do anything. Further research is needed. + bool countIsDefault{}; + int count = 1; + LOT item; + + inStream.Read(bConfirmed); + inStream.Read(countIsDefault); + if (countIsDefault) inStream.Read(count); + inStream.Read(item); + return { bConfirmed, count, item }; + } + + // GameMessages::HandleSellToVendor + struct LegacySellToVendor { int count{}; LWOOBJID iObjID{}; }; + inline LegacySellToVendor ReadSellToVendor(RakNet::BitStream& inStream) { + bool countIsDefault{}; + int count = 1; + LWOOBJID iObjID; + + inStream.Read(countIsDefault); + if (countIsDefault) inStream.Read(count); + inStream.Read(iObjID); + return { count, iObjID }; + } + + // GameMessages::HandleBuybackFromVendor + struct LegacyBuybackFromVendor { bool confirmed{}; int count{}; LWOOBJID iObjID{}; }; + inline LegacyBuybackFromVendor ReadBuybackFromVendor(RakNet::BitStream& inStream) { + bool confirmed = false; + bool countIsDefault{}; + int count = 1; + LWOOBJID iObjID; + + inStream.Read(confirmed); + inStream.Read(countIsDefault); + if (countIsDefault) inStream.Read(count); + inStream.Read(iObjID); + return { confirmed, count, iObjID }; + } + + // GameMessages::HandleAddDonationItem + struct LegacyAddDonationItem { uint32_t count{}; LWOOBJID itemId{}; }; + inline LegacyAddDonationItem ReadAddDonationItem(RakNet::BitStream& inStream) { + uint32_t count = 1; + bool hasCount = false; + inStream.Read(hasCount); + if (hasCount) inStream.Read(count); + LWOOBJID itemId = LWOOBJID_EMPTY; + inStream.Read(itemId); + return { count, itemId }; + } + + // GameMessages::HandleRemoveDonationItem + struct LegacyRemoveDonationItem { bool confirmed{}; uint32_t count{}; LWOOBJID itemId{}; }; + inline LegacyRemoveDonationItem ReadRemoveDonationItem(RakNet::BitStream& inStream) { + bool confirmed = false; + inStream.Read(confirmed); + uint32_t count = 1; + bool hasCount = false; + inStream.Read(hasCount); + if (hasCount) inStream.Read(count); + LWOOBJID itemId = LWOOBJID_EMPTY; + inStream.Read(itemId); + return { confirmed, count, itemId }; + } + + // GameMessages::HandleClientTradeRequest + struct LegacyClientTradeRequest { bool bNeedInvitePopUp{}; LWOOBJID i64Invitee{}; }; + inline LegacyClientTradeRequest ReadClientTradeRequest(RakNet::BitStream& inStream) { + bool bNeedInvitePopUp = inStream.ReadBit(); + LWOOBJID i64Invitee; + + inStream.Read(i64Invitee); + return { bNeedInvitePopUp, i64Invitee }; + } + + // GameMessages::HandleClientTradeAccept + inline bool ReadClientTradeAccept(RakNet::BitStream& inStream) { + bool bFirst = inStream.ReadBit(); + return bFirst; + } + + // GameMessages::HandleClientTradeUpdate. The per item log values are returned alongside the TradeItems. + struct LegacyTradeItemLog { LWOOBJID itemId{}; LWOOBJID itemId2{}; LOT lot{}; LWOOBJID unknown1{}; uint32_t unknown2{}; uint16_t slot{}; uint32_t unknown3{}; bool unknown4{}; }; + struct LegacyClientTradeUpdate { bool rejected{}; uint64_t currency{}; std::vector items{}; std::vector logs{}; }; + inline LegacyClientTradeUpdate ReadClientTradeUpdate(RakNet::BitStream& inStream) { + LegacyClientTradeUpdate result; + uint64_t currency; + uint32_t itemCount; + + inStream.Read(currency); + inStream.Read(itemCount); + if (itemCount > MAX_MESSAGE_LENGTH) { result.rejected = true; return result; } + + std::vector items{}; + + for (size_t i = 0; i < itemCount; i++) { + LWOOBJID itemId; + LWOOBJID itemId2; + + inStream.Read(itemId); + inStream.Read(itemId2); + + LOT lot = 0; + LWOOBJID unknown1 = 0; + uint32_t unknown2 = 0; + uint16_t slot = 0; + uint32_t unknown3 = 0; + uint32_t ldfSize = 0; + bool unknown4; + + inStream.Read(lot); + if (inStream.ReadBit()) { + inStream.Read(unknown1); + } + if (inStream.ReadBit()) { + inStream.Read(unknown2); + } + if (inStream.ReadBit()) { + inStream.Read(slot); + } + if (inStream.ReadBit()) { + inStream.Read(unknown3); + } + if (inStream.ReadBit()) // No + { + inStream.Read(ldfSize); + bool compressed = inStream.ReadBit(); + if (compressed) { + uint32_t ldfCompressedSize = 0; + inStream.Read(ldfCompressedSize); + inStream.IgnoreBytes(ldfCompressedSize); + } else { + inStream.IgnoreBytes(ldfSize); + } + } + unknown4 = inStream.ReadBit(); + + items.push_back({ itemId, lot, unknown2 }); + result.logs.push_back({ itemId, itemId2, lot, unknown1, unknown2, slot, unknown3, unknown4 }); + } + result.currency = currency; + result.items = items; + return result; + } +} + +#endif // VENDORMESSAGESLEGACY_H diff --git a/tests/dGameTests/dGameMessagesTests/VendorMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/VendorMessagesTests.cpp new file mode 100644 index 000000000..f2e6fb908 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/VendorMessagesTests.cpp @@ -0,0 +1,383 @@ +#include "VendorMessages.h" +#include "TradeMessages.h" +#include "GameDependencies.h" +#include "GameMessageTestUtils.h" +#include "Legacy/VendorMessagesLegacy.h" + +#include "eVendorTransactionResult.h" + +#include +#include +#include +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + const std::vector g_Ints = { 0, 1, -1, 5, 1727, std::numeric_limits::max(), std::numeric_limits::min() }; + const std::vector g_Names = { u"", u"A", u"Mythran", u"é中文", 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; + } + + // The entry DLU writes for a traded item. + GameMessages::TradeItemEntry EntryFor(const TradeItem& item) { + GameMessages::TradeItemEntry entry; + entry.key = item.itemId; + entry.itemID = item.itemId; + entry.templateID = item.itemLot; + entry.count = item.itemCount; + return entry; + } +} + +class VendorMessagesTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +TEST_F(VendorMessagesTests, VendorOpenWindowMatchesLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + GameMessages::VendorOpenWindow msg; + msg.target = target; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendVendorOpenWindow(&entity, a); }, msg, SendMode::SendToClient); + RoundTrip(msg); + } +} + +TEST_F(VendorMessagesTests, VendorStatusUpdateMatchesLegacy) { + std::vector many; + for (int32_t i = 0; i < 300; i++) many.emplace_back(1000 + i, i); + const std::vector> stocks = { {}, { SoldItem(1727, 0) }, { SoldItem(-1, -1), SoldItem(0, std::numeric_limits::max()), SoldItem(16253, 2) }, many }; + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto& stock : stocks) { + for (const bool bUpdateOnly : { false, true }) { + GameMessages::VendorStatusUpdate msg; + msg.target = target; + msg.bUpdateOnly = bUpdateOnly; + for (const auto& item : stock) msg.inventoryList.push_back({ item.lot, item.sortPriority }); + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendVendorStatusUpdate(&entity, a, stock, bUpdateOnly); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.bUpdateOnly, bUpdateOnly); + EXPECT_EQ(copy.inventoryList, msg.inventoryList); + } + } + } +} + +TEST_F(VendorMessagesTests, VendorTransactionResultMatchesLegacy) { + for (const auto target : g_Targets) { + Entity entity(target, info); + for (const auto result : { eVendorTransactionResult::SELL_SUCCESS, eVendorTransactionResult::SELL_FAIL, eVendorTransactionResult::PURCHASE_SUCCESS, eVendorTransactionResult::PURCHASE_FAIL, eVendorTransactionResult::DONATION_FAIL, eVendorTransactionResult::DONATION_FULL, static_cast(0xFFFFFFFF) }) { + GameMessages::VendorTransactionResult msg; + msg.target = target; + msg.iResult = result; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendVendorTransactionResult(&entity, a, result); }, msg, SendMode::SendToClient); + EXPECT_EQ(RoundTrip(msg).iResult, result); + } + } +} + +TEST_F(VendorMessagesTests, TradeRepliesMatchLegacy) { + for (const auto target : g_Targets) { + for (const auto other : g_Targets) { + for (const auto& name : g_Names) { + for (const bool flag : { false, true }) { + GameMessages::ServerTradeInvite invite; + invite.target = target; + invite.bNeedInvitePopUp = flag; + invite.i64Requestor = other; + invite.wsName = name; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeInvite(target, flag, other, name, a); }, invite); + const auto inviteCopy = RoundTrip(invite); + EXPECT_EQ(inviteCopy.bNeedInvitePopUp, flag); + EXPECT_EQ(inviteCopy.i64Requestor, other); + EXPECT_EQ(inviteCopy.wsName, name); + + GameMessages::ServerTradeFinalReply finalReply; + finalReply.target = target; + finalReply.bResult = flag; + finalReply.i64Invitee = other; + finalReply.wsName = name; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeFinalReply(target, flag, other, name, a); }, finalReply); + const auto finalCopy = RoundTrip(finalReply); + EXPECT_EQ(finalCopy.bResult, flag); + EXPECT_EQ(finalCopy.i64Invitee, other); + EXPECT_EQ(finalCopy.wsName, name); + } + for (const auto resultType : g_Ints) { + GameMessages::ServerTradeInitialReply initialReply; + initialReply.target = target; + initialReply.i64Invitee = other; + initialReply.resultType = resultType; + initialReply.wsName = name; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeInitialReply(target, other, resultType, name, a); }, initialReply); + const auto initialCopy = RoundTrip(initialReply); + EXPECT_EQ(initialCopy.i64Invitee, other); + EXPECT_EQ(initialCopy.resultType, resultType); + EXPECT_EQ(initialCopy.wsName, name); + } + } + } + } +} + +TEST_F(VendorMessagesTests, TradeAcceptAndCancelMatchLegacy) { + for (const auto target : g_Targets) { + for (const bool bFirst : { false, true }) { + GameMessages::ServerTradeAccept accept; + accept.target = target; + accept.bFirst = bFirst; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeAccept(target, bFirst, a); }, accept); + EXPECT_EQ(RoundTrip(accept).bFirst, bFirst); + } + GameMessages::ServerTradeCancel cancel; + cancel.target = target; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeCancel(target, a); }, cancel); + RoundTrip(cancel); + } +} + +TEST_F(VendorMessagesTests, ServerTradeUpdateMatchesLegacy) { + std::vector many; + for (uint32_t i = 0; i < 50; i++) many.push_back({ 0x1000000000000000LL + i, static_cast(i), i }); + const std::vector> itemSets = { + {}, + { { 0x0102030405060708LL, 1727, 1 } }, + { { LWOOBJID_EMPTY, LOT_NULL, 0 }, { 0x1000000000000001LL, 0, std::numeric_limits::max() } }, + many, + }; + for (const auto target : g_Targets) { + for (const uint64_t coins : { uint64_t{ 0 }, uint64_t{ 250 }, std::numeric_limits::max() }) { + for (const auto& items : itemSets) { + GameMessages::ServerTradeUpdate msg; + msg.target = target; + msg.i64Currency = coins; + for (const auto& item : items) msg.inventoryMap.push_back(EntryFor(item)); + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendServerTradeUpdate(target, coins, items, a); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.i64Currency, coins); + EXPECT_EQ(copy.inventoryMap, msg.inventoryMap); + } + } + } +} + +TEST_F(VendorMessagesTests, VendorInboundReadsLikeLegacy) { + for (const auto id : g_Targets) { + for (const auto count : g_Ints) { + for (const bool confirmed : { false, true }) { + GameMessages::BuyFromVendor buy; + buy.confirmed = confirmed; + buy.count = count; + buy.item = static_cast(id); + const auto legacyBuy = ReadWithLegacy(buy, LegacyGameMessages::ReadBuyFromVendor); + const auto buyCopy = RoundTrip(buy); + EXPECT_EQ(buyCopy.confirmed, legacyBuy.bConfirmed); + EXPECT_EQ(buyCopy.count, legacyBuy.count); + EXPECT_EQ(buyCopy.item, legacyBuy.item); + ExpectTruncatedFails(buy); + + GameMessages::BuybackFromVendor buyback; + buyback.confirmed = confirmed; + buyback.count = count; + buyback.item = id; + const auto legacyBuyback = ReadWithLegacy(buyback, LegacyGameMessages::ReadBuybackFromVendor); + const auto buybackCopy = RoundTrip(buyback); + EXPECT_EQ(buybackCopy.confirmed, legacyBuyback.confirmed); + EXPECT_EQ(buybackCopy.count, legacyBuyback.count); + EXPECT_EQ(buybackCopy.item, legacyBuyback.iObjID); + ExpectTruncatedFails(buyback); + + GameMessages::RemoveDonationItem remove; + remove.confirmed = confirmed; + remove.count = static_cast(count); + remove.itemObjID = id; + const auto legacyRemove = ReadWithLegacy(remove, LegacyGameMessages::ReadRemoveDonationItem); + const auto removeCopy = RoundTrip(remove); + EXPECT_EQ(removeCopy.confirmed, legacyRemove.confirmed); + EXPECT_EQ(removeCopy.count, legacyRemove.count); + EXPECT_EQ(removeCopy.itemObjID, legacyRemove.itemId); + ExpectTruncatedFails(remove); + } + + GameMessages::SellToVendor sell; + sell.count = count; + sell.itemObjID = id; + const auto legacySell = ReadWithLegacy(sell, LegacyGameMessages::ReadSellToVendor); + const auto sellCopy = RoundTrip(sell); + EXPECT_EQ(sellCopy.count, legacySell.count); + EXPECT_EQ(sellCopy.itemObjID, legacySell.iObjID); + ExpectTruncatedFails(sell); + + GameMessages::AddDonationItem add; + add.count = static_cast(count); + add.itemObjID = id; + const auto legacyAdd = ReadWithLegacy(add, LegacyGameMessages::ReadAddDonationItem); + const auto addCopy = RoundTrip(add); + EXPECT_EQ(addCopy.count, legacyAdd.count); + EXPECT_EQ(addCopy.itemObjID, legacyAdd.itemId); + ExpectTruncatedFails(add); + } + + // The old handler read nothing, so ConfirmDonationOnPlayer is accepted with or without its vendorID. + GameMessages::ConfirmDonationOnPlayer confirm; + confirm.vendorID = id; + EXPECT_EQ(RoundTrip(confirm).vendorID, id); + RakNet::BitStream empty; + GameMessages::ConfirmDonationOnPlayer fromEmpty; + EXPECT_TRUE(fromEmpty.Deserialize(empty)); + } + + // Messages without a payload. + RakNet::BitStream empty; + EXPECT_TRUE(GameMessages::RequestVendorStatusUpdate().Deserialize(empty)); + EXPECT_TRUE(GameMessages::CancelDonationOnPlayer().Deserialize(empty)); + EXPECT_TRUE(GameMessages::ClientTradeCancel().Deserialize(empty)); +} + +TEST_F(VendorMessagesTests, TradeInboundReadsLikeLegacy) { + for (const auto id : g_Targets) { + for (const bool flag : { false, true }) { + GameMessages::ClientTradeRequest request; + request.bNeedInvitePopUp = flag; + request.i64Invitee = id; + const auto legacyRequest = ReadWithLegacy(request, LegacyGameMessages::ReadClientTradeRequest); + const auto requestCopy = RoundTrip(request); + EXPECT_EQ(requestCopy.bNeedInvitePopUp, legacyRequest.bNeedInvitePopUp); + EXPECT_EQ(requestCopy.i64Invitee, legacyRequest.i64Invitee); + ExpectTruncatedFails(request); + + GameMessages::ClientTradeAccept accept; + accept.bFirst = flag; + EXPECT_EQ(ReadWithLegacy(accept, LegacyGameMessages::ReadClientTradeAccept), flag); + EXPECT_EQ(RoundTrip(accept).bFirst, flag); + ExpectTruncatedFails(accept); + } + } + + // Every optional field of an entry, set and unset, with raw and compressed config blocks. + std::vector entries; + for (uint32_t mask = 0; mask < 64; mask++) { + GameMessages::TradeItemEntry entry; + entry.key = 0x0102030405060708LL + mask; + entry.itemID = 0x1000000000000001LL + mask; + entry.templateID = 1727 + static_cast(mask); + if (mask & 1) entry.subkey = 0x2000000000000002LL; + if (mask & 2) entry.count = 7 + mask; + if (mask & 4) entry.slot = static_cast(3 + mask); + if (mask & 8) entry.inventoryType = 5; + if (mask & 16) { + GameMessages::TradeItemEntry::Config config; + config.compressed = (mask & 32) != 0; + config.data = { 0xde, 0xad, 0xbe, 0xef, static_cast(mask) }; + config.size = config.compressed ? 40 : static_cast(config.data.size()); + config.compressedSize = config.compressed ? static_cast(config.data.size()) : 0; + entry.config = config; + } + entry.unknownFlag = (mask & 32) != 0; + entries.push_back(entry); + } + const std::vector> entrySets = { {}, { entries[0] }, { entries[3], entries[63] }, entries }; + + for (const uint64_t currency : { uint64_t{ 0 }, uint64_t{ 99 }, std::numeric_limits::max() }) { + for (const auto& set : entrySets) { + GameMessages::ClientTradeUpdate update; + update.i64Currency = currency; + update.inventoryMap = set; + const auto legacy = ReadWithLegacy(update, LegacyGameMessages::ReadClientTradeUpdate); + const auto copy = RoundTrip(update); + ASSERT_FALSE(legacy.rejected); + EXPECT_EQ(copy.i64Currency, legacy.currency); + ASSERT_EQ(copy.inventoryMap.size(), legacy.items.size()); + for (size_t i = 0; i < legacy.items.size(); i++) { + const auto& entry = copy.inventoryMap[i]; + // What the handler hands to the trade. + EXPECT_EQ(entry.key, legacy.items[i].itemId); + EXPECT_EQ(entry.templateID, legacy.items[i].itemLot); + EXPECT_EQ(entry.count.value_or(0), legacy.items[i].itemCount); + // What it logs. + EXPECT_EQ(entry.itemID, legacy.logs[i].itemId2); + EXPECT_EQ(entry.subkey.value_or(0), legacy.logs[i].unknown1); + EXPECT_EQ(entry.slot.value_or(0), legacy.logs[i].slot); + EXPECT_EQ(entry.inventoryType.value_or(0), legacy.logs[i].unknown3); + EXPECT_EQ(entry.unknownFlag, legacy.logs[i].unknown4); + } + if (set.size() <= 2) ExpectTruncatedFails(update); + } + } + + // The old handler ignored updates claiming more than MAX_MESSAGE_LENGTH items; the struct refuses them. + RakNet::BitStream huge; + huge.Write(1); + huge.Write(MAX_MESSAGE_LENGTH + 1); + RakNet::BitStream legacyHuge(huge.GetData(), huge.GetNumberOfBytesUsed(), false); + EXPECT_TRUE(LegacyGameMessages::ReadClientTradeUpdate(legacyHuge).rejected); + GameMessages::ClientTradeUpdate hugeUpdate; + EXPECT_FALSE(hugeUpdate.Deserialize(huge)); +} + +// Independent of the legacy code: hand computed payloads (RakNet writes MSB first). +TEST_F(VendorMessagesTests, GoldenBytes) { + GameMessages::BuyFromVendor buy; + buy.confirmed = true; + buy.item = 0x11; + EXPECT_PACKET_EQ(FromHex("84 40 00 00 00", 34), Payload(buy)); + buy.confirmed = false; + buy.count = 5; + EXPECT_PACKET_EQ(FromHex("41 40 00 00 04 40 00 00 00", 66), Payload(buy)); + + GameMessages::SellToVendor sell; + sell.itemObjID = 0x33; + EXPECT_PACKET_EQ(FromHex("19 80 00 00 00 00 00 00 00", 65), Payload(sell)); + + GameMessages::VendorStatusUpdate status; + status.bUpdateOnly = true; + status.inventoryList = { { 1727, 0 }, { 3, -1 } }; + EXPECT_PACKET_EQ(FromHex("81 00 00 00 5f 83 00 00 00 00 00 00 01 80 00 00 7f ff ff ff 80", 161), Payload(status)); + + GameMessages::VendorTransactionResult result; + result.iResult = eVendorTransactionResult::PURCHASE_FAIL; + EXPECT_PACKET_EQ(FromHex("03 00 00 00"), Payload(result)); + + GameMessages::ServerTradeInvite invite; + invite.i64Requestor = 0x11; + invite.wsName = u"A"; + EXPECT_PACKET_EQ(FromHex("08 80 00 00 00 00 00 00 00 80 00 00 20 80 00", 113), Payload(invite)); + + GameMessages::ServerTradeUpdate update; + update.i64Currency = 250; + update.inventoryMap.push_back(EntryFor({ 0x22, 1727, 3 })); + EXPECT_PACKET_EQ(FromHex("7d 00 00 00 00 00 00 00 00 80 00 00 11 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 5f 83 00 00 20 60 00 00 00", 295), Payload(update)); + + GameMessages::ServerTradeAccept accept; + accept.bFirst = true; + EXPECT_PACKET_EQ(FromHex("80", 1), Payload(accept)); +} + +// The whole packet as Send puts it on the wire, including the header, target and message ID. +TEST_F(VendorMessagesTests, GoldenPacket) { + GameMessages::VendorOpenWindow open; + open.target = 0x0102030405060708LL; + // 53 = packet ID, 0005 = CLIENT, 0000000c = GAME_MSG, pad, target, u16 369. + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 71 01"), StructPacket(open)); +}