fix: limit the count of a vendor purchase and work its price out in 64 bits

A purchase took any count, and the coin, alternate currency and
commendation prices were worked out as 32 bit numbers that wrap for a
large count. A purchase is now for 1 to 1000 items and the prices are
64 bit numbers.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 13:01:17 -06:00
parent 2d24608186
commit 9ffed42772
2 changed files with 23 additions and 8 deletions

View File

@@ -6,6 +6,9 @@
#include "CDItemComponentTable.h"
#include "eVendorTransactionResult.h"
#include "CheatDetection.h"
#include "Logger.h"
#include "Game.h"
#include "VendorPricing.h"
#include "UserManager.h"
#include "CDMissionsTable.h"
#include "Entity.h"
@@ -41,6 +44,12 @@ bool AchievementVendorComponent::SellsItem(Entity* buyer, const LOT lot) {
}
void AchievementVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
if (!VendorPricing::IsValidBuyCount(count)) {
LOG("Player %llu tried to buy %u of item %i from vendor %i", buyer->GetObjectID(), count, lot, m_Parent->GetLOT());
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
return;
}
// get the item Comp from the item LOT
CDComponentsRegistryTable* compRegistryTable = CDClientManager::GetTable<CDComponentsRegistryTable>();
CDItemComponentTable* itemComponentTable = CDClientManager::GetTable<CDItemComponentTable>();
@@ -71,8 +80,8 @@ void AchievementVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
if (missionComponent->GetMissionState(578) == eMissionState::COMPLETE) costLOT = 8320; // "Paradox Token"
}
const uint32_t altCurrencyCost = itemComp.commendationCost * count;
if (inventoryComponent->GetLotCount(costLOT) < altCurrencyCost || !inventoryComponent->RemoveItem(costLOT, altCurrencyCost, eInventoryType::ALL)) {
const uint64_t altCurrencyCost = static_cast<uint64_t>(itemComp.commendationCost) * count;
if (inventoryComponent->GetLotCount(costLOT) < altCurrencyCost || !inventoryComponent->RemoveItem(costLOT, static_cast<uint32_t>(altCurrencyCost), eInventoryType::ALL)) {
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
return;
}

View File

@@ -162,6 +162,12 @@ void BaseVendorComponent::HandleMrReeCameras(){
void BaseVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
// A count the client did not send as a positive number arrives here as a very large one
if (!VendorPricing::IsValidBuyCount(count)) {
LOG("Player %llu tried to buy %u of item %i from vendor %i", buyer->GetObjectID(), count, lot, m_Parent->GetLOT());
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
return;
}
if (!SellsItem(lot)) {
auto* user = UserManager::Instance()->GetUser(buyer->GetSystemAddress());
@@ -183,7 +189,7 @@ void BaseVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
// 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)) {
if (inventoryComponent->GetLotCount(crafintCurrencyLOT) < (static_cast<uint64_t>(crafintCurrencyCount) * count)) {
SendTransactionResult(buyer->GetObjectID(), buyer->GetSystemAddress(), eVendorTransactionResult::PURCHASE_FAIL);
return;
}
@@ -201,24 +207,24 @@ void BaseVendorComponent::Buy(Entity* buyer, LOT lot, uint32_t count) {
}
float buyScalar = GetBuyScalar();
const auto coinCost = static_cast<uint32_t>(std::floor((itemComp.baseValue * buyScalar) * count));
const uint64_t coinCost = VendorPricing::GetPriceTotal(itemComp.baseValue, buyScalar, count);
Character* character = buyer->GetCharacter();
if (!character || character->GetCoins() < coinCost) {
if (!character || character->GetCoins() < 0 || static_cast<uint64_t>(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;
const uint64_t altCurrencyCost = VendorPricing::GetPricePerItem(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);
inventoryComponent->RemoveItem(itemComp.currencyLOT, static_cast<uint32_t>(altCurrencyCost), eInventoryType::ALL);
}
character->SetCoins(character->GetCoins() - (coinCost), eLootSourceType::VENDOR, CoinSource::Object(*m_Parent));
character->SetCoins(character->GetCoins() - static_cast<int64_t>(coinCost), eLootSourceType::VENDOR, CoinSource::Object(*m_Parent));
// Live sent the vendor's LOT, and the coins paid when there were any, with the bought items
LootMetrics metrics{};
metrics.sourceLot = m_Parent->GetLOT();