mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
Converts the vendor, donation vendor and trade game messages to
NetGameMsgs in VendorMessages.{h,cpp} and TradeMessages.{h,cpp}:
VendorOpenWindow and VendorTransactionResult (SendToClient: the old
functions never broadcast), VendorStatusUpdate, ServerTradeInvite,
ServerTradeInitialReply, ServerTradeFinalReply, ServerTradeAccept,
ServerTradeCancel and ServerTradeUpdate, and the received
RequestVendorStatusUpdate, BuyFromVendor, SellToVendor,
BuybackFromVendor, AddDonationItem, RemoveDonationItem,
ConfirmDonationOnPlayer, CancelDonationOnPlayer, ClientTradeRequest,
ClientTradeCancel, ClientTradeAccept and ClientTradeUpdate. A trade
offer entry (the client's inventory item layout, optional fields and
config block included) is TradeItemEntry, shared by both trade updates.
Selling and buying back move into VendorComponent (SellToVendor,
BuybackFromVendor), adding and confirming donations into
DonationVendorComponent, and VendorComponent builds its own status
update and transaction results. Only the VENDOR component sends its
stock, as before. The received messages are registered in
GameMessageHandler's map, the switch cases and the old functions are
deleted. Handler logic is unchanged.
No wire change and no change in recipients. Verified byte for byte
against a frozen verbatim copy of the old functions over an input grid
(to one client and broadcast), received messages compared with the old
handlers' read sequences (every optional field of a trade entry,
raw and compressed config blocks) and truncated payloads rejected, hand
computed golden bytes, round trips, and a deliberate width mutation made
the tests fail. ConfirmDonationOnPlayer still accepts a message without
its vendor ID, since the old handler read nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
13 KiB
C++
318 lines
13 KiB
C++
#include "VendorComponent.h"
|
|
#include "BitStream.h"
|
|
#include "dServer.h"
|
|
#include "dZoneManager.h"
|
|
#include "WorldConfig.h"
|
|
#include "CDComponentsRegistryTable.h"
|
|
#include "CDVendorComponentTable.h"
|
|
#include "CDLootMatrixTable.h"
|
|
#include "CDLootTableTable.h"
|
|
#include "CDItemComponentTable.h"
|
|
#include "InventoryComponent.h"
|
|
#include "Character.h"
|
|
#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;
|
|
m_HasMultiCostItems = false;
|
|
SetupConstants();
|
|
RefreshInventory(true);
|
|
}
|
|
|
|
void VendorComponent::Serialize(RakNet::BitStream& outBitStream, bool bIsInitialUpdate) {
|
|
outBitStream.Write(bIsInitialUpdate || m_DirtyVendor);
|
|
if (bIsInitialUpdate || m_DirtyVendor) {
|
|
outBitStream.Write(m_HasStandardCostItems);
|
|
outBitStream.Write(m_HasMultiCostItems);
|
|
if (!bIsInitialUpdate) m_DirtyVendor = false;
|
|
}
|
|
}
|
|
|
|
void VendorComponent::OnUse(Entity* originator) {
|
|
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<VendorComponent*>(m_Parent->GetComponent(eReplicaComponentType::VENDOR));
|
|
if (vendor) vendor->SendStatusUpdate(originator->GetSystemAddress());
|
|
}
|
|
|
|
void VendorComponent::RefreshInventory(bool isCreation) {
|
|
SetHasStandardCostItems(false);
|
|
SetHasMultiCostItems(false);
|
|
m_Inventory.clear();
|
|
|
|
// Custom code for Vanity Vendor Invetory Override
|
|
if(m_Parent->HasVar(u"vendorInvOverride")) {
|
|
std::vector<std::string> items = GeneralUtils::SplitString(m_Parent->GetVarAsString(u"vendorInvOverride"), ',');
|
|
uint32_t sortPriority = -1;
|
|
for (auto& itemString : items) {
|
|
itemString.erase(remove_if(itemString.begin(), itemString.end(), isspace), itemString.end());
|
|
auto item = GeneralUtils::TryParse<uint32_t>(itemString);
|
|
if (!item) continue;
|
|
if (SetupItem(item.value())) {
|
|
sortPriority++;
|
|
m_Inventory.push_back(SoldItem(item.value(), sortPriority));
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto* lootMatrixTable = CDClientManager::GetTable<CDLootMatrixTable>();
|
|
const auto& lootMatrices = lootMatrixTable->GetMatrix(m_LootMatrixID);
|
|
|
|
if (lootMatrices.empty()) return;
|
|
|
|
auto* lootTableTable = CDClientManager::GetTable<CDLootTableTable>();
|
|
|
|
for (const auto& lootMatrix : lootMatrices) {
|
|
auto vendorItems = lootTableTable->GetTable(lootMatrix.LootTableIndex);
|
|
if (lootMatrix.maxToDrop == 0 || lootMatrix.minToDrop == 0) {
|
|
for (const auto& item : vendorItems) {
|
|
if (SetupItem(item.itemid)) m_Inventory.push_back(SoldItem(item.itemid, item.sortPriority));
|
|
}
|
|
} else {
|
|
auto randomCount = GeneralUtils::GenerateRandomNumber<int32_t>(lootMatrix.minToDrop, lootMatrix.maxToDrop);
|
|
|
|
for (size_t i = 0; i < randomCount; i++) {
|
|
if (vendorItems.empty()) break;
|
|
auto randomItemIndex = GeneralUtils::GenerateRandomNumber<int32_t>(0, vendorItems.size() - 1);
|
|
const auto& randomItem = vendorItems.at(randomItemIndex);
|
|
if (SetupItem(randomItem.itemid)) m_Inventory.push_back(SoldItem(randomItem.itemid, randomItem.sortPriority));
|
|
vendorItems.erase(vendorItems.begin() + randomItemIndex);
|
|
}
|
|
}
|
|
}
|
|
HandleMrReeCameras();
|
|
|
|
// Callback timer to refresh this inventory.
|
|
if (m_RefreshTimeSeconds > 0.0) {
|
|
m_Parent->AddCallbackTimer(m_RefreshTimeSeconds, [this]() {
|
|
RefreshInventory();
|
|
});
|
|
}
|
|
Game::entityManager->SerializeEntity(m_Parent);
|
|
// Only the entity's VENDOR component sends its stock (achievement and donation vendors never did).
|
|
auto* vendor = static_cast<VendorComponent*>(m_Parent->GetComponent(eReplicaComponentType::VENDOR));
|
|
if (vendor) vendor->SendStatusUpdate(UNASSIGNED_SYSTEM_ADDRESS);
|
|
}
|
|
|
|
void VendorComponent::SetupConstants() {
|
|
auto* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
int componentID = compRegistryTable->GetByIDAndType(m_Parent->GetLOT(), eReplicaComponentType::VENDOR);
|
|
|
|
auto* vendorComponentTable = CDClientManager::GetTable<CDVendorComponentTable>();
|
|
std::vector<CDVendorComponent> vendorComps = vendorComponentTable->Query([=](CDVendorComponent entry) { return (entry.id == componentID); });
|
|
if (vendorComps.empty()) return;
|
|
auto vendorData = vendorComps.at(0);
|
|
if (vendorData.buyScalar == 0.0) m_BuyScalar = Game::zoneManager->GetWorldConfig().vendorBuyMultiplier;
|
|
else m_BuyScalar = vendorData.buyScalar;
|
|
m_SellScalar = vendorData.sellScalar;
|
|
m_RefreshTimeSeconds = vendorData.refreshTimeSeconds;
|
|
m_LootMatrixID = vendorData.LootMatrixIndex;
|
|
}
|
|
|
|
bool VendorComponent::SellsItem(const LOT item) const {
|
|
return std::count_if(m_Inventory.begin(), m_Inventory.end(), [item](const SoldItem& lhs) {
|
|
return lhs.lot == item;
|
|
}) > 0;
|
|
}
|
|
|
|
void VendorComponent::HandleMrReeCameras(){
|
|
if (m_Parent->GetLOT() == 13569) {
|
|
SetHasStandardCostItems(true);
|
|
auto randomCamera = GeneralUtils::GenerateRandomNumber<int32_t>(0, 2);
|
|
|
|
LOT camera = 0;
|
|
DluAssert(randomCamera >= 0 && randomCamera <= 2);
|
|
switch (randomCamera) {
|
|
case 0:
|
|
camera = 16253; // Grungagroid
|
|
break;
|
|
case 1:
|
|
camera = 16254; // Hipstabrick
|
|
break;
|
|
case 2:
|
|
camera = 16204; // Megabrixel snapshot
|
|
break;
|
|
}
|
|
m_Inventory.push_back(SoldItem(camera, 0));
|
|
}
|
|
}
|
|
|
|
|
|
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());
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
|
|
return;
|
|
}
|
|
|
|
auto* inventoryComponent = buyer->GetComponent<InventoryComponent>();
|
|
if (!inventoryComponent) {
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
|
|
return;
|
|
}
|
|
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
|
int itemCompID = compRegistryTable->GetByIDAndType(lot, eReplicaComponentType::ITEM);
|
|
CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID);
|
|
|
|
// Extra currency that needs to be deducted in case of crafting
|
|
auto craftingCurrencies = CDItemComponentTable::ParseCraftingCurrencies(itemComp);
|
|
for (const auto& [crafintCurrencyLOT, crafintCurrencyCount]: craftingCurrencies) {
|
|
if (inventoryComponent->GetLotCount(crafintCurrencyLOT) < (crafintCurrencyCount * count)) {
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool success = true;
|
|
for (const auto& [crafintCurrencyLOT, crafintCurrencyCount]: craftingCurrencies) {
|
|
success = inventoryComponent->RemoveItem(crafintCurrencyLOT, crafintCurrencyCount * count, eInventoryType::ALL);
|
|
if (!success) break;
|
|
}
|
|
|
|
if (!success) {
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
|
|
return;
|
|
}
|
|
|
|
float buyScalar = GetBuyScalar();
|
|
const auto coinCost = static_cast<uint32_t>(std::floor((itemComp.baseValue * buyScalar) * count));
|
|
|
|
Character* character = buyer->GetCharacter();
|
|
if (!character || character->GetCoins() < coinCost) {
|
|
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) {
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
|
|
return;
|
|
}
|
|
inventoryComponent->RemoveItem(itemComp.currencyLOT, altCurrencyCost, eInventoryType::ALL);
|
|
}
|
|
|
|
character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR);
|
|
inventoryComponent->AddItem(lot, count, eLootSourceType::VENDOR);
|
|
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_SUCCESS);
|
|
}
|
|
|
|
bool VendorComponent::SetupItem(LOT item) {
|
|
|
|
auto* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
|
auto* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
|
|
auto itemComponentID = compRegistryTable->GetByIDAndType(item, eReplicaComponentType::ITEM, -1);
|
|
if (itemComponentID == -1) {
|
|
LOG("Attempted to add item %i with ItemComponent ID -1 to vendor %i inventory. Not adding item!", itemComponentID, m_Parent->GetLOT());
|
|
return false;
|
|
}
|
|
|
|
if (!m_HasStandardCostItems || !m_HasMultiCostItems) {
|
|
auto itemComponent = itemComponentTable->GetItemComponentByID(itemComponentID);
|
|
if (!m_HasStandardCostItems && itemComponent.baseValue != -1) SetHasStandardCostItems(true);
|
|
if (!m_HasMultiCostItems && !itemComponent.currencyCosts.empty()) SetHasMultiCostItems(true);
|
|
}
|
|
|
|
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<InventoryComponent*>(player.GetComponent(eReplicaComponentType::INVENTORY));
|
|
if (!inv) return;
|
|
|
|
Item* item = inv->FindItemById(itemObjID);
|
|
if (!item) return;
|
|
|
|
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
|
|
|
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<uint32_t>(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<uint32_t>(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<InventoryComponent*>(player.GetComponent(eReplicaComponentType::INVENTORY));
|
|
if (!inv) return;
|
|
|
|
Item* item = inv->FindItemById(itemObjID);
|
|
if (!item) return;
|
|
|
|
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
|
|
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
|
|
|
|
int itemCompID = compRegistryTable->GetByIDAndType(item->GetLot(), eReplicaComponentType::ITEM);
|
|
CDItemComponent itemComp = itemComponentTable->GetItemComponentByID(itemCompID);
|
|
|
|
float sellScalar = GetSellScalar();
|
|
|
|
const auto cost = static_cast<uint32_t>(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);
|
|
}
|