eqemu-server/zone/trading.cpp
Mitch Freeman 345d452a7e Remove FindTraderItemSerialNumber and FIndTraderItemBySerialNumber as they are no longer used.
Updated sharedbank to store unique_item_id instead of guid.
2025-09-21 19:14:16 -03:00

3794 lines
116 KiB
C++

/* EQEMu: Everquest Server Emulator
Copyright (C) 2001-2002 EQEMu Development Team (http://eqemu.org)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY except by those people which sell it, which
are required to give you total support for your newly bought product;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "../common/global_define.h"
#include "../common/eqemu_logsys.h"
#include "../common/rulesys.h"
#include "../common/strings.h"
#include "../common/eq_packet_structs.h"
#include "../common/misc_functions.h"
#include "../common/events/player_event_logs.h"
#include "../common/repositories/trader_repository.h"
#include "../common/repositories/buyer_repository.h"
#include "../common/repositories/buyer_buy_lines_repository.h"
#include "../common/repositories/character_offline_transactions_repository.h"
#include "../common/repositories/account_repository.h"
#include "client.h"
#include "entity.h"
#include "mob.h"
#include "quest_parser_collection.h"
#include "string_ids.h"
#include "worldserver.h"
#include "../common/bazaar.h"
#include <numeric>
class QueryServ;
extern WorldServer worldserver;
extern QueryServ* QServ;
// The maximum amount of a single bazaar/barter transaction expressed in copper.
// Equivalent to 2 Million plat
constexpr auto MAX_TRANSACTION_VALUE = 2000000000;
// ##########################################
// Trade implementation
// ##########################################
Trade::Trade(Mob* in_owner)
{
owner = in_owner;
Reset();
}
Trade::~Trade()
{
Reset();
}
void Trade::Reset()
{
state = TradeNone;
with_id = 0;
pp=0; gp=0; sp=0; cp=0;
}
// Initiate a trade with another mob
// initiate_with specifies whether to start trade with other mob as well
void Trade::Start(uint32 mob_id, bool initiate_with)
{
Reset();
state = Trading;
with_id = mob_id;
// Autostart on other mob?
if (initiate_with) {
Mob *with = With();
if (with) {
with->trade->Start(owner->GetID(), false);
}
}
}
// Add item from a given slot to trade bucket (automatically does bag data too)
void Trade::AddEntity(uint16 trade_slot_id, uint32 stack_size) {
// TODO: review for inventory saves / consider changing return type to bool so failure can be passed to desync handler
if (!owner || !owner->IsClient()) {
// This should never happen
LogDebug("Programming error: NPC's should not call Trade::AddEntity()");
return;
}
// If one party accepted the trade then an item was added, their state needs to be reset
owner->trade->state = Trading;
Mob* with = With();
if (with)
with->trade->state = Trading;
// Item always goes into trade bucket from cursor
Client* client = owner->CastToClient();
EQ::ItemInstance* inst = client->GetInv().GetItem(EQ::invslot::slotCursor);
if (!inst) {
client->Message(Chat::Red, "Error: Could not find item on your cursor!");
return;
}
EQ::ItemInstance* inst2 = client->GetInv().GetItem(trade_slot_id);
// it looks like the original code attempted to allow stacking...
// (it just didn't handle partial stack move actions)
if (stack_size > 0) {
if (!inst->IsStackable() || !inst2 || !inst2->GetItem() || (inst->GetID() != inst2->GetID()) || (stack_size > inst->GetCharges())) {
client->Kick("Error stacking item in trade");
return;
}
uint32 _stack_size = 0;
if ((stack_size + inst2->GetCharges()) > inst2->GetItem()->StackSize) {
_stack_size = (stack_size + inst2->GetCharges()) - inst->GetItem()->StackSize;
inst2->SetCharges(inst2->GetItem()->StackSize);
}
else {
_stack_size = inst->GetCharges() - stack_size;
inst2->SetCharges(stack_size + inst2->GetCharges());
}
LogTrading("[{}] added partial item [{}] stack (qty: [{}]) to trade slot [{}]", owner->GetName(), inst->GetItem()->Name, stack_size, trade_slot_id);
if (_stack_size > 0)
inst->SetCharges(_stack_size);
else
client->DeleteItemInInventory(EQ::invslot::slotCursor);
SendItemData(inst2, trade_slot_id);
}
else {
if (inst2 && inst2->GetID()) {
client->Kick("Attempting to add null item to trade");
return;
}
SendItemData(inst, trade_slot_id);
LogTrading("[{}] added item [{}] to trade slot [{}]", owner->GetName(), inst->GetItem()->Name, trade_slot_id);
client->PutItemInInventory(trade_slot_id, *inst);
client->DeleteItemInInventory(EQ::invslot::slotCursor);
}
}
// Retrieve mob the owner is trading with
// Done like this in case 'with' mob goes LD and Mob* becomes invalid
Mob* Trade::With()
{
return entity_list.GetMob(with_id);
}
// Private Method: Send item data for trade item to other person involved in trade
void Trade::SendItemData(const EQ::ItemInstance* inst, int16 dest_slot_id)
{
if (inst == nullptr)
return;
// @merth: This needs to be redone with new item classes
Mob* mob = With();
if (!mob->IsClient())
return; // Not sending packets to NPCs!
Client* with = mob->CastToClient();
Client* trader = owner->CastToClient();
if (with && with->IsClient()) {
with->SendItemPacket(dest_slot_id - EQ::invslot::TRADE_BEGIN, inst, ItemPacketTradeView);
if (inst->GetItem()->ItemClass == 1) {
for (uint16 i = EQ::invbag::SLOT_BEGIN; i <= EQ::invbag::SLOT_END; i++) {
uint16 bagslot_id = EQ::InventoryProfile::CalcSlotId(dest_slot_id, i);
const EQ::ItemInstance* bagitem = trader->GetInv().GetItem(bagslot_id);
if (bagitem) {
with->SendItemPacket(bagslot_id - EQ::invslot::TRADE_BEGIN, bagitem, ItemPacketTradeView);
}
}
}
//safe_delete(outapp);
}
}
Mob *Trade::GetOwner() const
{
return owner;
}
void Client::ResetTrade() {
AddMoneyToPP(trade->cp, trade->sp, trade->gp, trade->pp, true);
// step 1: process bags
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
const EQ::ItemInstance* inst = m_inv[trade_slot];
if (inst && inst->IsClassBag()) {
int16 free_slot = m_inv.FindFreeSlotForTradeItem(inst);
if (free_slot != INVALID_INDEX) {
PutItemInInventory(free_slot, *inst);
SendItemPacket(free_slot, inst, ItemPacketTrade);
}
else {
DropInst(inst);
}
DeleteItemInInventory(trade_slot);
}
}
// step 2a: process stackables
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
EQ::ItemInstance* inst = GetInv().GetItem(trade_slot);
if (inst && inst->IsStackable()) {
while (true) {
// there's no built-in safety check against an infinite loop..but, it should break on one of the conditional checks
int16 free_slot = m_inv.FindFreeSlotForTradeItem(inst);
if ((free_slot == EQ::invslot::slotCursor) || (free_slot == INVALID_INDEX))
break;
EQ::ItemInstance* partial_inst = GetInv().GetItem(free_slot);
if (!partial_inst)
break;
if (partial_inst->GetID() != inst->GetID()) {
LogDebug("[CLIENT] Client::ResetTrade() - an incompatible location reference was returned by Inventory::FindFreeSlotForTradeItem()");
break;
}
if ((partial_inst->GetCharges() + inst->GetCharges()) > partial_inst->GetItem()->StackSize) {
int16 new_charges = (partial_inst->GetCharges() + inst->GetCharges()) - partial_inst->GetItem()->StackSize;
partial_inst->SetCharges(partial_inst->GetItem()->StackSize);
inst->SetCharges(new_charges);
}
else {
partial_inst->SetCharges(partial_inst->GetCharges() + inst->GetCharges());
inst->SetCharges(0);
}
PutItemInInventory(free_slot, *partial_inst);
SendItemPacket(free_slot, partial_inst, ItemPacketTrade);
if (inst->GetCharges() == 0) {
DeleteItemInInventory(trade_slot);
break;
}
}
}
}
// step 2b: adjust trade stack bias
// (if any partial stacks exist before the final stack, FindFreeSlotForTradeItem() will return that slot in step 3 and an overwrite will occur)
for (int16 trade_slot = EQ::invslot::TRADE_END; trade_slot >= EQ::invslot::TRADE_BEGIN; --trade_slot) {
EQ::ItemInstance* inst = GetInv().GetItem(trade_slot);
if (inst && inst->IsStackable()) {
for (int16 bias_slot = EQ::invslot::TRADE_BEGIN; bias_slot <= EQ::invslot::TRADE_END; ++bias_slot) {
if (bias_slot >= trade_slot)
break;
EQ::ItemInstance* bias_inst = GetInv().GetItem(bias_slot);
if (!bias_inst || (bias_inst->GetID() != inst->GetID()) || (bias_inst->GetCharges() >= bias_inst->GetItem()->StackSize))
continue;
if ((bias_inst->GetCharges() + inst->GetCharges()) > bias_inst->GetItem()->StackSize) {
int16 new_charges = (bias_inst->GetCharges() + inst->GetCharges()) - bias_inst->GetItem()->StackSize;
bias_inst->SetCharges(bias_inst->GetItem()->StackSize);
inst->SetCharges(new_charges);
}
else {
bias_inst->SetCharges(bias_inst->GetCharges() + inst->GetCharges());
inst->SetCharges(0);
}
if (inst->GetCharges() == 0) {
DeleteItemInInventory(trade_slot);
break;
}
}
}
}
// step 3: process everything else
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
const EQ::ItemInstance* inst = m_inv[trade_slot];
if (inst) {
int16 free_slot = m_inv.FindFreeSlotForTradeItem(inst);
if (free_slot != INVALID_INDEX) {
PutItemInInventory(free_slot, *inst);
SendItemPacket(free_slot, inst, ItemPacketTrade);
}
else {
DropInst(inst);
}
DeleteItemInInventory(trade_slot);
}
}
}
void Client::FinishTrade(Mob* tradingWith, bool finalizer, void* event_entry, std::list<void*>* event_details) {
if (!tradingWith) {
return;
}
if (tradingWith->IsClient()) {
Client * other = tradingWith->CastToClient();
if(other) {
LogTrading("Finishing trade with client [{}]", other->GetName());
AddMoneyToPP(other->trade->cp, other->trade->sp, other->trade->gp, other->trade->pp, true);
// step 1: process bags
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
const EQ::ItemInstance* inst = m_inv[trade_slot];
if (inst && inst->IsClassBag()) {
LogTrading("Giving container [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName());
// TODO: need to check bag items/augments for no drop..everything for attuned...
if (
inst->GetItem()->NoDrop != 0 ||
CanTradeFVNoDropItem() ||
other == this
) {
int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst);
if (free_slot != INVALID_INDEX) {
if (other->PutItemInInventory(free_slot, *inst, true)) {
inst->TransferOwnership(database, other->CharacterID());
LogTrading("Container [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID);
}
else {
LogTrading("Transfer of container [{}] ([{}]) to [{}] failed, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID, other->GetName());
PushItemOnCursor(*inst, true);
}
}
else {
LogTrading("[{}]'s inventory is full, returning container [{}] ([{}]) to giver", other->GetName(), inst->GetItem()->Name, inst->GetItem()->ID);
PushItemOnCursor(*inst, true);
}
}
else {
LogTrading("Container [{}] ([{}]) is NoDrop, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID);
PushItemOnCursor(*inst, true);
}
DeleteItemInInventory(trade_slot);
}
}
// step 2a: process stackables
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
EQ::ItemInstance* inst = GetInv().GetItem(trade_slot);
if (inst && inst->IsStackable()) {
while (true) {
// there's no built-in safety check against an infinite loop..but, it should break on one of the conditional checks
int16 partial_slot = other->GetInv().FindFreeSlotForTradeItem(inst);
if ((partial_slot == EQ::invslot::slotCursor) || (partial_slot == INVALID_INDEX))
break;
EQ::ItemInstance* partial_inst = other->GetInv().GetItem(partial_slot);
if (!partial_inst)
break;
if (partial_inst->GetID() != inst->GetID()) {
LogTrading("[CLIENT] Client::ResetTrade() - an incompatible location reference was returned by Inventory::FindFreeSlotForTradeItem()");
break;
}
int16 old_charges = inst->GetCharges();
int16 partial_charges = partial_inst->GetCharges();
if ((partial_inst->GetCharges() + inst->GetCharges()) > partial_inst->GetItem()->StackSize) {
int16 new_charges = (partial_inst->GetCharges() + inst->GetCharges()) - partial_inst->GetItem()->StackSize;
partial_inst->SetCharges(partial_inst->GetItem()->StackSize);
inst->SetCharges(new_charges);
}
else {
partial_inst->SetCharges(partial_inst->GetCharges() + inst->GetCharges());
inst->SetCharges(0);
}
LogTrading("Transferring partial stack [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName());
if (other->PutItemInInventory(partial_slot, *partial_inst, true)) {
LogTrading(
"Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot",
inst->GetItem()->Name,
inst->GetItem()->ID,
(old_charges - inst->GetCharges())
);
inst->TransferOwnership(database, other->CharacterID());
}
else {
LogTrading("Transfer of partial stack [{}] ([{}]) to [{}] failed, returning [{}] charges to trade slot",
inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges()));
inst->SetCharges(old_charges);
partial_inst->SetCharges(partial_charges);
break;
}
if (inst->GetCharges() == 0) {
DeleteItemInInventory(trade_slot);
break;
}
}
}
}
// step 2b: adjust trade stack bias
// (if any partial stacks exist before the final stack, FindFreeSlotForTradeItem() will return that slot in step 3 and an overwrite will occur)
for (int16 trade_slot = EQ::invslot::TRADE_END; trade_slot >= EQ::invslot::TRADE_BEGIN; --trade_slot) {
EQ::ItemInstance* inst = GetInv().GetItem(trade_slot);
if (inst && inst->IsStackable()) {
for (int16 bias_slot = EQ::invslot::TRADE_BEGIN; bias_slot <= EQ::invslot::TRADE_END; ++bias_slot) {
if (bias_slot >= trade_slot)
break;
EQ::ItemInstance* bias_inst = GetInv().GetItem(bias_slot);
if (!bias_inst || (bias_inst->GetID() != inst->GetID()) || (bias_inst->GetCharges() >= bias_inst->GetItem()->StackSize))
continue;
int16 old_charges = inst->GetCharges();
if ((bias_inst->GetCharges() + inst->GetCharges()) > bias_inst->GetItem()->StackSize) {
int16 new_charges = (bias_inst->GetCharges() + inst->GetCharges()) - bias_inst->GetItem()->StackSize;
bias_inst->SetCharges(bias_inst->GetItem()->StackSize);
inst->SetCharges(new_charges);
}
else {
bias_inst->SetCharges(bias_inst->GetCharges() + inst->GetCharges());
inst->SetCharges(0);
}
if (inst->GetCharges() == 0) {
DeleteItemInInventory(trade_slot);
break;
}
}
}
}
// step 3: process everything else
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) {
const EQ::ItemInstance* inst = m_inv[trade_slot];
if (inst) {
LogTrading("Giving item [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName());
// TODO: need to check bag items/augments for no drop..everything for attuned...
if (inst->GetItem()->NoDrop != 0 || CanTradeFVNoDropItem() || other == this) {
int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst);
if (free_slot != INVALID_INDEX) {
if (other->PutItemInInventory(free_slot, *inst, true)) {
inst->TransferOwnership(database, other->CharacterID());
LogTrading("Item [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID);
}
else {
LogTrading("Transfer of Item [{}] ([{}]) to [{}] failed, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID, other->GetName());
PushItemOnCursor(*inst, true);
}
}
else {
LogTrading("[{}]'s inventory is full, returning item [{}] ([{}]) to giver", other->GetName(), inst->GetItem()->Name, inst->GetItem()->ID);
PushItemOnCursor(*inst, true);
}
}
else {
LogTrading("Item [{}] ([{}]) is NoDrop, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID);
PushItemOnCursor(*inst, true);
}
DeleteItemInInventory(trade_slot);
}
}
//Do not reset the trade here, done by the caller.
}
}
else if(tradingWith->IsNPC()) {
bool quest_npc = false;
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
quest_npc = true;
}
// take ownership of all trade slot items
EQ::ItemInstance* insts[4] = { 0 };
for (int i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; ++i) {
insts[i - EQ::invslot::TRADE_BEGIN] = m_inv.PopItem(i);
database.SaveInventory(CharacterID(), nullptr, i);
}
// copy to be filtered by task updates, null trade slots preserved for quest event arg
std::vector<EQ::ItemInstance*> items(insts, insts + std::size(insts));
if (RuleB(TaskSystem, EnableTaskSystem)) {
if (UpdateTasksOnDeliver(items, *trade, tradingWith->CastToNPC())) {
if (!tradingWith->IsMoving()) {
tradingWith->FaceTarget(this);
}
}
}
if (!quest_npc) {
for (auto &inst: items) {
if (!inst || !inst->GetItem()) {
continue;
}
// remove delivered task items
if (RuleB(TaskSystem, EnableTaskSystem) && inst->GetTaskDeliveredCount() > 0) {
int remaining = inst->RemoveTaskDeliveredItems();
if (remaining <= 0) {
inst = nullptr;
continue; // all items in trade slot consumed by task update
}
}
auto with = tradingWith->CastToNPC();
const EQ::ItemData *item = inst->GetItem();
const bool is_pet = with->IsPetOwnerOfClientBot() || with->IsCharmedPet();
if (is_pet && with->CanPetTakeItem(inst)) {
// pets need to look inside bags and try to equip items found there
if (item->IsClassBag() && item->BagSlots > 0) {
// if an item inside the bag can't be given to the pet, keep the bag
bool keep_bag = false;
int item_count = 0;
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
const EQ::ItemInstance *baginst = inst->GetItem(bslot);
if (baginst && baginst->GetItem() && with->CanPetTakeItem(baginst)) {
// add item to pet's inventory
auto lde = LootdropEntriesRepository::NewNpcEntity();
lde.equip_item = 1;
lde.item_charges = static_cast<int8>(baginst->GetCharges());
with->AddLootDrop(baginst->GetItem(), lde, true);
inst->DeleteItem(bslot);
item_count++;
}
else {
keep_bag = true;
}
}
// add item to pet's inventory
if (!keep_bag || item_count == 0) {
auto lde = LootdropEntriesRepository::NewNpcEntity();
lde.equip_item = 1;
lde.item_charges = static_cast<int8>(inst->GetCharges());
with->AddLootDrop(item, lde, true);
inst = nullptr;
}
}
else {
// add item to pet's inventory
auto lde = LootdropEntriesRepository::NewNpcEntity();
lde.equip_item = 1;
lde.item_charges = static_cast<int8>(inst->GetCharges());
with->AddLootDrop(item, lde, true);
inst = nullptr;
}
}
}
}
std::string currencies[] = {"copper", "silver", "gold", "platinum"};
int32 amounts[] = {trade->cp, trade->sp, trade->gp, trade->pp};
for (int i = 0; i < 4; ++i) {
parse->AddVar(
fmt::format("{}.{}", currencies[i], tradingWith->GetNPCTypeID()),
fmt::format("{}", amounts[i])
);
}
if (tradingWith->GetAppearance() != eaDead) {
tradingWith->FaceTarget(this);
}
// we cast to any to pass through the quest event system
std::vector<std::any> item_list(items.begin(), items.end());
for (EQ::ItemInstance *inst: items) {
if (!inst || !inst->GetItem()) {
continue;
}
item_list.emplace_back(inst);
}
auto handin_npc = tradingWith->CastToNPC();
m_external_handin_money_returned = {};
m_external_handin_items_returned = {};
bool has_aggro = tradingWith->CheckAggro(this);
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE) && !has_aggro) {
// This CheckHandin call enables eq.handin and quest::handin to recognize the hand-in context.
// It initializes the first hand-in bucket, which is then reused for the EVENT_TRADE subroutine.
std::map<std::string, uint32> handin = {
{"copper", trade->cp},
{"silver", trade->sp},
{"gold", trade->gp},
{"platinum", trade->pp}
};
for (EQ::ItemInstance *inst: items) {
if (!inst || !inst->GetItem()) {
continue;
}
std::string item_id = fmt::format("{}", inst->GetItem()->ID);
handin[item_id] += inst->GetCharges();
}
handin_npc->CheckHandin(this, handin, {}, items);
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
LogNpcHandinDetail("EVENT_TRADE triggered for NPC [{}]", tradingWith->GetNPCTypeID());
}
// this is a catch-all return for items that weren't consumed by the EVENT_TRADE subroutine
// it's possible we have a quest NPC that doesn't have an EVENT_TRADE subroutine
// we can't double fire the ReturnHandinItems() event, so we need to check if it's already been processed from EVENT_TRADE
if (!handin_npc->HasProcessedHandinReturn()) {
if (!handin_npc->HandinStarted()) {
LogNpcHandinDetail("EVENT_TRADE did not process handin, calling ReturnHandinItems() for NPC [{}]", tradingWith->GetNPCTypeID());
std::map<std::string, uint32> handin = {
{"copper", trade->cp},
{"silver", trade->sp},
{"gold", trade->gp},
{"platinum", trade->pp}
};
for (EQ::ItemInstance *inst: items) {
if (!inst || !inst->GetItem()) {
continue;
}
std::string item_id = fmt::format("{}", inst->GetItem()->ID);
handin[item_id] += inst->GetCharges();
}
handin_npc->CheckHandin(this, handin, {}, items);
}
if (RuleB(Items, AlwaysReturnHandins)) {
handin_npc->ReturnHandinItems(this);
LogNpcHandin("ReturnHandinItems called for NPC [{}]", handin_npc->GetNPCTypeID());
}
}
handin_npc->ResetHandin();
for (auto &inst: insts) {
if (inst) {
safe_delete(inst);
}
}
}
}
bool Client::CheckTradeLoreConflict(Client* other)
{
if (!other) {
return true;
}
bool has_lore_item = false;
std::vector<uint32> lore_item_ids;
for (int16 index = EQ::invslot::TRADE_BEGIN; index <= EQ::invslot::TRADE_END; ++index) {
const auto inst = m_inv[index];
if (!inst || !inst->GetItem()) {
continue;
}
if (other->CheckLoreConflict(inst->GetItem())) {
lore_item_ids.emplace_back(inst->GetItem()->ID);
has_lore_item = true;
}
}
for (int16 index = EQ::invbag::TRADE_BAGS_BEGIN; index <= EQ::invbag::TRADE_BAGS_END; ++index) {
const auto inst = m_inv[index];
if (!inst || !inst->GetItem()) {
continue;
}
if (other->CheckLoreConflict(inst->GetItem())) {
lore_item_ids.emplace_back(inst->GetItem()->ID);
has_lore_item = true;
}
}
if (has_lore_item && RuleB(Character, PlayerTradingLoreFeedback)) {
for (const uint32 lore_item_id : lore_item_ids) {
Message(
Chat::Red,
fmt::format(
"{} already has a lore {} in their inventory.",
other->GetCleanName(),
database.CreateItemLink(lore_item_id)
).c_str()
);
}
}
return has_lore_item;
}
bool Client::CheckTradeNonDroppable()
{
for (int16 index = EQ::invslot::TRADE_BEGIN; index <= EQ::invslot::TRADE_END; ++index){
const EQ::ItemInstance* inst = m_inv[index];
if (!inst)
continue;
if (!inst->IsDroppable())
return true;
}
return false;
}
void Client::TraderShowItems()
{
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", CharacterID()));
if (trader_items.empty()) {
return;
}
TraderClientMessaging_Struct tcm{};
tcm.action = ListTraderItems;
for (auto const &t: trader_items) {
TraderItems_Struct items{};
items.item_unique_id = t.item_unique_id;
items.item_id = t.item_id;
items.item_cost = t.item_cost;
tcm.items.push_back(items);
}
{ ar(tcm); }
uint32 packet_size = ss.str().length();
auto outapp = new EQApplicationPacket(OP_Trader, packet_size);
memcpy(outapp->pBuffer, ss.str().data(), packet_size);
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::SendTraderPacket(Client* Trader, uint32 Unknown72)
{
if(!Trader)
return;
auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct));
BecomeTrader_Struct* bts = (BecomeTrader_Struct*)outapp->pBuffer;
bts->action = BazaarTrader_StartTraderMode;
bts->trader_id = Trader->CharacterID();
bts->entity_id = Trader->GetID();
strn0cpy(bts->trader_name, Trader->GetName(), sizeof(bts->trader_name));
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::Trader_CustomerBrowsing(Client *Customer)
{
auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct));
auto sis = (Trader_ShowItems_Struct *) outapp->pBuffer;
sis->action = CustomerBrowsing;
sis->entity_id = Customer->GetID();
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::TraderStartTrader(const EQApplicationPacket *app)
{
uint32 max_items = GetInv().GetLookup()->InventoryTypeSize.Bazaar;
auto inv = GetTraderItems();
bool trade_items_valid = true;
std::vector<TraderRepository::Trader> trader_items{};
ClickTraderNew_Struct in;
EQ::Util::MemoryStreamReader ss(reinterpret_cast<char *>(app->pBuffer), app->size);
cereal::BinaryInputArchive ar(ss);
{
ar(in);
}
uint32 slot_id = 0;
for (auto &i: in.items) {
auto const inst = FindTraderItemByUniqueID(i.unique_id);
if (!inst) {
trade_items_valid = false;
break;
}
if (inst) {
if (inst->GetItem() && inst->GetItem()->NoDrop == 0) {
Message(
Chat::Red,
fmt::format(
"Item: {} is NODROP and found in a Trader's Satchel. Please remove and restart trader mode",
inst->GetItem()->Name)
.c_str());
TraderEndTrader();
safe_delete(inv);
return;
}
}
TraderRepository::Trader trader_item{};
trader_item.id = 0;
trader_item.char_entity_id = GetID();
trader_item.character_id = CharacterID();
trader_item.char_zone_id = GetZoneID();
trader_item.char_zone_instance_id = GetInstanceID();
trader_item.item_charges = inst->GetCharges();
trader_item.item_cost = i.cost;
trader_item.item_id = inst->GetID();
trader_item.item_unique_id = i.unique_id;
trader_item.slot_id = slot_id;
trader_item.listing_date = time(nullptr);
if (inst->IsAugmented()) {
auto augs = inst->GetAugmentIDs();
trader_item.augment_one = augs.at(0);
trader_item.augment_two = augs.at(1);
trader_item.augment_three = augs.at(2);
trader_item.augment_four = augs.at(3);
trader_item.augment_five = augs.at(4);
trader_item.augment_six = augs.at(5);
}
trader_items.emplace_back(trader_item);
}
if (!trade_items_valid || trader_items.empty()) {
Message(Chat::Red, "You are not able to become a trader at this time. Invalid item found.");
TraderEndTrader();
safe_delete(inv);
return;
}
TraderRepository::DeleteWhere(database, fmt::format("`character_id` = {};", CharacterID()));
TraderRepository::ReplaceMany(database, trader_items);
safe_delete(inv);
// This refreshes the Trader window to display the End Trader button
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
auto outapp = new EQApplicationPacket(OP_Trader, sizeof(TraderStatus_Struct));
auto data = (TraderStatus_Struct *) outapp->pBuffer;
data->Code = TraderAck2;
QueuePacket(outapp);
safe_delete(outapp);
}
MessageString(Chat::Yellow, TRADER_MODE_ON);
SetTrader(true);
SendTraderMode(TraderOn);
SendBecomeTraderToWorld(this, TraderOn);
UpdateWho();
LogTrading("Trader Mode ON for Player [{}] with client version {}.", GetCleanName(), (uint32) ClientVersion());
}
void Client::TraderEndTrader()
{
if (IsThereACustomer()) {
auto customer = entity_list.GetClientByID(GetCustomerID());
if (customer) {
auto end_session = new EQApplicationPacket(OP_ShopEnd);
customer->FastQueuePacket(&end_session);
}
}
TraderRepository::DeleteWhere(database, fmt::format("`character_id` = {}", CharacterID()));
SendBecomeTraderToWorld(this, TraderOff);
SendTraderMode(TraderOff);
WithCustomer(0);
SetTrader(false);
UpdateWho();
}
void Client::SendTraderItem(uint32 ItemID, uint16 Quantity, TraderRepository::Trader &t) {
std::string Packet;
int16 FreeSlotID=0;
const EQ::ItemData* item = database.GetItem(ItemID);
if(!item){
LogTrading("Bogus item deleted in Client::SendTraderItem!\n");
return;
}
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
item,
Quantity,
t.augment_one,
t.augment_two,
t.augment_three,
t.augment_four,
t.augment_five,
t.augment_six
)
);
if (inst)
{
bool is_arrow = (inst->GetItem()->ItemType == EQ::item::ItemTypeArrow) ? true : false;
FreeSlotID = m_inv.FindFreeSlot(false, true, inst->GetItem()->Size, is_arrow);
if (TryStacking(inst.get(), ItemPacketTrade, true, false)) {
}
else {
PutItemInInventory(FreeSlotID, *inst);
SendItemPacket(FreeSlotID, inst.get(), ItemPacketTrade);
}
Save();
}
}
void Client::SendSingleTraderItem(uint32 character_id, const std::string &serial_number)
{
auto inst = database.LoadSingleTraderItem(character_id, serial_number);
if (inst) {
SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor?
}
}
void Client::BulkSendTraderInventory(uint32 character_id)
{
const EQ::ItemData *item;
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", character_id));
uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ?
GetInv().GetLookup()->InventoryTypeSize.Bazaar :
trader_items.size();
for (int16 i = 0; i < item_limit; i++) {
if (trader_items.at(i).item_id == 0 || trader_items.at(i).item_cost == 0) {
continue;
}
item = database.GetItem(trader_items.at(i).item_id);
if (item && (item->NoDrop != 0)) {
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
trader_items.at(i).item_id,
trader_items.at(i).item_charges,
trader_items.at(i).augment_one,
trader_items.at(i).augment_two,
trader_items.at(i).augment_three,
trader_items.at(i).augment_four,
trader_items.at(i).augment_five,
trader_items.at(i).augment_six
)
);
if (inst) {
inst->SetUniqueID(trader_items.at(i).item_unique_id);
inst->SetMerchantCount(inst->IsStackable() ? inst->GetCharges() : 1);
inst->SetMerchantSlot(i + 1);
inst->SetPrice(trader_items.at(i).item_cost);
AddDataToMerchantList(i + 1, inst->GetID(), inst->GetMerchantCount(), inst->GetUniqueID());
SendItemPacket(i + 1, inst.get(), ItemPacketMerchant);
}
else
LogTrading("Client::BulkSendTraderInventory nullptr inst pointer");
}
}
}
EQ::ItemInstance *Client::FindTraderItemByUniqueID(std::string &unique_id)
{
EQ::ItemInstance *item = nullptr;
int16 slot_id = 0;
for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) {
item = GetInv().GetItem(i);
if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) {
for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) {
// we already have the parent bag and a contents iterator..why not just iterate the bag!??
slot_id = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(slot_id);
if (item && item->GetUniqueID().compare(unique_id) == 0) {
return item;
}
}
}
}
LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id);
return nullptr;
}
EQ::ItemInstance *Client::FindTraderItemByUniqueID(const char* unique_id)
{
EQ::ItemInstance *item = nullptr;
int16 slot_id = 0;
for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) {
item = GetInv().GetItem(i);
if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) {
for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) {
// we already have the parent bag and a contents iterator..why not just iterate the bag!??
slot_id = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(slot_id);
if (item) {
if (item->GetUniqueID().compare(unique_id) == 0) {
return item;
}
}
}
}
}
LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id);
return nullptr;
}
std::vector<EQ::ItemInstance *> Client::FindTraderItemsByUniqueID(const char* unique_id)
{
std::vector<EQ::ItemInstance *> items{};
EQ::ItemInstance *item = nullptr;
int16 slot_id = 0;
for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) {
item = GetInv().GetItem(i);
if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) {
for (int16 x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) {
// we already have the parent bag and a contents iterator..why not just iterate the bag!??
slot_id = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(slot_id);
if (item && item->GetUniqueID().compare(unique_id) == 0) {
items.push_back(item);
}
}
}
}
LogTrading("Couldn't find item! item_unique_id was [{}]", unique_id);
return items;
}
GetBazaarItems_Struct *Client::GetTraderItems()
{
const EQ::ItemInstance *item = nullptr;
int16 slot_id = INVALID_INDEX;
auto gis = new GetBazaarItems_Struct{0};
uint8 ndx = 0;
for (int16 i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) {
if (ndx >= GetInv().GetLookup()->InventoryTypeSize.Bazaar) {
break;
}
item = GetInv().GetItem(i);
if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel) {
for (int x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++) {
if (ndx >= GetInv().GetLookup()->InventoryTypeSize.Bazaar) {
break;
}
slot_id = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(slot_id);
if (item) {
gis->items[ndx] = item->GetID();
gis->serial_number[ndx] = item->GetUniqueID();
gis->charges[ndx] = item->GetCharges() == 0 ? 1 : item->GetCharges();
ndx++;
}
}
}
}
return gis;
}
uint16 Client::FindTraderItem(std::string &serial_number, uint16 Quantity){
const EQ::ItemInstance* item= nullptr;
uint16 SlotID = 0;
for (int i = EQ::invslot::GENERAL_BEGIN; i <= EQ::invslot::GENERAL_END; i++) {
item = GetInv().GetItem(i);
if (item && item->GetItem()->BagType == EQ::item::BagTypeTradersSatchel){
for (int x = EQ::invbag::SLOT_BEGIN; x <= EQ::invbag::SLOT_END; x++){
SlotID = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(SlotID);
if (item && item->GetUniqueID().compare(serial_number) == 0 &&
(item->GetCharges() >= Quantity || (item->GetCharges() <= 0 && Quantity == 1)))
{
return SlotID;
}
}
}
}
LogTrading("Could NOT find a match for Item: [{}] with a quantity of: [{}] on Trader: [{}]\n",
serial_number , Quantity, GetName());
return 0;
}
void Client::NukeTraderItem(
uint16 slot,
int16 charges,
int16 quantity,
Client *customer,
uint16 trader_slot,
const std::string &serial_number,
int32 item_id
)
{
if (!customer) {
return;
}
LogTrading("NukeTraderItem(Slot <green>[{}] Charges <green>[{}] Quantity <green>[{}]", slot, charges, quantity);
if (quantity < charges) {
customer->SendSingleTraderItem(CharacterID(), serial_number);
m_inv.DeleteItem(slot, quantity);
}
else {
auto outapp = new EQApplicationPacket(OP_TraderDelItem, sizeof(TraderDelItem_Struct));
auto tdis = (TraderDelItem_Struct *) outapp->pBuffer;
tdis->unknown_000 = 0;
tdis->trader_id = customer->GetID();
tdis->item_id = Strings::ToUnsignedBigInt(serial_number);
strn0cpy(tdis->item_unique_id, serial_number.c_str(), sizeof(tdis->item_unique_id));
tdis->unknown_012 = 0;
customer->QueuePacket(outapp);
safe_delete(outapp);
m_inv.DeleteItem(slot);
}
// This updates the trader. Removes it from his trading bags.
//
const EQ::ItemInstance *Inst = m_inv[slot];
database.SaveInventory(CharacterID(), Inst, slot);
EQApplicationPacket *outapp2;
if (quantity < charges) {
outapp2 = new EQApplicationPacket(OP_DeleteItem, sizeof(MoveItem_Struct));
}
else {
outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct));
}
auto mis = (MoveItem_Struct *) outapp2->pBuffer;
mis->from_slot = slot;
mis->to_slot = 0xFFFFFFFF;
mis->number_in_stack = 0xFFFFFFFF;
if (quantity >= charges) {
quantity = 1;
}
for (int i = 0; i < quantity; i++) {
QueuePacket(outapp2);
}
safe_delete(outapp2);
}
void Client::FindAndNukeTraderItem(std::string &item_unique_id, int16 quantity, Client *customer, uint16 trader_slot)
{
const EQ::ItemInstance *item = nullptr;
bool stackable = false;
int16 charges = 0;
uint16 slot_id = FindTraderItem(item_unique_id, quantity);
if (slot_id > 0) {
item = GetInv().GetItem(slot_id);
if (!item) {
LogTrading("Could not find Item: [{}] on Trader: [{}]", item_unique_id, quantity, GetName());
return;
}
charges = GetInv().GetItem(slot_id)->GetCharges();
stackable = item->IsStackable();
if (!stackable) {
quantity = (charges > 0) ? charges : 1;
}
LogTrading("FindAndNuke <green>[{}] charges <green>[{}] quantity <green>[{}]",
item->GetItem()->Name,
charges,
quantity
);
if (charges <= quantity || (charges <= 0 && quantity == 1) || !stackable) {
DeleteItemInInventory(slot_id, quantity);
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`character_id` = {}", CharacterID()));
uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ?
GetInv().GetLookup()->InventoryTypeSize.Bazaar :
trader_items.size();
uint8 count = 0;
bool test_slot = true;
std::vector<TraderRepository::Trader> delete_queue{};
for (int i = 0; i < item_limit; i++) {
if (test_slot && i < trader_items.size() && trader_items.at(i).item_unique_id.compare(item_unique_id) == 0) {
delete_queue.push_back(trader_items.at(i));
NukeTraderItem(
slot_id,
charges,
quantity,
customer,
trader_slot,
trader_items.at(i).item_unique_id,
trader_items.at(i).item_id
);
test_slot = false;
}
else if (i < trader_items.size() && trader_items.at(i).item_id > 0) {
count++;
}
}
TraderRepository::DeleteMany(database, delete_queue);
if (count == 0) {
TraderEndTrader();
}
return;
}
else {
NukeTraderItem(slot_id, charges, quantity, customer, trader_slot, item->GetUniqueID(), item->GetID());
return;
}
}
LogTrading("Could NOT find a match for Item: <red>[{}] with a quantity of: <red>[{}] on Trader: <red>[{}]\n",
item_unique_id,
quantity,
GetName()
);
}
void Client::ReturnTraderReq(const EQApplicationPacket *app, int16 trader_item_charges, uint32 item_id)
{
auto tbs = (TraderBuy_Struct *) app->pBuffer;
auto outapp = new EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct));
auto outtbs = (TraderBuy_Struct *) outapp->pBuffer;
memcpy(outtbs, tbs, app->size);
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
// Convert Serial Number back to Item ID for RoF+
outtbs->item_id = item_id;
}
else {
// RoF+ requires individual price, but older clients require total price
outtbs->price = (tbs->price * static_cast<uint32>(trader_item_charges));
}
outtbs->quantity = trader_item_charges;
// This should probably be trader ID, not customer ID as it is below.
outtbs->trader_id = GetID();
outtbs->already_sold = 0;
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::TradeRequestFailed(const EQApplicationPacket *app)
{
auto tbs = (TraderBuy_Struct *) app->pBuffer;
auto outapp = new EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct));
auto outtbs = (TraderBuy_Struct *) outapp->pBuffer;
memcpy(outtbs, tbs, app->size);
outtbs->already_sold = 0xFFFFFFFF;
outtbs->trader_id = 0xFFFFFFFF;
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::TradeRequestFailed(TraderBuy_Struct &in)
{
auto outapp = EQApplicationPacket(OP_TraderBuy, sizeof(TraderBuy_Struct));
auto data = reinterpret_cast<TraderBuy_Struct *>(outapp.pBuffer);
data->method = in.method;
data->action = in.action;
data->sub_action = Failed;
data->already_sold = 0xFFFFFFFF;
data->item_id = in.item_id;
data->price = in.price;
data->quantity = in.quantity;
data->trader_id = 0xFFFFFFFF;
strn0cpy(data->buyer_name, in.buyer_name, sizeof(data->buyer_name));
strn0cpy(data->item_name, in.item_name, sizeof(data->item_name));
strn0cpy(data->item_unique_id, in.item_unique_id, sizeof(data->item_unique_id));
strn0cpy(data->seller_name, in.seller_name, sizeof(data->seller_name));
QueuePacket(&outapp);
}
void Client::BuyTraderItem(const EQApplicationPacket *app)
{
auto in = reinterpret_cast<TraderBuy_Struct *>(app->pBuffer);
auto trader = entity_list.GetClientByID(in->trader_id);
if (!trader || !trader->IsTrader()) {
Message(Chat::Red, "The trader could not be found.");
TradeRequestFailed(app);
return;
}
auto trader_packet = std::make_unique<EQApplicationPacket>(OP_Trader, static_cast<uint32>(sizeof(TraderBuy_Struct)));
auto data = reinterpret_cast<TraderBuy_Struct*>(trader_packet->pBuffer);
auto buy_inst = trader->FindTraderItemByUniqueID(in->item_unique_id);
std::unique_ptr<EQ::ItemInstance> inst_copy(buy_inst ? buy_inst->Clone() : nullptr);
if (!buy_inst || !inst_copy) {
LogTrading("Unable to find item id <red>[{}] item_sn <red>[{}] on trader", in->item_id, in->item_unique_id);
Message(Chat::Red, "The trader no longer has the item for sale. Please refresh the merchant window.");
TradeRequestFailed(app);
return;
}
auto quantity = in->quantity;
inst_copy->SetCharges(quantity);
if (buy_inst->GetItem()->MaxCharges > 0) {
inst_copy->SetCharges(buy_inst->GetCharges());
}
if (inst_copy->IsStackable() && quantity != buy_inst->GetCharges()) {
inst_copy->CreateUniqueID();
}
LogTrading(
"Name: <green>[{}] IsStackable: <green>[{}] Requested Quantity: <green>[{}]",
buy_inst->GetItem()->Name,
buy_inst->IsStackable(),
quantity
);
if (CheckLoreConflict(inst_copy->GetItem())) {
MessageString(Chat::Red, DUPLICATE_LORE);
TradeRequestFailed(app);
return;
}
if (in->price * quantity <= 0) {
Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1");
trader->Message(Chat::Red, "Internal error. Aborting trade. Please report this to the ServerOP. Error code is 1");
LogError(
"Bazaar: Zero price transaction between <red>[{}] and <red>[{}] aborted. Item: <red>[{}] Charges: "
"<red>[{}] Qty <red>[{}] Price: <red>[{}]",
GetCleanName(),
trader->GetCleanName(),
buy_inst->GetItem()->Name,
buy_inst->GetCharges(),
quantity,
in->price
);
TradeRequestFailed(app);
return;
}
uint64 total_transaction_value = static_cast<uint64>(in->price) * static_cast<uint64>(quantity);
if (total_transaction_value > EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction) {
Message(
Chat::Red,
"That would exceed the single transaction limit of %u platinum.",
EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction / 1000
);
TradeRequestFailed(app);
return;
}
uint64 total_cost = in->price * quantity;
if (!TakeMoneyFromPP(total_cost)) {
MessageString(Chat::Red, INSUFFICIENT_FUNDS);
TradeRequestFailed(app);
return;
}
if (!trader->RemoveItemByItemUniqueId(buy_inst->GetUniqueID(), quantity)) {
AddMoneyToPP(total_cost, true);
Message(Chat::Red, "The Trader no longer has the item. Please refresh the merchant window.");
TradeRequestFailed(app);
return;
}
trader->AddMoneyToPP(total_cost, true);
if (!PutItemInInventoryWithStacking(inst_copy.get())) {
AddMoneyToPP(total_cost, true);
trader->TakeMoneyFromPP(total_cost, true);
trader->PutItemInInventoryWithStacking(buy_inst);
MessageString(Chat::Red, HOW_CAN_YOU_BUY_MORE, trader->GetCleanName());
TradeRequestFailed(app);
return;
}
auto [slot_id, merchant_data] = GetDataFromMerchantListByItemUniqueId(buy_inst->GetUniqueID());
auto [item_id, merchant_quantity, item_unique_id] = merchant_data;
data->action = BazaarBuyItem;
data->price = in->price;
data->quantity = quantity;
data->trader_id = trader->GetID();
strn0cpy(data->seller_name, trader->GetCleanName(), sizeof(data->seller_name));
strn0cpy(data->buyer_name, GetCleanName(), sizeof(data->buyer_name));
strn0cpy(data->item_name, buy_inst->GetItem()->Name, sizeof(data->item_name));
strn0cpy(data->item_unique_id, buy_inst->GetUniqueID().data(), sizeof(data->item_unique_id));
trader->QueuePacket(trader_packet.get());
QueuePacket(app);
LogTrading("Customer Paid: [{}] to {}", DetermineMoneyString(total_cost), trader->GetCleanName());
LogTrading("Customer Received: [{}] {} {} with unique_id of {}",
quantity,
in->item_name,
inst_copy->GetItem()->MaxCharges > 0 ? fmt::format("with {} charges ", inst_copy->GetCharges()).c_str() : std::string(""),
inst_copy->GetUniqueID()
);
LogTrading("Trader Received: [{}] from {}", DetermineMoneyString(total_cost), GetCleanName());
LogTrading("Trader Sent: [{}] {} {} with unique_id of {}",
quantity,
in->item_name,
buy_inst->GetItem()->MaxCharges > 0 ? fmt::format("with {} charges ", buy_inst->GetCharges()).c_str() : std::string(""),
buy_inst->GetUniqueID()
);
if (merchant_quantity > quantity) {
std::unique_ptr<EQ::ItemInstance> vendor_inst(buy_inst ? buy_inst->Clone() : nullptr);
vendor_inst->SetMerchantCount(merchant_quantity - quantity);
vendor_inst->SetMerchantSlot(slot_id);
vendor_inst->SetPrice(in->price);
auto list = GetTraderMerchantList();
std::get<1>(list->at(slot_id)) -= quantity;
TraderRepository::UpdateQuantity(database, item_unique_id, merchant_quantity - quantity);
SendItemPacket(slot_id, vendor_inst.get(), ItemPacketMerchant);
}
else {
auto client_packet = new EQApplicationPacket(OP_ShopDelItem, static_cast<uint32>(sizeof(Merchant_DelItem_Struct)));
auto client_data = reinterpret_cast<struct Merchant_DelItem_Struct *>(client_packet->pBuffer);
client_data->npcid = trader->GetID();
client_data->playerid = GetID();
client_data->itemslot = slot_id;
QueuePacket(client_packet);
safe_delete(client_packet);
auto list = GetTraderMerchantList();
list->erase(slot_id);
TraderRepository::DeleteWhere(database, fmt::format("`item_unique_id` = '{}'", item_unique_id));
}
if (buy_inst && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
auto e = PlayerEvent::TraderPurchaseEvent{
.item_id = buy_inst->GetID(),
.augment_1_id = buy_inst->GetAugmentItemID(0),
.augment_2_id = buy_inst->GetAugmentItemID(1),
.augment_3_id = buy_inst->GetAugmentItemID(2),
.augment_4_id = buy_inst->GetAugmentItemID(3),
.augment_5_id = buy_inst->GetAugmentItemID(4),
.augment_6_id = buy_inst->GetAugmentItemID(5),
.item_name = buy_inst->GetItem()->Name,
.trader_id = trader->CharacterID(),
.trader_name = trader->GetCleanName(),
.price = in->price,
.quantity = quantity,
.charges = buy_inst->GetCharges(),
.total_cost = total_cost,
.player_money_balance = GetCarriedMoney(),
.offline_purchase = trader->IsOffline(),
};
RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e);
}
if (buy_inst && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) {
auto e = PlayerEvent::TraderSellEvent{
.item_id = buy_inst->GetID(),
.augment_1_id = buy_inst->GetAugmentItemID(0),
.augment_2_id = buy_inst->GetAugmentItemID(1),
.augment_3_id = buy_inst->GetAugmentItemID(2),
.augment_4_id = buy_inst->GetAugmentItemID(3),
.augment_5_id = buy_inst->GetAugmentItemID(4),
.augment_6_id = buy_inst->GetAugmentItemID(5),
.item_name = buy_inst->GetItem()->Name,
.buyer_id = CharacterID(),
.buyer_name = GetCleanName(),
.price = in->price,
.quantity = quantity,
.charges = buy_inst->GetCharges(),
.total_cost = total_cost,
.player_money_balance = trader->GetCarriedMoney(),
.offline_purchase = trader->IsOffline(),
};
RecordPlayerEventLogWithClient(trader, PlayerEvent::TRADER_SELL, e);
if (trader->IsOffline()) {
auto e = CharacterOfflineTransactionsRepository::NewEntity();
e.character_id = trader->CharacterID();
e.item_name = buy_inst->GetItem()->Name;
e.price = total_cost;
e.quantity = quantity;
e.type = TRADER_TRANSACTION;
e.buyer_name = GetCleanName();
CharacterOfflineTransactionsRepository::InsertOne(database, e);
}
}
}
void Client::SendBazaarWelcome()
{
const auto results = TraderRepository::GetWelcomeData(database);
EQApplicationPacket outapp(OP_BazaarSearch, static_cast<uint32>(sizeof(BazaarWelcome_Struct)));
auto data = (BazaarWelcome_Struct *) outapp.pBuffer;
data->action = BazaarWelcome;
data->traders = results.count_of_traders;
data->items = results.count_of_items;
QueuePacket(&outapp);
}
void Client::SendBarterWelcome()
{
const auto results = BuyerBuyLinesRepository::GetWelcomeData(database);
MessageString(Chat::White, BUYER_WELCOME, std::to_string(results.count_of_buyers).c_str());
}
void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria)
{
std::vector<BazaarSearchResultsFromDB_Struct> results = Bazaar::GetSearchResults(
database,
content_db,
search_criteria,
GetZoneID(),
GetInstanceID()
);
if (results.empty()) {
SendBazaarDone(GetID());
return;
}
SetTraderTransactionDate();
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
ar(results);
uint32 packet_size = ss.str().length() + sizeof(BazaarSearchMessaging_Struct);
auto out = new EQApplicationPacket(OP_BazaarSearch, packet_size);
auto data = (BazaarSearchMessaging_Struct *) out->pBuffer;
data->action = BazaarSearch;
memcpy(data->payload, ss.str().data(), ss.str().length());
FastQueuePacket(&out);
SendBazaarDone(GetID());
SendBazaarDeliveryCosts();
}
static void UpdateTraderCustomerItemsAdded(
uint32 customer_id,
std::vector<BaseTraderRepository::Trader> trader_items,
uint32 item_id,
uint32 item_limit
)
{
// Send Item packets to the customer to update the Merchant window with the
// new items for sale, and give them a message in their chat window.
auto customer = entity_list.GetClientByID(customer_id);
if (!customer) {
return;
}
const EQ::ItemData *item = database.GetItem(item_id);
if (!item) {
return;
}
customer->Message(Chat::Red, "The Trader has put up %s for sale.", item->Name);
for (auto const &i: trader_items) {
if (i.item_id == item_id) {
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
i.item_id,
i.item_charges,
i.augment_one,
i.augment_two,
i.augment_three,
i.augment_four,
i.augment_five,
i.augment_six
)
);
if (!inst) {
return;
}
inst->SetCharges(i.item_charges);
inst->SetPrice(i.item_cost);
inst->SetUniqueID(i.item_unique_id);
if (inst->IsStackable()) {
inst->SetMerchantCount(i.item_charges);
}
customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor?
LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges",
item->Name, i.item_unique_id, i.item_charges);
}
}
}
static void UpdateTraderCustomerPriceChanged(
uint32 customer_id,
std::vector<BaseTraderRepository::Trader> trader_items,
uint32 item_id,
int32 charges,
uint32 new_price,
uint32 item_limit
)
{
// Send ItemPackets to update the customer's Merchant window with the new price (or remove the item if
// the new price is 0) and inform them with a chat message.
auto customer = entity_list.GetClientByID(customer_id);
if (!customer) {
return;
}
const EQ::ItemData *item = database.GetItem(item_id);
if (!item) {
return;
}
if (new_price == 0) {
// If the new price is 0, remove the item(s) from the window.
auto outapp = new EQApplicationPacket(OP_TraderDelItem, sizeof(TraderDelItem_Struct));
auto tdis = (TraderDelItem_Struct *) outapp->pBuffer;
tdis->unknown_000 = 0;
tdis->trader_id = customer->GetID();
tdis->unknown_012 = 0;
customer->Message(Chat::Red, "The Trader has withdrawn the %s from sale.", item->Name);
for (int i = 0; i < item_limit; i++) {
if (trader_items.at(i).item_id == item_id) {
if (customer->ClientVersion() >= EQ::versions::ClientVersion::RoF) {
// RoF+ use Item IDs for now
tdis->item_id = trader_items.at(i).item_id;
}
LogTrading("Telling customer to remove item [{}] with [{}] charges and S/N [{}]",
item_id, charges, trader_items.at(i).item_unique_id);
customer->QueuePacket(outapp);
}
}
safe_delete(outapp);
return;
}
LogTrading("Sending price updates to customer [{}]", customer->GetName());
auto it = std::find_if(
trader_items.begin(),
trader_items.end(),
[&](TraderRepository::Trader x) {
return x.item_id == item->ID;
}
);
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
it->item_id,
it->item_charges,
it->augment_one,
it->augment_two,
it->augment_three,
it->augment_four,
it->augment_five,
it->augment_six
)
);
if (!inst) {
return;
}
if (charges > 0) {
inst->SetCharges(charges);
}
inst->SetPrice(new_price);
if (inst->IsStackable()) {
inst->SetMerchantCount(charges);
}
// Let the customer know the price in the window has suddenly just changed on them.
customer->Message(Chat::Red, "The Trader has changed the price of %s.", item->Name);
for (int i = 0; i < item_limit; i++) {
if ((trader_items.at(i).item_id != item_id) ||
((!item->Stackable) && (trader_items.at(i).item_charges != charges))) {
continue;
}
inst->SetUniqueID(trader_items.at(i).item_unique_id);
LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges",
item->Name, trader_items.at(i).item_unique_id, trader_items.at(i).item_charges);
customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor??
}
}
void Client::SendBuyerResults(BarterSearchRequest_Struct& bsr)
{
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
std::string search_string(bsr.search_string);
BuyerLineSearch_Struct results{};
SetBarterTime();
if (bsr.search_scope == 1) {
// Local Buyers
results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, 0, GetZoneID(), GetInstanceID());
}
else if (bsr.buyer_id) {
// Specific Buyer
results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, bsr.buyer_id);
} else {
// All Buyers
results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string);
}
if (results.buy_line.empty()) {
Message(Chat::White, "No buylines could be found.");
return;
}
std::string buyer_name = "ID {} not in zone.";
if (search_string.empty()) {
search_string = "*";
}
results.search_string = std::move(search_string);
results.transaction_id = bsr.transaction_id;
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
{ ar(results); }
auto packet = std::make_unique<EQApplicationPacket>(
OP_BuyerItems,
static_cast<uint32>(ss.str().length()) + static_cast<uint32>(sizeof(BuyerGeneric_Struct))
);
auto emu = (BuyerGeneric_Struct *) packet->pBuffer;
emu->action = Barter_BuyerSearch;
memcpy(emu->payload, ss.str().data(), ss.str().length());
QueuePacket(packet.get());
ss.str("");
ss.clear();
}
}
void Client::ShowBuyLines(const EQApplicationPacket *app)
{
auto bir = (BuyerInspectRequest_Struct *) app->pBuffer;
auto buyer = entity_list.GetClientByID(bir->buyer_id);
if (!buyer || buyer->GetCustomerID()) {
bir->approval = 0; // Tell the client that the Buyer is unavailable
QueuePacket(app);
MessageString(Chat::Yellow, TRADER_BUSY);
return;
}
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
SetBarterTime();
bir->approval = buyer->WithCustomer(GetID());
QueuePacket(app);
auto results = BuyerBuyLinesRepository::GetBuyLines(database, buyer->CharacterID());
auto greeting = BuyerRepository::GetWelcomeMessage(database, buyer->GetBuyerID());
if (greeting.length() == 0) {
greeting = "Welcome!";
}
MessageString(Chat::NPCQuestSay, BUYER_GREETING, buyer->GetName(), greeting.c_str());
const std::string name(GetName());
buyer->SendSellerBrowsing(name);
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
for (auto l : results) {
const EQ::ItemData *item = database.GetItem(l.item_id);
l.enabled = 1;
l.item_icon = item->Icon;
l.item_toggle = 1;
{ ar(l); }
auto packet = std::make_unique<EQApplicationPacket>(
OP_BuyerItems,
static_cast<uint32>(ss.str().length()) + static_cast<uint32>(sizeof(BuyerGeneric_Struct))
);
auto emu = (BuyerGeneric_Struct *) packet->pBuffer;
emu->action = Barter_BuyerInspectBegin;
memcpy(emu->payload, ss.str().data(), ss.str().length());
QueuePacket(packet.get());
ss.str("");
ss.clear();
}
return;
}
}
void Client::SellToBuyer(const EQApplicationPacket *app)
{
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
BuyerLineSellItem_Struct sell_line{};
auto in = (BuyerGeneric_Struct *) app->pBuffer;
EQ::Util::MemoryStreamReader ss_in(
reinterpret_cast<char *>(in->payload),
app->size - sizeof(BuyerGeneric_Struct));
cereal::BinaryInputArchive ar(ss_in);
ar(sell_line);
sell_line.seller_name = GetCleanName();
switch (sell_line.purchase_method) {
case BarterInBazaar:
case BarterByVendor: {
auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id);
if (!buyer) {
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
break;
}
if (sell_line.purchase_method == BarterInBazaar && buyer->IsThereACustomer()) {
auto customer = entity_list.GetClientByID(buyer->GetCustomerID());
if (customer) {
customer->CancelBuyerTradeWindow();
}
}
if (!DoBarterBuyerChecks(sell_line)) {
SendBarterBuyerClientMessage(
sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure
);
return;
}
if (!DoBarterSellerChecks(sell_line)) {
SendBarterBuyerClientMessage(
sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure
);
return;
}
BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr));
if (!FindNumberOfFreeInventorySlotsWithSizeCheck(sell_line.trade_items)) {
LogTradingDetail("Seller {} has insufficient inventory space for {} compensation items.",
GetCleanName(),
sell_line.trade_items.size()
);
Message(Chat::Red, "Insufficient inventory space for the compensation items.");
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
return;
}
for (auto const &ti: sell_line.trade_items) {
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
ti.item_id,
ti.item_quantity *
sell_line.seller_quantity
)
);
if (inst.get()->GetItem()) {
buyer->RemoveItem(ti.item_id, ti.item_quantity * sell_line.seller_quantity);
if (!PutItemInInventoryWithStacking(inst.get())) {
Message(Chat::Red, "Error putting item in your inventory.");
buyer->PutItemInInventoryWithStacking(inst.get());
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
return;
}
}
}
std::unique_ptr<EQ::ItemInstance> buy_inst(
database.CreateItem(
sell_line.item_id,
sell_line.seller_quantity
)
);
RemoveItem(sell_line.item_id, sell_line.seller_quantity);
if (buy_inst->IsStackable()) {
if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) {
buyer->Message(Chat::Red, "Error putting item in your inventory.");
PutItemInInventoryWithStacking(buy_inst.get());
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
return;
}
}
else {
for (int i = 1; i <= sell_line.seller_quantity; i++) {
buy_inst->SetCharges(1);
if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) {
buyer->Message(Chat::Red, "Error putting item in your inventory.");
PutItemInInventoryWithStacking(buy_inst.get());
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
return;
}
}
}
uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity;
AddMoneyToPP(total_cost, false);
buyer->TakeMoneyFromPP(total_cost, false);
if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) {
PlayerEvent::BarterTransaction e{};
e.status = "Successful Barter Transaction";
e.item_id = sell_line.item_id;
e.item_quantity = sell_line.seller_quantity;
e.item_name = sell_line.item_name;
e.trade_items = sell_line.trade_items;
for (auto &t: e.trade_items) {
t *= sell_line.seller_quantity;
}
e.total_cost = total_cost;
e.buyer_name = buyer->GetCleanName();
e.seller_name = GetCleanName();
RecordPlayerEventLog(PlayerEvent::BARTER_TRANSACTION, e);
}
if (buyer->IsOffline()) {
auto e = CharacterOfflineTransactionsRepository::NewEntity();
e.character_id = buyer->CharacterID();
e.item_name = sell_line.item_name;
e.price = total_cost;
e.quantity = sell_line.seller_quantity;
e.type = BUYER_TRANSACTION;
e.buyer_name = GetCleanName();
CharacterOfflineTransactionsRepository::InsertOne(database, e);
}
SendWindowUpdatesToSellerAndBuyer(sell_line);
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Success,
Barter_Success
);
buyer->SendBarterBuyerClientMessage(
sell_line,
Barter_BuyerTransactionComplete,
Barter_Success,
Barter_Success
);
break;
}
case BarterOutsideBazaar: {
bool seller_error = false;
auto buyer_time = BuyerRepository::GetTransactionDate(database, sell_line.buyer_id);
if (buyer_time > GetBarterTime()) {
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_DataOutOfDate
);
return;
}
if (sell_line.trade_items.size() > 0) {
Message(Chat::Red, "You must visit the buyer directly when receiving compensation items.");
seller_error = true;
}
auto buy_item_slot_id = GetInv().HasItem(
sell_line.item_id,
sell_line.seller_quantity,
invWherePersonal
);
auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id);
if (!buy_item) {
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_SellerDoesNotHaveItem
);
break;
}
if (seller_error) {
LogTradingDetail("Seller Error <red>[{}] Sell/Buy Transaction Failed.",
seller_error
);
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_Failure
);
return;
}
BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr));
auto server_packet = std::make_unique<ServerPacket>(
ServerOP_BuyerMessaging,
static_cast<uint32>(sizeof(BuyerMessaging_Struct))
);
auto data = (BuyerMessaging_Struct *) server_packet->pBuffer;
data->action = Barter_SellItem;
data->buyer_entity_id = sell_line.buyer_entity_id;
data->buyer_id = sell_line.buyer_id;
data->seller_entity_id = GetID();
data->buy_item_id = sell_line.item_id;
data->buy_item_qty = sell_line.item_quantity;
data->buy_item_cost = sell_line.item_cost;
data->buy_item_icon = sell_line.item_icon;
data->zone_id = GetZoneID();
data->slot = sell_line.slot;
data->seller_quantity = sell_line.seller_quantity;
data->purchase_method = sell_line.purchase_method;
strn0cpy(data->item_name, sell_line.item_name, sizeof(data->item_name));
strn0cpy(data->buyer_name, sell_line.buyer_name.c_str(), sizeof(data->buyer_name));
strn0cpy(data->seller_name, GetCleanName(), sizeof(data->seller_name));
worldserver.SendPacket(server_packet.get());
break;
}
}
}
}
void Client::SendBuyerPacket(Client* Buyer) {
// This is the Buyer Appearance packet. This method is called for each Buyer when a Client connects to the zone.
//
auto outapp = new EQApplicationPacket(OP_Barter, 13 + strlen(GetName()));
char* Buf = (char*)outapp->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerAppearance);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID());
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x01);
VARSTRUCT_ENCODE_STRING(Buf, GetName());
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::ToggleBuyerMode(bool status)
{
auto outapp = std::make_unique<EQApplicationPacket>(
OP_Barter,
static_cast<uint32>(sizeof(BuyerSetAppearance_Struct))
);
auto data = (BuyerSetAppearance_Struct *) outapp->pBuffer;
data->action = Barter_BuyerAppearance;
data->entity_id = GetID();
if (status && IsInBuyerSpace()) {
SetBuyerID(CharacterID());
BuyerRepository::Buyer b{};
b.id = 0;
b.char_id = GetBuyerID();
b.char_entity_id = GetID();
b.char_zone_id = GetZoneID();
b.char_zone_instance_id = GetInstanceID();
b.char_name = GetCleanName();
b.transaction_date = time(nullptr);
BuyerRepository::DeleteBuyer(database, GetBuyerID());
BuyerRepository::InsertOne(database, b);
data->status = BuyerBarter::On;
SetCustomerID(0);
SendBuyerMode(true);
SendBuyerToBarterWindow(this, Barter_AddToBarterWindow);
UpdateWho();
Message(Chat::Yellow, "Barter Mode ON.");
}
else {
data->status = BuyerBarter::Off;
BuyerRepository::DeleteBuyer(database, GetBuyerID());
SetCustomerID(0);
SendBuyerToBarterWindow(this, Barter_RemoveFromBarterWindow);
SendBuyerMode(false);
SetBuyerID(0);
if (!IsInBuyerSpace()) {
Message(Chat::Red, "You must be in a Barter Stall to start Barter Mode.");
}
UpdateWho();
Message(Chat::Yellow, fmt::format("Barter Mode OFF. Buy lines deactivated.").c_str());
}
entity_list.QueueClients(this, outapp.get(), false);
}
void Client::ModifyBuyLine(const EQApplicationPacket *app)
{
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
BuyerBuyLines_Struct bl{};
auto in = (BuyerGeneric_Struct *) app->pBuffer;
EQ::Util::MemoryStreamReader ss_in(
reinterpret_cast<char *>(in->payload),
app->size - sizeof(BuyerGeneric_Struct)
);
cereal::BinaryInputArchive ar(ss_in);
ar(bl);
if (bl.buy_lines.empty()) {
return;
}
BuyerRepository::UpdateTransactionDate(database, GetBuyerID(), time(nullptr));
int64 current_total_cost = 0;
bool pass = false;
auto current_buy_lines = BuyerBuyLinesRepository::GetBuyLines(database, CharacterID());
std::map<uint32, BuylineItemDetails_Struct> item_map;
BuildBuyLineMapFromVector(item_map, current_buy_lines);
current_total_cost = ValidateBuyLineCost(item_map);
auto buy_line = bl.buy_lines.front();
auto it = std::find_if(
current_buy_lines.cbegin(),
current_buy_lines.cend(),
[&](BuyerLineItems_Struct bl) {
return bl.slot == buy_line.slot;
}
);
if (buy_line.item_toggle) {
current_total_cost += buy_line.item_cost * buy_line.item_quantity;
if (it != std::end(current_buy_lines)) {
current_total_cost -= it->item_cost * it->item_quantity;
if (current_total_cost > GetCarriedMoney()) {
buy_line.item_cost = it->item_cost;
buy_line.item_quantity = it->item_quantity;
Message(
Chat::Red,
fmt::format(
"You currently do not have sufficient funds to support your buy lines. You have {} and need {}",
DetermineMoneyString(GetCarriedMoney()),
DetermineMoneyString(current_total_cost)).c_str()
);
SendBuyLineUpdate(buy_line);
return;
}
else {
RemoveItemFromBuyLineMap(item_map, *it);
BuildBuyLineMapFromVector(item_map, bl.buy_lines);
}
}
else {
BuildBuyLineMapFromVector(item_map, bl.buy_lines);
}
}
else {
current_total_cost -= static_cast<int64>(buy_line.item_cost) * static_cast<int64>(buy_line.item_quantity);
std::map<uint32, BuylineItemDetails_Struct> item_map_tmp;
BuildBuyLineMapFromVector(item_map_tmp, bl.buy_lines);
if (ValidateBuyLineItems(item_map_tmp)) {
pass = true;
}
}
if (current_total_cost > static_cast<int64>(GetCarriedMoney())) {
Message(
Chat::Red,
fmt::format(
"You currently do not have sufficient funds to support your buy lines. You have {} and need {}",
DetermineMoneyString(GetCarriedMoney()),
DetermineMoneyString(current_total_cost)).c_str()
);
buy_line.item_toggle = 0;
SendBuyLineUpdate(buy_line);
return;
}
bool buyer_error = false;
if (!ValidateBuyLineItems(item_map)) {
buy_line.item_toggle = 0;
}
buy_line.item_icon = database.GetItem(buy_line.item_id)->Icon;
if ((buy_line.item_toggle && it != std::end(current_buy_lines)) || pass) {
BuyerBuyLinesRepository::ModifyBuyLine(database, buy_line, GetBuyerID());
Message(Chat::Yellow, fmt::format("Buy line for {} modified.", buy_line.item_name).c_str());
}
else if (buy_line.item_toggle && it == std::end(current_buy_lines)) {
BuyerBuyLinesRepository::CreateBuyLine(database, buy_line, GetBuyerID());
Message(Chat::Yellow, fmt::format("Buy line for {} enabled.", buy_line.item_name).c_str());
}
else if (!buy_line.item_toggle) {
BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot);
Message(Chat::Yellow, fmt::format("Buy line for {} disabled.", buy_line.item_name).c_str());
}
else {
BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot);
Message(
Chat::Yellow,
fmt::format("Unhandled modification. Buy line for {} disabled.", buy_line.item_name).c_str());
}
SendBuyLineUpdate(buy_line);
if (IsThereACustomer()) {
auto customer = entity_list.GetClientByID(GetCustomerID());
if (!customer) {
return;
}
auto it = std::find_if(
current_buy_lines.cbegin(),
current_buy_lines.cend(),
[&](BuyerLineItems_Struct bl) {
return bl.slot == buy_line.slot;
}
);
if (it == std::end(current_buy_lines) && !buy_line.item_toggle) {
return;
}
std::stringstream ss_customer{};
cereal::BinaryOutputArchive ar_customer(ss_customer);
BuyerLineItems_Struct blis{};
blis.enabled = buy_line.enabled;
blis.item_cost = buy_line.item_cost;
blis.item_icon = buy_line.item_icon;
blis.item_id = buy_line.item_id;
blis.item_quantity = buy_line.item_quantity;
blis.item_toggle = buy_line.item_toggle;
blis.slot = buy_line.slot;
blis.item_name = buy_line.item_name;
for (auto const &i: buy_line.trade_items) {
BuyerLineTradeItems_Struct bltis{};
bltis.item_icon = i.item_icon;
bltis.item_id = i.item_id;
bltis.item_quantity = i.item_quantity;
bltis.item_name = i.item_name;
blis.trade_items.push_back(bltis);
}
{ ar_customer(blis); }
auto packet = std::make_unique<EQApplicationPacket>(
OP_BuyerItems,
static_cast<uint32>(ss_customer.str().length()) + static_cast<uint32>(sizeof(BuyerGeneric_Struct))
);
auto emu = (BuyerGeneric_Struct *) packet->pBuffer;
emu->action = Barter_BuyerInspectBegin;
memcpy(emu->payload, ss_customer.str().data(), ss_customer.str().length());
customer->QueuePacket(packet.get());
ss_customer.str("");
ss_customer.clear();
}
}
return;
}
void Client::BuyerItemSearch(const EQApplicationPacket *app)
{
auto bis = (BuyerItemSearch_Struct *) app->pBuffer;
const EQ::ItemData *item = 0;
uint32 it = 0;
BuyerItemSearchResults_Struct bisr{};
while ((item = database.IterateItems(&it)) && bisr.results.size() < RuleI(Bazaar, MaxBuyerInventorySearchResults)) {
if (!item->NoDrop) {
continue;
}
auto item_name_match = std::strstr(
Strings::ToLower(item->Name).c_str(),
Strings::ToLower(bis->search_string).c_str()
);
if (item_name_match) {
BuyerItemSearchResultEntry_Struct bisre{};
bisre.item_id = item->ID;
bisre.item_icon = item->Icon;
strn0cpy(bisre.item_name, item->Name, sizeof(bisre.item_name));
bisr.results.push_back(bisre);
}
}
bisr.action = Barter_BuyerSearchResults;
bisr.result_count = bisr.results.size();
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
{ ar(bisr); }
uint32 packet_size = sizeof(BuyerGeneric_Struct) + ss.str().length();
auto outapp = std::make_unique<EQApplicationPacket>(OP_Barter, packet_size);
auto emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = Barter_BuyerSearchResults;
memcpy(emu->payload, ss.str().data(), ss.str().length());
QueuePacket(outapp.get());
ss.str("");
ss.clear();
}
const std::string &Client::GetMailKeyFull() const
{
return m_mail_key_full;
}
const std::string &Client::GetMailKey() const
{
return m_mail_key;
}
void Client::SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action)
{
auto outapp = new ServerPacket(ServerOP_TraderMessaging, sizeof(TraderMessaging_Struct));
auto data = (TraderMessaging_Struct *) outapp->pBuffer;
data->action = action;
data->entity_id = trader->GetID();
data->trader_id = trader->CharacterID();
data->zone_id = trader->GetZoneID();
data->instance_id = trader->GetInstanceID();
strn0cpy(data->trader_name, trader->GetName(), sizeof(data->trader_name));
worldserver.SendPacket(outapp);
safe_delete(outapp);
}
void Client::SendBecomeTrader(BazaarTraderBarterActions action, uint32 entity_id)
{
if (entity_id <= 0) {
return;
}
auto trader = entity_list.GetClientByID(entity_id);
if (!trader) {
return;
}
auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct));
auto data = (BecomeTrader_Struct *) outapp->pBuffer;
data->action = action;
data->entity_id = trader->GetID();
data->trader_id = trader->CharacterID();
data->zone_id = trader->GetZoneID();
data->zone_instance_id = trader->GetInstanceID();
strn0cpy(data->trader_name, trader->GetCleanName(), sizeof(data->trader_name));
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::SendTraderMode(BazaarTraderBarterActions status)
{
auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct));
auto data = (Trader_ShowItems_Struct *) outapp->pBuffer;
data->action = status;
data->entity_id = GetID();
QueuePacket(outapp);
safe_delete(outapp);
}
void Client::TraderUpdateItem(const EQApplicationPacket *app)
{
auto in = reinterpret_cast<TraderPriceUpdate_Struct *>(app->pBuffer);
uint32 new_price = in->new_price;
auto inst = FindTraderItemByUniqueID(in->item_unique_id);
auto customer = entity_list.GetClientByID(GetCustomerID());
if (new_price == 0) {
auto result = TraderRepository::DeleteWhere(database, fmt::format("`item_unique_id` = '{}'", in->item_unique_id));
if (!result) {
LogError("Trader {} attempt to remove item_unique_id {} failed", CharacterID(), in->item_unique_id);
return;
}
in->sub_action = BazaarPriceChange_RemoveItem;
QueuePacket(app);
if (customer && inst) {
auto list = customer->GetTraderMerchantList();
auto client_packet =
new EQApplicationPacket(OP_ShopDelItem, static_cast<uint32>(sizeof(Merchant_DelItem_Struct)));
auto client_data = reinterpret_cast<struct Merchant_DelItem_Struct *>(client_packet->pBuffer);
client_data->npcid = GetID();
client_data->playerid = customer->GetID();
for (auto const [slot_id, merchant_data]: *list) {
auto const [item_id, merchant_quantity, item_unique_id] = merchant_data;
if (item_id == inst->GetID()) {
client_data->itemslot = slot_id;
customer->QueuePacket(client_packet);
AddDataToMerchantList(slot_id, 0, 0, "0000000000000000");
}
}
safe_delete(client_packet);
customer->Message(
Chat::Red,
fmt::format(
"Trader {} removed item {} from the bazaar. Item no longer available.",
GetCleanName(),
inst->GetItem()->Name
).c_str()
);
LogTrading("Trader removed item from trader list with item_unique_id {}", in->item_unique_id);
}
return;
}
auto result = TraderRepository::UpdatePrice(database, in->item_unique_id, new_price);
if (result.empty()) {
auto trader_items = FindTraderItemsByUniqueID(in->item_unique_id);
std::vector<TraderRepository::Trader> queue{};
for (auto const& i : trader_items) {
TraderRepository::Trader e{};
e.id = 0;
e.char_entity_id = GetID();
e.character_id = CharacterID();
e.char_zone_id = GetZoneID();
e.char_zone_instance_id = GetInstanceID();
e.item_charges = i->GetCharges();
e.item_cost = new_price;
e.item_id = i->GetID();
e.item_unique_id = i->GetUniqueID();
e.slot_id = 0;
e.listing_date = time(nullptr);
if (i->IsAugmented()) {
auto augs = i->GetAugmentIDs();
e.augment_one = augs.at(0);
e.augment_two = augs.at(1);
e.augment_three = augs.at(2);
e.augment_four = augs.at(3);
e.augment_five = augs.at(4);
e.augment_six = augs.at(5);
}
queue.push_back(e);
if (customer) {
int16 next_slot_id = GetNextFreeSlotFromMerchantList();
if (next_slot_id != INVALID_INDEX) {
std::unique_ptr<EQ::ItemInstance> vendor_inst_copy(i ? i->Clone() : nullptr);
vendor_inst_copy->SetUniqueID(i->GetUniqueID());
vendor_inst_copy->SetMerchantCount(i->IsStackable() ? i->GetCharges() : 1);
vendor_inst_copy->SetMerchantSlot(next_slot_id );
vendor_inst_copy->SetPrice(new_price);
AddDataToMerchantList(next_slot_id, i->GetID(), i->GetMerchantCount(), i->GetUniqueID());
customer->SendItemPacket(next_slot_id, vendor_inst_copy.get(), ItemPacketMerchant);
}
}
}
if (!queue.empty()) {
TraderRepository::ReplaceMany(database, queue);
}
}
else {
if (customer) {
for (auto const i : result) {
auto [slot_id, merchant_data] = customer->GetDataFromMerchantListByItemUniqueId(i.item_unique_id);
auto [item_id, merchant_quantity, item_unique_id] = merchant_data;
std::unique_ptr<EQ::ItemInstance> vendor_inst_copy(inst ? inst->Clone() : nullptr);
vendor_inst_copy->SetUniqueID(i.item_unique_id);
vendor_inst_copy->SetMerchantCount(i.item_charges);
vendor_inst_copy->SetMerchantSlot(slot_id);
vendor_inst_copy->SetPrice(new_price);
customer->SendItemPacket(slot_id, vendor_inst_copy.get(), ItemPacketMerchant);
}
customer->Message(
Chat::Red,
fmt::format("Trader {} updated the price of item {}", GetCleanName(), inst->GetItem()->Name).c_str()
);
}
}
in->sub_action = BazaarPriceChange_UpdatePrice;
QueuePacket(app);
}
void Client::SendBazaarDone(uint32 trader_id)
{
auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct));
auto brds = (BazaarReturnDone_Struct *) outapp2->pBuffer;
brds->TraderID = trader_id;
brds->Type = BazaarSearchDone;
brds->Unknown008 = 0xFFFFFFFF;
brds->Unknown012 = 0xFFFFFFFF;
brds->Unknown016 = 0xFFFFFFFF;
QueuePacket(outapp2);
safe_delete(outapp2);
}
void Client::SendBulkBazaarTraders()
{
if (ClientVersion() < EQ::versions::ClientVersion::RoF2) {
return;
}
TraderRepository::BulkTraders_Struct results{};
if (RuleB(Bazaar, UseAlternateBazaarSearch))
{
if (GetZoneID() == Zones::BAZAAR) {
results = TraderRepository::GetDistinctTraders(database, GetInstanceID());
}
uint32 number = 1;
auto shards = CharacterDataRepository::GetInstanceZonePlayerCounts(database, Zones::BAZAAR);
for (auto const &shard: shards) {
if (GetZoneID() != Zones::BAZAAR || (GetZoneID() == Zones::BAZAAR && GetInstanceID() != shard.instance_id)) {
TraderRepository::DistinctTraders_Struct t{};
t.entity_id = 0;
t.trader_id = TraderRepository::TRADER_CONVERT_ID + shard.instance_id;
t.trader_name = fmt::format("Bazaar Shard {}", number);
t.zone_id = Zones::BAZAAR;
t.zone_instance_id = shard.instance_id;
results.count += 1;
results.name_length += t.trader_name.length() + 1;
results.traders.push_back(t);
}
number++;
}
}
else {
results = TraderRepository::GetDistinctTraders(
database,
GetInstanceID(),
EQ::constants::StaticLookup(ClientVersion())->BazaarTraderLimit
);
}
SetTraderCount(results.count);
auto p_size = 4 + 12 * results.count + results.name_length;
auto buffer = std::make_unique<char[]>(p_size);
memset(buffer.get(), 0, p_size);
char *bufptr = buffer.get();
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, results.count);
for (auto t : results.traders) {
VARSTRUCT_ENCODE_TYPE(uint16, bufptr, t.zone_id);
VARSTRUCT_ENCODE_TYPE(uint16, bufptr, t.zone_instance_id);
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.trader_id);
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.entity_id);
VARSTRUCT_ENCODE_STRING(bufptr, t.trader_name.c_str());
}
auto outapp = std::make_unique<EQApplicationPacket>(OP_TraderBulkSend, p_size);
memcpy(outapp->pBuffer, buffer.get(), p_size);
QueuePacket(outapp.get());
}
void Client::DoBazaarInspect(BazaarInspect_Struct &in)
{
auto items = TraderRepository::GetWhere(
database, fmt::format("`item_unique_id` = '{}'", in.item_unique_id)
);
if (items.empty()) {
LogInfo("Failed to find item with serial number [{}]", in.item_unique_id);
return;
}
auto &item = items.front();
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
item.item_id,
item.item_charges,
item.augment_one,
item.augment_two,
item.augment_three,
item.augment_four,
item.augment_five,
item.augment_six
)
);
if (inst) {
SendItemPacket(0, inst.get(), ItemPacketViewLink);
}
}
void Client::SendBazaarDeliveryCosts()
{
auto outapp = std::make_unique<EQApplicationPacket>(
OP_BazaarSearch,
static_cast<uint32>(sizeof(BazaarDeliveryCost_Struct))
);
auto data = (BazaarDeliveryCost_Struct *) outapp->pBuffer;
data->action = DeliveryCostUpdate;
data->voucher_delivery_cost = RuleI(Bazaar, VoucherDeliveryCost);
data->parcel_deliver_cost = RuleR(Bazaar, ParcelDeliveryCostMod);
QueuePacket(outapp.get());
}
std::string Client::DetermineMoneyString(uint64 cp)
{
uint32 plat = cp / 1000;
uint32 gold = (cp - plat * 1000) / 100;
uint32 silver = (cp - plat * 1000 - gold * 100) / 10;
uint32 copper = (cp - plat * 1000 - gold * 100 - silver * 10);
if (!plat && !gold && !silver && !copper) {
return std::string("No Money");
}
std::string money {};
if (plat) {
money += fmt::format("{}p ", plat);
}
if (gold) {
money += fmt::format("{}g ", gold);
}
if (silver) {
money += fmt::format("{}s ", silver);
}
if (copper) {
money += fmt::format("{}c", copper);
}
return fmt::format("{}", money);
}
void Client::BuyTraderItemFromBazaarWindow(const EQApplicationPacket *app)
{
auto in = reinterpret_cast<TraderBuy_Struct *>(app->pBuffer);
auto trader_item = TraderRepository::GetItemByItemUniqueNumber(database, in->item_unique_id);
LogTradingDetail(
"Packet details: \n"
"Action :{}\n"
"Method :{}\n"
"SubAction :{}\n"
"Unknown_012 :{}\n"
"Trader ID :{}\n"
"Buyer Name :{}\n"
"Seller Name :{}\n"
"Unknown_148 :{}\n"
"Item Name :{}\n"
"Item Unique ID :{}\n"
"Unknown_261 :{}\n"
"Item ID :{}\n"
"Price :{}\n"
"Already Sold :{}\n"
"Unknown_276 :{}\n"
"Quantity :{}\n",
in->action,
in->method,
in->sub_action,
in->unknown_012,
in->trader_id,
in->buyer_name,
in->seller_name,
in->unknown_148,
in->item_name,
in->item_unique_id,
in->unknown_261,
in->item_id,
in->price,
in->already_sold,
in->unknown_276,
in->quantity
);
if (!trader_item.id || GetTraderTransactionDate() < trader_item.listing_date) {
LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item unique_id "
"[{}] The Traders data was outdated.",
in->trader_id,
in->item_unique_id
);
in->method = BazaarByParcel;
in->sub_action = DataOutDated;
TradeRequestFailed(app);
return;
}
if (trader_item.active_transaction) {
LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number "
"[{}] The item is already within an active transaction.",
in->trader_id,
in->item_unique_id
);
in->method = BazaarByParcel;
in->sub_action = DataOutDated;
TradeRequestFailed(app);
return;
}
auto next_slot = FindNextFreeParcelSlot(CharacterID());
if (next_slot == INVALID_INDEX) {
LogTrading(
"{} attempted to purchase {} from the bazaar with parcel delivery. Unfortunately their parcel limit was "
"reached. Purchase unsuccessful.",
GetCleanName(),
in->item_name
);
in->method = BazaarByParcel;
in->sub_action = TooManyParcels;
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
TraderRepository::UpdateActiveTransaction(database, trader_item.id, true);
uint32 quantity = in->quantity;
auto item = database.GetItem(trader_item.item_id);
int16 charges = 1;
if (trader_item.item_charges > 0 || item->Stackable || item->MaxCharges > 0) {
charges = trader_item.item_charges;
}
LogTradingDetail(
"Step 1:Bazaar Purchase. Buyer [{}] Seller [{}] Quantity [{}] Charges [{}] Item_Unique_ID [{}]",
CharacterID(),
in->trader_id,
quantity,
charges,
in->item_unique_id
);
uint64 total_cost = static_cast<uint64>(in->price) * static_cast<uint64>(quantity);
if (total_cost > EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction) {
Message(
Chat::Red,
"That would exceed the single transaction limit of %u platinum.",
EQ::constants::StaticLookup(ClientVersion())->BazaarMaxTransaction / 1000
);
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
uint64 fee = std::round(total_cost * RuleR(Bazaar, ParcelDeliveryCostMod));
if (!TakeMoneyFromPP(total_cost + fee, false)) {
in->method = BazaarByParcel;
in->sub_action = InsufficientFunds;
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
Message(Chat::Red, fmt::format("You paid {} for the parcel delivery.", DetermineMoneyString(fee)).c_str());
SendMoneyUpdate();
LogTradingDetail("Step 2:Bazaar Purchase. Took [{}] {}from Buyer [{}] for purchase of [{}] {}{}",
DetermineMoneyString(total_cost),
fee > 0 ? fmt::format("plus a fee of [{}] ", fee) : std::string(""),
CharacterID(),
quantity,
quantity > 1 ? fmt::format("{}s", in->item_name) : in->item_name,
item->MaxCharges > 0 ? fmt::format(" with charges of [{}]", charges) : std::string("")
);
auto out_server = std::make_unique<ServerPacket>(ServerOP_BazaarPurchase, sizeof(BazaarPurchaseMessaging_Struct));
auto out_data = reinterpret_cast<BazaarPurchaseMessaging_Struct *>(out_server->pBuffer);
out_data->transaction_status = BazaarPurchaseBuyerCompleteSendToSeller;
out_data->trader_buy_struct.action = in->action;
out_data->trader_buy_struct.method = in->method;
out_data->trader_buy_struct.already_sold = in->already_sold;
out_data->trader_buy_struct.item_id = item->ID;
out_data->trader_buy_struct.price = in->price;
out_data->trader_buy_struct.quantity = in->quantity;
out_data->trader_buy_struct.sub_action = in->sub_action;
out_data->trader_buy_struct.trader_id = trader_item.character_id;
out_data->buyer_id = CharacterID();
out_data->item_aug_1 = trader_item.augment_one;
out_data->item_aug_2 = trader_item.augment_two;
out_data->item_aug_3 = trader_item.augment_three;
out_data->item_aug_4 = trader_item.augment_four;
out_data->item_aug_5 = trader_item.augment_five;
out_data->item_aug_6 = trader_item.augment_six;
out_data->item_quantity = quantity;
out_data->item_charges = charges;
out_data->id = trader_item.id;
out_data->trader_zone_id = trader_item.char_zone_id;
out_data->trader_zone_instance_id = trader_item.char_zone_instance_id;
out_data->buyer_zone_id = GetZoneID();
out_data->buyer_zone_instance_id = GetInstanceID();
strn0cpy(out_data->trader_buy_struct.buyer_name, GetCleanName(), sizeof(out_data->trader_buy_struct.buyer_name));
strn0cpy(out_data->trader_buy_struct.seller_name, in->seller_name, sizeof(out_data->trader_buy_struct.seller_name));
strn0cpy(out_data->trader_buy_struct.item_name, in->item_name, sizeof(out_data->trader_buy_struct.item_name));
strn0cpy(
out_data->trader_buy_struct.item_unique_id,
in->item_unique_id,
sizeof(out_data->trader_buy_struct.item_unique_id)
);
worldserver.SendPacket(out_server.get());
LogTradingDetail("Step 3:Bazaar Purchase. Buyer checks passed, sending bazaar messaging data to trader via world.\n"
"Action: {} \n"
"Sub Action: {} \n"
"Method: {} \n"
"Item ID: {} \n"
"Item Unique ID: {} \n"
"Item Name: {} \n"
"Price: {} \n"
"Quantity: {} \n"
"Charges: {} \n"
"Augment One: {} \n"
"Augment Two: {} \n"
"Augment Three: {} \n"
"Augment Four: {} \n"
"Augment Five: {} \n"
"Augment Six: {} \n"
"Already Sold: {} \n"
"DB ID: {} \n"
"Trader ID: {} \n"
"Trader: {} \n"
"Trader Zone ID {} \n"
"Trader Zone Instance ID {} \n"
"Buyer ID: {} \n"
"Buyer: {} \n",
out_data->trader_buy_struct.action,
out_data->trader_buy_struct.sub_action,
out_data->trader_buy_struct.method,
out_data->trader_buy_struct.item_id,
out_data->trader_buy_struct.item_unique_id,
out_data->trader_buy_struct.item_name,
out_data->trader_buy_struct.price,
out_data->trader_buy_struct.quantity,
out_data->item_charges,
out_data->item_aug_1,
out_data->item_aug_2,
out_data->item_aug_3,
out_data->item_aug_4,
out_data->item_aug_5,
out_data->item_aug_6,
out_data->trader_buy_struct.already_sold,
out_data->id,
out_data->trader_buy_struct.trader_id,
out_data->trader_buy_struct.seller_name,
out_data->trader_zone_id,
out_data->trader_zone_instance_id,
out_data->buyer_id,
out_data->trader_buy_struct.buyer_name);
}
void Client::SetBuyerWelcomeMessage(const char *welcome_message)
{
BuyerRepository::UpdateWelcomeMessage(database, CharacterID(), welcome_message);
}
void Client::SendBuyerGreeting(uint32 buyer_id)
{
auto buyer = BuyerRepository::GetWhere(database, fmt::format("`char_id` = '{}'", buyer_id));
if (buyer.empty()) {
Message(Chat::White, "Welcome!");
return;
}
Message(Chat::White, buyer.front().welcome_message.c_str());
}
void Client::SendSellerBrowsing(const std::string &browser)
{
auto outapp = std::make_unique<EQApplicationPacket>(OP_Barter, static_cast<uint32>(sizeof(BuyerBrowsing_Struct)));
auto eq = (BuyerBrowsing_Struct *) outapp->pBuffer;
eq->action = Barter_SellerBrowsing;
strn0cpy(eq->char_name, browser.c_str(), sizeof(eq->char_name));
QueuePacket(outapp.get());
}
void Client::SendBuyerMode(bool status)
{
auto outapp = std::make_unique<EQApplicationPacket>(OP_Barter, 4);
auto emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = status ? Barter_BuyerModeOn : Barter_BuyerModeOff;
QueuePacket(outapp.get());
}
bool Client::IsInBuyerSpace()
{
#define BUYER_DOOR_ARC_RADIUS_HIGH 91
#define BUYER_DOOR_ARC_RADIUS_LOW 71
#define BUYER_DOOR_OPEN_TYPE 155
#define TRADER_DOOR_OPEN_TYPE 153
struct BuyerDoorDataStruct {
uint32 door_id;
uint32 arc_offset;
};
std::vector<BuyerDoorDataStruct> buyer_door_data = {
{.door_id = 2}, {.arc_offset = 90},{.door_id = 3} ,{.arc_offset = 0} ,{.door_id = 4}, {.arc_offset = 0},
{.door_id = 5}, {.arc_offset = 0} ,{.door_id = 6} ,{.arc_offset = 90},{.door_id = 7}, {.arc_offset = 0},
{.door_id = 8}, {.arc_offset = 0} ,{.door_id = 9} ,{.arc_offset = 0} ,{.door_id = 10}, {.arc_offset = 0},
{.door_id = 11},{.arc_offset = 0} ,{.door_id = 12},{.arc_offset = 0} ,{.door_id = 13}, {.arc_offset = 0},
{.door_id = 14},{.arc_offset = 0} ,{.door_id = 15},{.arc_offset = 0} ,{.door_id = 16}, {.arc_offset = 90},
{.door_id = 17},{.arc_offset = 0} ,{.door_id = 18},{.arc_offset = 0} ,{.door_id = 19}, {.arc_offset = 0},
{.door_id = 20},{.arc_offset = 0} ,{.door_id = 21},{.arc_offset = 0} ,{.door_id = 22}, {.arc_offset = 0},
{.door_id = 23},{.arc_offset = 0} ,{.door_id = 24},{.arc_offset = 0} ,{.door_id = 25}, {.arc_offset = 0},
{.door_id = 26},{.arc_offset = 0} ,{.door_id = 27},{.arc_offset = 0} ,{.door_id = 28}, {.arc_offset = 0},
{.door_id = 29},{.arc_offset = 90},{.door_id = 30},{.arc_offset = 0} ,{.door_id = 31}, {.arc_offset = 0},
{.door_id = 32},{.arc_offset = 0} ,{.door_id = 33},{.arc_offset = 0} ,{.door_id = 34}, {.arc_offset = 0},
{.door_id = 35},{.arc_offset = 0} ,{.door_id = 36},{.arc_offset = 90},{.door_id = 37}, {.arc_offset = 0},
{.door_id = 38},{.arc_offset = 0} ,{.door_id = 39},{.arc_offset = 0} ,{.door_id = 40}, {.arc_offset = 0},
{.door_id = 41},{.arc_offset = 0} ,{.door_id = 42},{.arc_offset = 0} ,{.door_id = 43}, {.arc_offset = 90},
{.door_id = 44},{.arc_offset = 0} ,{.door_id = 45},{.arc_offset = 0} ,{.door_id = 46}, {.arc_offset = 0},
{.door_id = 47},{.arc_offset = 0} ,{.door_id = 48},{.arc_offset = 0} ,{.door_id = 49}, {.arc_offset = 0},
{.door_id = 50},{.arc_offset = 90},{.door_id = 51},{.arc_offset = 90},{.door_id = 52}, {.arc_offset = 0},
{.door_id = 53},{.arc_offset = 0} ,{.door_id = 54},{.arc_offset = 0}, {.door_id = 55}, {.arc_offset = 0},
{.door_id = 56},{.arc_offset = 0} ,{.door_id = 57},{.arc_offset = 0}, {.door_id = 122},{.arc_offset = 0}
};
auto m_location = GetPosition();
for (auto const &d: buyer_door_data) {
auto door = entity_list.GetDoorsByDoorID(d.door_id);
if (door && IsWithinCircularArc(
door->GetPosition(),
m_location,
d.arc_offset,
BUYER_DOOR_ARC_RADIUS_HIGH,
BUYER_DOOR_ARC_RADIUS_LOW
)
) {
return true;
}
}
for (auto const& d:entity_list.GetDoorsList()) {
if (d.second->GetOpenType() == DoorType::BuyerStall) {
if (IsWithinSquare(d.second->GetPosition(), d.second->GetSize(), GetPosition())) {
return true;
}
}
}
return false;
}
void Client::CreateStartingBuyLines(const EQApplicationPacket *app)
{
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
BuyerBuyLines_Struct bl{};
auto in = (BuyerGeneric_Struct *) app->pBuffer;
EQ::Util::MemoryStreamReader ss_in(
reinterpret_cast<char *>(in->payload),
app->size - sizeof(BuyerGeneric_Struct));
cereal::BinaryInputArchive ar(ss_in);
ar(bl);
if (bl.buy_lines.empty()) {
return;
}
std::map<uint32, BuylineItemDetails_Struct> item_map{};
if (!BuildBuyLineMap(item_map, bl)) {
ToggleBuyerMode(false);
return;
}
auto proposed_total_cost = ValidateBuyLineCost(item_map);
if (proposed_total_cost == 0) {
ToggleBuyerMode(false);
return;
}
if (!ValidateBuyLineItems(item_map)) {
ToggleBuyerMode(false);
return;
}
std::stringstream ss_out{};
cereal::BinaryOutputArchive ar_out(ss_out);
for (auto &b: bl.buy_lines) {
BuyerBuyLinesRepository::CreateBuyLine(database, b, CharacterID());
{ ar_out(b); }
uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct);
auto out = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
auto data = (BazaarSearchMessaging_Struct *) out->pBuffer;
data->action = Barter_BuyerItemUpdate;
memcpy(data->payload, ss_out.str().data(), ss_out.str().length());
QueuePacket(out.get());
ss_out.str("");
ss_out.clear();
}
Message(Chat::Yellow, fmt::format("{} buy lines enabled.", bl.buy_lines.size()).c_str());
}
}
void Client::SendBuyLineUpdate(const BuyerLineItems_Struct &buy_line)
{
std::stringstream ss_out{};
cereal::BinaryOutputArchive ar_out(ss_out);
{ ar_out(buy_line); }
uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct);
auto out = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
auto data = (BazaarSearchMessaging_Struct *) out->pBuffer;
data->action = Barter_BuyerItemUpdate;
memcpy(data->payload, ss_out.str().data(), ss_out.str().length());
QueuePacket(out.get());
ss_out.str("");
ss_out.clear();
}
void Client::CheckIfMovedItemIsPartOfBuyLines(uint32 item_id)
{
auto b_trade_items = BuyerTradeItemsRepository::GetTradeItems(database, GetBuyerID());
if (b_trade_items.empty()) {
return;
}
auto it = std::find_if(
b_trade_items.cbegin(),
b_trade_items.cend(),
[&](const BaseBuyerTradeItemsRepository::BuyerTradeItems bti) {
return bti.item_id == item_id;
}
);
if (it != std::end(b_trade_items)) {
auto item = GetInv().GetItem(GetInv().HasItem(item_id, 1, invWherePersonal));
if (!item) {
return;
}
Message(
Chat::Red,
fmt::format(
"You moved an item ({}) that is part of an active buy line.",
item->GetItem()->Name
).c_str()
);
ToggleBuyerMode(false);
}
}
void Client::SendWindowUpdatesToSellerAndBuyer(BuyerLineSellItem_Struct &blsi)
{
auto buyer = entity_list.GetClientByID(blsi.buyer_entity_id);
auto seller = this;
if (!buyer || !seller) {
return;
}
if (blsi.item_quantity - blsi.seller_quantity <= 0) {
auto outapp = std::make_unique<EQApplicationPacket>(
OP_BuyerItems,
static_cast<uint32>(sizeof(BuyerRemoveItemFromMerchantWindow_Struct))
);
auto data = (BuyerRemoveItemFromMerchantWindow_Struct *) outapp->pBuffer;
data->action = Barter_RemoveFromMerchantWindow;
data->buy_slot_id = blsi.slot;
QueuePacket(outapp.get());
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
BuyerLineItems_Struct bl{};
bl.enabled = 0;
bl.item_cost = blsi.item_cost;
bl.item_icon = blsi.item_icon;
bl.item_id = blsi.item_id;
bl.item_quantity = blsi.item_quantity - blsi.seller_quantity;
bl.item_name = blsi.item_name;
bl.item_toggle = 0;
bl.slot = blsi.slot;
for (auto const &b: blsi.trade_items) {
BuyerLineTradeItems_Struct blti{};
blti.item_icon = b.item_icon;
blti.item_id = b.item_id;
blti.item_quantity = b.item_quantity;
blti.item_name = b.item_name;
bl.trade_items.push_back(blti);
}
{ ar(bl); }
uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct);
outapp = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
auto emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = Barter_BuyerItemUpdate;
memcpy(emu->payload, ss.str().data(), ss.str().length());
buyer->QueuePacket(outapp.get());
BuyerBuyLinesRepository::DeleteBuyLine(database, buyer->CharacterID(), blsi.slot);
}
else {
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
BuyerLineItems_Struct bli{};
bli.enabled = 1;
bli.item_cost = blsi.item_cost;
bli.item_icon = blsi.item_icon;
bli.item_id = blsi.item_id;
bli.item_quantity = blsi.item_quantity - blsi.seller_quantity;
bli.item_toggle = 1;
bli.slot = blsi.slot;
bli.item_name = blsi.item_name;
for (auto const &b: blsi.trade_items) {
BuyerLineTradeItems_Struct blti{};
blti.item_id = b.item_id;
blti.item_icon = b.item_icon;
blti.item_quantity = b.item_quantity;
blti.item_name = b.item_name;
bli.trade_items.push_back(blti);
}
{ ar(bli); }
uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct);
auto outapp = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
auto emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = Barter_BuyerInspectBegin;
memcpy(emu->payload, ss.str().data(), ss.str().length());
QueuePacket(outapp.get());
outapp = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = Barter_BuyerItemUpdate;
memcpy(emu->payload, ss.str().data(), ss.str().length());
buyer->QueuePacket(outapp.get());
BuyerBuyLinesRepository::ModifyBuyLine(database, bli, buyer->GetBuyerID());
}
}
void Client::SendBuyerToBarterWindow(Client *buyer, uint32 action)
{
auto server_packet = std::make_unique<ServerPacket>(
ServerOP_BuyerMessaging,
static_cast<uint32>(sizeof(BuyerMessaging_Struct))
);
auto data = (BuyerMessaging_Struct *) server_packet->pBuffer;
data->action = action;
data->zone_id = buyer->GetZoneID();
data->buyer_id = buyer->GetBuyerID();
data->buyer_entity_id = buyer->GetID();
strn0cpy(data->buyer_name, buyer->GetCleanName(), sizeof(data->buyer_name));
worldserver.SendPacket(server_packet.get());
}
void Client::SendBulkBazaarBuyers()
{
auto results = BuyerRepository::All(database);
if (results.empty()) {
return;
}
auto outapp = std::make_unique<EQApplicationPacket>(
OP_Barter,
static_cast<uint32>(sizeof(BuyerAddBuyertoBarterWindow_Struct))
);
auto emu = (BuyerAddBuyertoBarterWindow_Struct *) outapp->pBuffer;
for (auto const &b: results) {
auto buyer = entity_list.GetClientByCharID(b.char_id);
emu->action = Barter_AddToBarterWindow;
emu->buyer_id = b.char_id;
emu->buyer_entity_id = buyer ? buyer->GetID() : 0;
emu->zone_id = buyer ? buyer->GetZoneID() : 0;
strn0cpy(emu->buyer_name, b.char_name.c_str(), sizeof(emu->buyer_name));
QueuePacket(outapp.get());
}
}
void Client::SendBarterBuyerClientMessage(
BuyerLineSellItem_Struct &blsi,
BarterBuyerActions action,
BarterBuyerSubActions sub_action,
BarterBuyerSubActions error_code
)
{
std::stringstream ss{};
cereal::BinaryOutputArchive ar(ss);
blsi.sub_action = sub_action;
blsi.error_code = error_code;
{ ar(blsi); }
uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct);
auto outapp = std::make_unique<EQApplicationPacket>(OP_BuyerItems, packet_size);
auto emu = (BuyerGeneric_Struct *) outapp->pBuffer;
emu->action = action;
memcpy(emu->payload, ss.str().data(), ss.str().length());
QueuePacket(outapp.get());
}
bool Client::BuildBuyLineMap(std::map<uint32, BuylineItemDetails_Struct> &item_map, BuyerBuyLines_Struct &bl)
{
bool buyer_error = false;
for (auto const &b: bl.buy_lines) {
if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) {
Message(
Chat::Red,
fmt::format(
"You cannot have two buy lines for the same item {}. Buy line not possible.",
b.item_name
).c_str()
);
buyer_error = true;
break;
}
BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity};
item_map.emplace(b.item_id, t);
for (auto const &i: b.trade_items) {
if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) {
Message(
Chat::Red,
fmt::format(
"You cannot buy {} and offer the same item as compensation. Buy line not possible.",
i.item_name
).c_str()
);
buyer_error = true;
break;
}
if (item_map.contains(i.item_id)) {
item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity;
continue;
}
t = {0, i.item_quantity * b.item_quantity};
item_map.emplace(i.item_id, t);
}
}
if (buyer_error) {
return false;
}
return true;
}
bool Client::BuildBuyLineMapFromVector(
std::map<uint32, BuylineItemDetails_Struct> &item_map,
std::vector<BuyerLineItems_Struct> &bl
)
{
bool buyer_error = false;
for (auto const &b: bl) {
if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) {
Message(
Chat::Red,
fmt::format(
"You cannot have two buy lines for the same item {}. Buy line not possible.",
b.item_name
).c_str()
);
buyer_error = true;
break;
}
BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity};
item_map.emplace(b.item_id, t);
for (auto const &i: b.trade_items) {
if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) {
Message(
Chat::Red,
fmt::format(
"You cannot buy {} and offer the same item as compensation. Buy line not possible.",
i.item_name
).c_str()
);
buyer_error = true;
break;
}
if (item_map.contains(i.item_id)) {
item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity;
continue;
}
t = {0, i.item_quantity * b.item_quantity};
item_map.emplace(i.item_id, t);
}
}
if (buyer_error) {
return false;
}
return true;
}
void
Client::RemoveItemFromBuyLineMap(std::map<uint32, BuylineItemDetails_Struct> &item_map, const BuyerLineItems_Struct &bl)
{
if (item_map.contains(bl.item_id) && item_map[bl.item_id].item_cost > 0) {
item_map.erase(bl.item_id);
}
for (auto const &i: bl.trade_items) {
if (item_map.contains(i.item_id) &&
(item_map[i.item_id].item_quantity - (i.item_quantity * bl.item_quantity)) == 0) {
item_map.erase(i.item_id);
}
else if (item_map.contains(i.item_id)) {
item_map[i.item_id].item_quantity -= i.item_quantity * bl.item_quantity;
}
}
}
bool Client::ValidateBuyLineItems(std::map<uint32, BuylineItemDetails_Struct> &item_map)
{
bool buyer_error = false;
for (auto const &i: item_map) {
auto item = database.GetItem(i.first);
if (!item) {
buyer_error = true;
break;
}
if (i.second.item_cost > 0) {
auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal);
auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id);
if (buy_item && CheckLoreConflict(buy_item->GetItem())) {
Message(
Chat::Red,
fmt::format(
"You already have a {}. Purchasing another will cause a lore conflict. Buy line not possible.",
buy_item->GetItem()->Name
).c_str()
);
buyer_error = true;
break;
}
}
if (i.second.item_cost == 0) {
if (i.second.item_quantity > 1 && CheckLoreConflict(item)) {
Message(
Chat::Red,
fmt::format(
"Your buy line requires {} {}s however the item is LORE. Buy line not possible.",
i.second.item_quantity,
item->Name
).c_str()
);
buyer_error = true;
break;
}
auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal);
auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id);
if (!buy_item) {
Message(
Chat::Red,
fmt::format(
"Your buy line(s) require a total of {} {}{} which could not be found. Buy line not possible.",
i.second.item_quantity,
item->Name,
i.second.item_quantity > 1 ? "s" : ""
).c_str()
);
buyer_error = true;
break;
}
if (buy_item->IsAugmentable() && buy_item->IsAugmented()) {
Message(
Chat::Red,
fmt::format(
"You cannot offer {} because it is augmented. Buy line not possible.",
buy_item->GetItem()->Name
).c_str()
);
buyer_error = true;
break;
}
if (!buy_item->IsDroppable()) {
Message(
Chat::Red,
fmt::format(
"You cannot offer {} because it is NoTrade. Buy line not possible.",
buy_item->GetItem()->Name
).c_str());
buyer_error = true;
break;
}
buyer_error = false;
}
}
return !buyer_error;
}
int64 Client::ValidateBuyLineCost(std::map<uint32, BuylineItemDetails_Struct> &item_map)
{
uint64 proposed_total_cost = std::accumulate(
item_map.cbegin(),
item_map.cend(),
static_cast<uint64>(0),
[](uint64 prev_sum, const std::pair<uint32, BuylineItemDetails_Struct> &x) {
return prev_sum + x.second.item_cost;
}
);
if (proposed_total_cost > GetCarriedMoney()) {
Message(
Chat::Red,
fmt::format(
"You currently do not have sufficient funds to support your buy lines. You have {} and need {}",
DetermineMoneyString(GetCarriedMoney()),
DetermineMoneyString(proposed_total_cost)).c_str()
);
return 0;
}
return proposed_total_cost;
}
bool Client::DoBarterBuyerChecks(BuyerLineSellItem_Struct &sell_line)
{
bool buyer_error = false;
auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id);
if (!buyer) {
return false;
}
auto buyer_time = BuyerRepository::GetTransactionDate(database, buyer->CharacterID());
if (buyer_time > GetBarterTime()) {
if (sell_line.purchase_method == BarterByVendor) {
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Success,
Barter_DataOutOfDate
);
return false;
}
SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_DataOutOfDate);
return false;
}
for (auto const &ti: sell_line.trade_items) {
auto ti_slot_id = buyer->GetInv().HasItem(
ti.item_id,
ti.item_quantity * sell_line.seller_quantity,
invWherePersonal
);
if (ti_slot_id == INVALID_INDEX) {
LogTradingDetail(
"Seller attempting to sell item <green>[{}] to buyer <green>[{}] though buyer no longer has compensation item <red>[{}]",
sell_line.item_name,
buyer->GetCleanName(),
ti.item_name
);
buyer->Message(
Chat::Red,
fmt::format(
"{} wanted to sell you {} however you no longer have compensation item {}",
sell_line.seller_name,
sell_line.item_name,
ti.item_name
).c_str());
buyer_error = true;
break;
}
}
uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity;
if (!buyer->HasMoney(total_cost)) {
LogTradingDetail(
"Seller attempting to sell item <green>[{}] to buyer <green>[{}] though buyer does not have enough money <red>[{}]",
sell_line.item_name,
buyer->GetCleanName(),
total_cost
);
buyer->Message(
Chat::Red,
fmt::format(
"{} wanted to sell you {} however you have insufficient funds.",
sell_line.seller_name,
sell_line.item_name
).c_str()
);
buyer_error = true;
}
auto buy_item_slot_id = buyer->GetInv().HasItem(
sell_line.item_id,
sell_line.seller_quantity,
invWherePersonal
);
auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : buyer->GetInv().GetItem(buy_item_slot_id);
if (buy_item && buyer->CheckLoreConflict(buy_item->GetItem())) {
LogTradingDetail(
"Seller attempting to sell item <green>[{}] to buyer <green>[{}] though buyer already has the item which is LORE.",
sell_line.item_name,
buyer->GetCleanName()
);
buyer->Message(
Chat::Red,
fmt::format(
"{} wanted to sell you {} however you already have the LORE item.",
sell_line.seller_name,
sell_line.item_name
).c_str()
);
buyer_error = true;
}
if (buyer_error) {
LogTradingDetail("Buyer error <red>[{}] Barter Sell/Buy Transaction Failed.", buyer_error);
SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure);
return false;
}
return true;
}
bool Client::DoBarterSellerChecks(BuyerLineSellItem_Struct &sell_line)
{
bool seller_error = false;
auto sell_item_slot_id = GetInv().HasItem(sell_line.item_id, sell_line.seller_quantity, invWherePersonal);
auto sell_item = sell_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(sell_item_slot_id);
if (!sell_item) {
seller_error = true;
LogTradingDetail("Seller no longer has item <red>[{}] to sell to buyer <red>[{}]",
sell_line.item_name,
sell_line.buyer_name
);
SendBarterBuyerClientMessage(
sell_line,
Barter_SellerTransactionComplete,
Barter_Failure,
Barter_SellerDoesNotHaveItem
);
}
if (sell_item && sell_item->IsAugmentable() && sell_item->IsAugmented()) {
seller_error = true;
LogTradingDetail("Seller item <red>[{}] is augmented therefore cannot be sold.",
sell_line.item_name
);
Message(Chat::Red, "The item that you are trying to sell is augmented. Please remove augments first");
}
if (sell_item && !sell_item->IsDroppable()) {
seller_error = true;
LogTradingDetail("Seller item <red>[{}] is non-tradeable therefore cannot be sold.",
sell_line.item_name
);
Message(Chat::Red, "The item that you are trying to sell is non-tradeable and therefore cannot be sold.");
}
if (seller_error) {
LogTradingDetail("Seller Error <red>[{}] Barter Sell/Buy Transaction Failed.", seller_error);
SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure);
return false;
}
return true;
}
void Client::CancelBuyerTradeWindow()
{
auto end_session = new EQApplicationPacket(OP_Barter, sizeof(BuyerRemoveItemFromMerchantWindow_Struct));
auto data = reinterpret_cast<BuyerRemoveItemFromMerchantWindow_Struct *>(end_session->pBuffer);
data->action = Barter_BuyerInspectBegin;
FastQueuePacket(&end_session);
}
void Client::CancelTraderTradeWindow()
{
auto end_session = new EQApplicationPacket(OP_ShopEnd);
FastQueuePacket(&end_session);
}
void Client::AddDataToMerchantList(int16 slot_id, uint32 item_id, int32 quantity, const std::string &item_unique_id)
{
auto list = GetTraderMerchantList();
list->emplace(std::pair(slot_id, std::make_tuple(item_id, quantity, item_unique_id)));
}
int16 Client::GetNextFreeSlotFromMerchantList()
{
auto list = GetTraderMerchantList();
for (auto const &[slot_id, merchant_data] : *list) {
auto [item_id, quantity, item_unique_id] = merchant_data;
if (item_id == 0) {
return slot_id;
}
}
if (list->size() == GetInv().GetLookup()->InventoryTypeSize.Bazaar) {
return INVALID_INDEX;
}
return list->size() + 1;
}
std::tuple<uint32, int32, std::string> Client::GetDataFromMerchantListByMerchantSlotId(int16 slot_id)
{
auto list = GetTraderMerchantList();
return list->contains(slot_id) ? list->at(slot_id) : std::make_tuple(0, 0, "0000000000000000");
}
int16 Client::GetSlotFromMerchantListByItemUniqueId(const std::string &unique_id)
{
auto list = GetTraderMerchantList();
for (auto [slot_id, merchant_data] : *list) {
auto [item_id, quantity, item_unique_id] = merchant_data;
if (item_unique_id == unique_id) {
return slot_id;
}
}
return INVALID_INDEX;
}
std::pair<int16, std::tuple<uint32, int32, std::string>> Client::GetDataFromMerchantListByItemUniqueId(const std::string &unique_id)
{
auto list = GetTraderMerchantList();
for (auto [slot_id, merchant_data] : *list) {
auto [item_id, quantity, item_unique_id] = merchant_data;
if (item_unique_id == unique_id) {
return { slot_id, merchant_data };
}
}
return std::make_pair(INVALID_INDEX, std::make_tuple(0, 0, "0000000000000000"));
}