mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
Live filled SetCurrency's source fields per source type (16,816 decoded live SetCurrency): pickups carry the position the client picked the coins up at (the one in its PickupCurrency) and no source; mission, achievement and activity rewards name the player (object and LOT 1); vendor buys, sells and buybacks name the vendor and its LOT; trades carry the trade ID; death, mail and everything else name nothing. No live SetCurrency sets loot_type. DLU wrote a present source LOT of 0, no object and a zero position for all of them. The position is the one the client uses: for pickups it projects it to the screen as the start of the coin counter animation (LWOCharacterComponent::SetCurrency), so it started from the world origin before. PickupCurrency now reads that position (the client always sends it). The trade ID is an 8-byte object ID in the client; DLU declared it 4 bytes, which only never broke because it was never set. Check: pick up coins and watch the coin counter animate from where the coins were; buy and sell at a vendor, finish a mission and trade coins with another player, and the coin total updates each time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
352 lines
15 KiB
C++
352 lines
15 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"
|
|
#include "EconomyLedger.h"
|
|
|
|
#include <algorithm>
|
|
#include <ranges>
|
|
|
|
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, CoinSource::Object(*m_Parent));
|
|
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.
|
|
}
|
|
|
|
MakeRoomInBuyback(*inv, *item, static_cast<uint32_t>(std::max(count, 0)));
|
|
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, CoinSource::Object(*m_Parent));
|
|
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, CoinSource::Object(*m_Parent));
|
|
//Game::entityManager->SerializeEntity(player); // so inventory updates
|
|
SendTransactionResult(m_Parent->GetObjectID(), sysAddr, eVendorTransactionResult::PURCHASE_SUCCESS);
|
|
}
|
|
|
|
void VendorComponent::MakeRoomInBuyback(InventoryComponent& inventory, const Item& sold, const uint32_t count) {
|
|
auto* const buyback = inventory.GetInventory(eInventoryType::VENDOR_BUYBACK);
|
|
if (!buyback) return;
|
|
|
|
// No new slot is needed when all of it fits on buyback stacks of the same item
|
|
const auto lot = sold.GetLot();
|
|
const auto stackSize = sold.GetInfo().stackSize;
|
|
const bool mergeable = sold.GetSubKey() == LWOOBJID_EMPTY && sold.GetConfig().values.empty() && !InventoryComponent::IsUniqueLot(lot);
|
|
if (mergeable) {
|
|
uint64_t room = 0;
|
|
for (const auto* const item : buyback->GetItems() | std::views::values) {
|
|
if (item->GetLot() == lot && item->GetCount() < stackSize) room += stackSize - item->GetCount();
|
|
}
|
|
if (room >= count) return;
|
|
}
|
|
|
|
// Live kept 27 items (the buyback window's size) and dropped the oldest when a 28th was sold: a 2014 capture shows
|
|
// RemoveItemFromInventory for the first item sold, in the buyback inventory, just before the 28th sale's item was
|
|
// added. Each item gets a new, higher object ID when it is sold, so the oldest is the one with the lowest ID.
|
|
// The ledger counted these items as gone when they were sold
|
|
EconomyLedger::ScopedItemTransfer alreadyCounted;
|
|
while (buyback->GetItems().size() >= BUYBACK_SIZE) {
|
|
const auto items = buyback->GetItems().size();
|
|
const auto oldest = std::ranges::min_element(buyback->GetItems(), {}, [](const auto& entry) { return entry.first; });
|
|
oldest->second->SetCount(0, false, false, false, eLootSourceType::VENDOR);
|
|
if (buyback->GetItems().size() >= items) break; // not removed (an empty stack)
|
|
}
|
|
}
|