eqemu-server/zone/trading.cpp
JJ ac24c9bf5a
[Bug Fix] Fix trader mode (#4397)
* Fix bazaar trading

* Update `constexpr`

* Added world trader table truncate on boot to ensure that the trader table is always empty when world starts.

---------

Co-authored-by: Mitch Freeman <65987027+neckkola@users.noreply.github.com>
2024-06-16 20:55:14 -04:00

3603 lines
113 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 "client.h"
#include "entity.h"
#include "mob.h"
#include "quest_parser_collection.h"
#include "string_ids.h"
#include "worldserver.h"
#include "../common/bazaar.h"
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 && tradingWith->IsClient()) {
Client * other = tradingWith->CastToClient();
PlayerLogTrade_Struct * qs_audit = nullptr;
bool qs_log = false;
if(other) {
LogTrading("Finishing trade with client [{}]", other->GetName());
AddMoneyToPP(other->trade->cp, other->trade->sp, other->trade->gp, other->trade->pp, true);
// step 0: pre-processing
// QS code
if (RuleB(QueryServ, PlayerLogTrades) && event_entry && event_details) {
qs_audit = (PlayerLogTrade_Struct*)event_entry;
qs_log = true;
if (finalizer) {
qs_audit->character_2_id = character_id;
qs_audit->character_2_money.platinum = trade->pp;
qs_audit->character_2_money.gold = trade->gp;
qs_audit->character_2_money.silver = trade->sp;
qs_audit->character_2_money.copper = trade->cp;
}
else {
qs_audit->character_1_id = character_id;
qs_audit->character_1_money.platinum = trade->pp;
qs_audit->character_1_money.gold = trade->gp;
qs_audit->character_1_money.silver = trade->sp;
qs_audit->character_1_money.copper = trade->cp;
}
}
// 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)) {
LogTrading("Container [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID);
if (qs_log) {
auto detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = trade_slot;
detail->to_character_id = other->CharacterID();
detail->to_slot = free_slot;
detail->item_id = inst->GetID();
detail->charges = 1;
detail->aug_1 = inst->GetAugmentItemID(1);
detail->aug_2 = inst->GetAugmentItemID(2);
detail->aug_3 = inst->GetAugmentItemID(3);
detail->aug_4 = inst->GetAugmentItemID(4);
detail->aug_5 = inst->GetAugmentItemID(5);
event_details->push_back(detail);
if (finalizer)
qs_audit->character_2_item_count += detail->charges;
else
qs_audit->character_1_item_count += detail->charges;
for (uint8 sub_slot = EQ::invbag::SLOT_BEGIN; (sub_slot <= EQ::invbag::SLOT_END); ++sub_slot) { // this is to catch ALL items
const EQ::ItemInstance* bag_inst = inst->GetItem(sub_slot);
if (bag_inst) {
detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = EQ::InventoryProfile::CalcSlotId(trade_slot, sub_slot);
detail->to_character_id = other->CharacterID();
detail->to_slot = EQ::InventoryProfile::CalcSlotId(free_slot, sub_slot);
detail->item_id = bag_inst->GetID();
detail->charges = (!bag_inst->IsStackable() ? 1 : bag_inst->GetCharges());
detail->aug_1 = bag_inst->GetAugmentItemID(1);
detail->aug_2 = bag_inst->GetAugmentItemID(2);
detail->aug_3 = bag_inst->GetAugmentItemID(3);
detail->aug_4 = bag_inst->GetAugmentItemID(4);
detail->aug_5 = bag_inst->GetAugmentItemID(5);
event_details->push_back(detail);
if (finalizer)
qs_audit->character_2_item_count += detail->charges;
else
qs_audit->character_1_item_count += detail->charges;
}
}
}
}
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()));
if (qs_log) {
auto detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = trade_slot;
detail->to_character_id = other->CharacterID();
detail->to_slot = partial_slot;
detail->item_id = inst->GetID();
detail->charges = (old_charges - inst->GetCharges());
detail->aug_1 = 0;
detail->aug_2 = 0;
detail->aug_3 = 0;
detail->aug_4 = 0;
detail->aug_5 = 0;
event_details->push_back(detail);
if (finalizer)
qs_audit->character_2_item_count += detail->charges;
else
qs_audit->character_1_item_count += detail->charges;
}
}
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 (qs_log) {
auto detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = trade_slot;
detail->to_character_id = character_id;
detail->to_slot = bias_slot;
detail->item_id = inst->GetID();
detail->charges = (old_charges - inst->GetCharges());
detail->aug_1 = 0;
detail->aug_2 = 0;
detail->aug_3 = 0;
detail->aug_4 = 0;
detail->aug_5 = 0;
event_details->push_back(detail);
}
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)) {
LogTrading("Item [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID);
if (qs_log) {
auto detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = trade_slot;
detail->to_character_id = other->CharacterID();
detail->to_slot = free_slot;
detail->item_id = inst->GetID();
detail->charges = (!inst->IsStackable() ? 1 : inst->GetCharges());
detail->aug_1 = inst->GetAugmentItemID(1);
detail->aug_2 = inst->GetAugmentItemID(2);
detail->aug_3 = inst->GetAugmentItemID(3);
detail->aug_4 = inst->GetAugmentItemID(4);
detail->aug_5 = inst->GetAugmentItemID(5);
event_details->push_back(detail);
if (finalizer)
qs_audit->character_2_item_count += detail->charges;
else
qs_audit->character_1_item_count += detail->charges;
// 'step 3' should never really see containers..but, just in case...
for (uint8 sub_slot = EQ::invbag::SLOT_BEGIN; (sub_slot <= EQ::invbag::SLOT_END); ++sub_slot) { // this is to catch ALL items
const EQ::ItemInstance* bag_inst = inst->GetItem(sub_slot);
if (bag_inst) {
detail = new PlayerLogTradeItemsEntry_Struct;
detail->from_character_id = character_id;
detail->from_slot = trade_slot;
detail->to_character_id = other->CharacterID();
detail->to_slot = free_slot;
detail->item_id = bag_inst->GetID();
detail->charges = (!bag_inst->IsStackable() ? 1 : bag_inst->GetCharges());
detail->aug_1 = bag_inst->GetAugmentItemID(1);
detail->aug_2 = bag_inst->GetAugmentItemID(2);
detail->aug_3 = bag_inst->GetAugmentItemID(3);
detail->aug_4 = bag_inst->GetAugmentItemID(4);
detail->aug_5 = bag_inst->GetAugmentItemID(5);
event_details->push_back(detail);
if (finalizer)
qs_audit->character_2_item_count += detail->charges;
else
qs_audit->character_1_item_count += detail->charges;
}
}
}
}
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 && tradingWith->IsNPC()) {
QSPlayerLogHandin_Struct* qs_audit = nullptr;
bool qs_log = false;
// QS code
if(RuleB(QueryServ, PlayerLogTrades) && event_entry && event_details) {
// Currently provides only basic functionality. Calling method will also
// need to be modified before item returns and rewards can be logged.
qs_audit = (QSPlayerLogHandin_Struct*)event_entry;
qs_log = true;
qs_audit->quest_id = 0;
qs_audit->char_id = character_id;
qs_audit->char_money.platinum = trade->pp;
qs_audit->char_money.gold = trade->gp;
qs_audit->char_money.silver = trade->sp;
qs_audit->char_money.copper = trade->cp;
qs_audit->char_count = 0;
qs_audit->npc_id = tradingWith->GetNPCTypeID();
qs_audit->npc_money.platinum = 0;
qs_audit->npc_money.gold = 0;
qs_audit->npc_money.silver = 0;
qs_audit->npc_money.copper = 0;
qs_audit->npc_count = 0;
}
if(qs_log) { // This can be incorporated below when revisions are made
for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_NPC_END; ++trade_slot) {
const EQ::ItemInstance* trade_inst = m_inv[trade_slot];
if(trade_inst) {
auto detail = new QSHandinItems_Struct;
strcpy(detail->action_type, "HANDIN");
detail->char_slot = trade_slot;
detail->item_id = trade_inst->GetID();
detail->charges = (!trade_inst->IsStackable() ? 1 : trade_inst->GetCharges());
detail->aug_1 = trade_inst->GetAugmentItemID(1);
detail->aug_2 = trade_inst->GetAugmentItemID(2);
detail->aug_3 = trade_inst->GetAugmentItemID(3);
detail->aug_4 = trade_inst->GetAugmentItemID(4);
detail->aug_5 = trade_inst->GetAugmentItemID(5);
event_details->push_back(detail);
qs_audit->char_count += detail->charges;
if (trade_inst->IsClassBag()) {
for (uint8 sub_slot = EQ::invbag::SLOT_BEGIN; sub_slot < trade_inst->GetItem()->BagSlots; ++sub_slot) {
const EQ::ItemInstance* trade_baginst = trade_inst->GetItem(sub_slot);
if(trade_baginst) {
detail = new QSHandinItems_Struct;
strcpy(detail->action_type, "HANDIN");
detail->char_slot = EQ::InventoryProfile::CalcSlotId(trade_slot, sub_slot);
detail->item_id = trade_baginst->GetID();
detail->charges = (!trade_inst->IsStackable() ? 1 : trade_inst->GetCharges());
detail->aug_1 = trade_baginst->GetAugmentItemID(1);
detail->aug_2 = trade_baginst->GetAugmentItemID(2);
detail->aug_3 = trade_baginst->GetAugmentItemID(3);
detail->aug_4 = trade_baginst->GetAugmentItemID(4);
detail->aug_5 = trade_baginst->GetAugmentItemID(5);
event_details->push_back(detail);
qs_audit->char_count += detail->charges;
}
}
}
}
}
}
bool quest_npc = false;
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
// This is a quest NPC
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);
EVENT_ITEM_ScriptStopReturn();
}
}
// Regardless of quest or non-quest NPC - No in combat trade completion
// is allowed.
if (tradingWith->CheckAggro(this))
{
for (EQ::ItemInstance* inst : items) {
if (!inst || !inst->GetItem()) {
continue;
}
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*inst, true);
}
}
// Only enforce trade rules if the NPC doesn't have an EVENT_TRADE
// subroutine. That overrides all.
else if (!quest_npc)
{
for (EQ::ItemInstance* 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
}
}
const EQ::ItemData* item = inst->GetItem();
const bool is_pet = _CLIENTPET(tradingWith) && tradingWith->GetPetType()<=petOther;
const bool is_quest_npc = tradingWith->CastToNPC()->IsQuestNPC();
const bool restrict_quest_items_to_quest_npc = RuleB(NPC, ReturnQuestItemsFromNonQuestNPCs);
const bool pets_can_take_quest_items = RuleB(Pets, CanTakeQuestItems);
const bool is_pet_and_can_have_nodrop_items = (RuleB(Pets, CanTakeNoDrop) && is_pet);
const bool is_pet_and_can_have_quest_items = (pets_can_take_quest_items && is_pet);
// if it was not a NO DROP or Attuned item (or if a GM is trading), let the NPC have it
if (GetGM() ||
(!restrict_quest_items_to_quest_npc || (is_quest_npc && item->IsQuestItem()) || !item->IsQuestItem()) && // If rule is enabled, return any quest items given to non-quest NPCs
(((item->NoDrop != 0 && !inst->IsAttuned()) || is_pet_and_can_have_nodrop_items) &&
((!item->IsQuestItem() || is_pet_and_can_have_quest_items || !is_pet)))) {
// pets need to look inside bags and try to equip items found there
if (item->IsClassBag() && item->BagSlots > 0) {
for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) {
const EQ::ItemInstance *baginst = inst->GetItem(bslot);
if (baginst) {
const EQ::ItemData *bagitem = baginst->GetItem();
if (bagitem && (GetGM() ||
(!restrict_quest_items_to_quest_npc ||
(is_quest_npc && bagitem->IsQuestItem()) || !bagitem->IsQuestItem()) &&
// If rule is enabled, return any quest items given to non-quest NPCs (inside bags)
(bagitem->NoDrop != 0 && !baginst->IsAttuned()) &&
((is_pet && (!bagitem->IsQuestItem() || pets_can_take_quest_items) ||
!is_pet)))) {
if (GetGM()) {
const std::string& item_link = database.CreateItemLink(bagitem->ID);
Message(
Chat::White,
fmt::format(
"Your GM flag allows you to give {} to {}.",
item_link,
GetTargetDescription(tradingWith)
).c_str()
);
}
auto loot_drop_entry = LootdropEntriesRepository::NewNpcEntity();
loot_drop_entry.equip_item = 1;
loot_drop_entry.item_charges = static_cast<int8>(baginst->GetCharges());
tradingWith->CastToNPC()->AddLootDrop(
bagitem,
loot_drop_entry,
true
);
// Return quest items being traded to non-quest NPC when the rule is true
} else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && bagitem->IsQuestItem())) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*baginst, true);
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
// Return quest items being traded to player pet when not allowed
} else if (is_pet && bagitem->IsQuestItem() && !pets_can_take_quest_items) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*baginst, true);
Message(Chat::Red, "You cannot trade quest items with your pet.");
} else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*baginst, true);
}
}
}
}
auto new_loot_drop_entry = LootdropEntriesRepository::NewNpcEntity();
new_loot_drop_entry.equip_item = 1;
new_loot_drop_entry.item_charges = static_cast<int8>(inst->GetCharges());
tradingWith->CastToNPC()->AddLootDrop(
item,
new_loot_drop_entry,
true
);
}
// Return quest items being traded to non-quest NPC when the rule is true
else if (restrict_quest_items_to_quest_npc && (!is_quest_npc && item->IsQuestItem())) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*inst, true);
Message(Chat::Red, "You can only trade quest items to quest NPCs.");
}
// Return quest items being traded to player pet when not allowed
else if (is_pet && item->IsQuestItem()) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*inst, true);
Message(Chat::Red, "You cannot trade quest items with your pet.");
}
// Return NO DROP and Attuned items being handed into a non-quest NPC if the rule is true
else if (RuleB(NPC, ReturnNonQuestNoDropItems)) {
tradingWith->SayString(TRADE_BACK, GetCleanName());
PushItemOnCursor(*inst, true);
}
}
}
char temp1[100] = { 0 };
char temp2[100] = { 0 };
snprintf(temp1, 100, "copper.%d", tradingWith->GetNPCTypeID());
snprintf(temp2, 100, "%u", trade->cp);
parse->AddVar(temp1, temp2);
snprintf(temp1, 100, "silver.%d", tradingWith->GetNPCTypeID());
snprintf(temp2, 100, "%u", trade->sp);
parse->AddVar(temp1, temp2);
snprintf(temp1, 100, "gold.%d", tradingWith->GetNPCTypeID());
snprintf(temp2, 100, "%u", trade->gp);
parse->AddVar(temp1, temp2);
snprintf(temp1, 100, "platinum.%d", tradingWith->GetNPCTypeID());
snprintf(temp2, 100, "%u", trade->pp);
parse->AddVar(temp1, temp2);
if(tradingWith->GetAppearance() != eaDead) {
tradingWith->FaceTarget(this);
}
if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) {
std::vector<std::any> item_list(items.begin(), items.end());
parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list);
}
for(int i = 0; i < 4; ++i) {
if(insts[i]) {
safe_delete(insts[i]);
}
}
}
}
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()
{
auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_Struct));
auto data = (Trader_Struct *) outapp->pBuffer;
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID()));
uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ?
GetInv().GetLookup()->InventoryTypeSize.Bazaar :
trader_items.size();
for (int i = 0; i < item_limit; i++) {
data->item_cost[i] = trader_items.at(i).item_cost;
data->items[i] = ClientVersion() == EQ::versions::ClientVersion::RoF2 ? trader_items.at(i).item_sn
: trader_items.at(i).item_id;
}
data->action = ListTraderItems;
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 in = (ClickTrader_Struct *) app->pBuffer;
auto inv = GetTraderItems();
bool trade_items_valid = true;
std::vector<TraderRepository::Trader> trader_items{};
//Check inventory for no-trade items
for (auto i = 0; i < max_items; i++) {
if (inv->items[i] == 0 || inv->serial_number[i] == 0) {
continue;
}
auto inst = FindTraderItemBySerialNumber(inv->serial_number[i]);
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;
}
}
}
for (uint32 i = 0; i < max_items; i++) {
if (inv->serial_number[i] <= 0) {
continue;
}
auto inst = FindTraderItemBySerialNumber(inv->serial_number[i]);
if (!inst) {
trade_items_valid = false;
break;
}
auto it = std::find(std::begin(in->serial_number), std::end(in->serial_number), inv->serial_number[i]);
if (inst && it != std::end(in->serial_number)) {
inst->SetPrice(in->item_cost[i]);
TraderRepository::Trader trader_item{};
trader_item.id = 0;
trader_item.char_entity_id = GetID();
trader_item.char_id = CharacterID();
trader_item.char_zone_id = GetZoneID();
trader_item.item_charges = inst->GetCharges() == 0 ? 1 : inst->GetCharges();
trader_item.item_cost = inst->GetPrice();
trader_item.item_id = inst->GetID();
trader_item.item_sn = in->serial_number[i];
trader_item.slot_id = i;
if (inst->IsAugmented()) {
auto augs = inst->GetAugmentIDs();
trader_item.aug_slot_1 = augs.at(0);
trader_item.aug_slot_2 = augs.at(1);
trader_item.aug_slot_3 = augs.at(2);
trader_item.aug_slot_4 = augs.at(3);
trader_item.aug_slot_5 = augs.at(4);
trader_item.aug_slot_6 = augs.at(5);
}
trader_items.emplace_back(trader_item);
continue;
}
else if (inst) {
Message(
Chat::Red,
fmt::format(
"Item: {} has no price set. Please set a price and try again.",
inst->GetItem()->Name
).c_str()
);
trade_items_valid = false;
continue;
}
}
if (!trade_items_valid) {
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("`char_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);
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("`char_id` = '{}'", CharacterID()));
SendBecomeTraderToWorld(this, TraderOff);
SendTraderMode(TraderOff);
WithCustomer(0);
SetTrader(false);
}
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.aug_slot_1,
t.aug_slot_2,
t.aug_slot_3,
t.aug_slot_4,
t.aug_slot_5,
t.aug_slot_6
)
);
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 char_id, int serial_number)
{
auto inst = database.LoadSingleTraderItem(char_id, serial_number);
if (inst) {
SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor?
}
}
void Client::BulkSendTraderInventory(uint32 char_id)
{
const EQ::ItemData *item;
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", char_id));
uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar ?
GetInv().GetLookup()->InventoryTypeSize.Bazaar :
trader_items.size();
for (uint32 i = 0; i < item_limit; i++) {
if ((trader_items.at(i).item_id == 0) || (trader_items.at(i).item_cost == 0)) {
continue;
}
else {
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).aug_slot_1,
trader_items.at(i).aug_slot_2,
trader_items.at(i).aug_slot_3,
trader_items.at(i).aug_slot_4,
trader_items.at(i).aug_slot_5,
trader_items.at(i).aug_slot_6
)
);
if (inst) {
inst->SetSerialNumber(trader_items.at(i).item_sn);
if (trader_items.at(i).item_charges > 0) {
inst->SetCharges(trader_items.at(i).item_charges);
}
if (inst->IsStackable()) {
inst->SetMerchantCount(trader_items.at(i).item_charges);
inst->SetMerchantSlot(trader_items.at(i).item_sn);
}
inst->SetPrice(trader_items.at(i).item_cost);
SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant);
// safe_delete(inst);
}
else
LogTrading("Client::BulkSendTraderInventory nullptr inst pointer");
}
}
}
uint32 Client::FindTraderItemSerialNumber(int32 ItemID) {
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++) {
// we already have the parent bag and a contents iterator..why not just iterate the bag!??
SlotID = EQ::InventoryProfile::CalcSlotId(i, x);
item = GetInv().GetItem(SlotID);
if (item) {
if (item->GetID() == ItemID)
return item->GetSerialNumber();
}
}
}
}
LogTrading("Client::FindTraderItemSerialNumber Couldn't find item! Item ID [{}]", ItemID);
return 0;
}
EQ::ItemInstance *Client::FindTraderItemBySerialNumber(int32 SerialNumber)
{
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->GetSerialNumber() == SerialNumber) {
return item;
}
}
}
}
}
LogTrading("Couldn't find item! Serial No. was [{}]", SerialNumber);
return nullptr;
}
GetItems_Struct *Client::GetTraderItems()
{
const EQ::ItemInstance *item = nullptr;
int16 slot_id = INVALID_INDEX;
auto gis = new GetItems_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->GetSerialNumber();
gis->charges[ndx] = item->GetCharges() == 0 ? 1 : item->GetCharges();
ndx++;
}
}
}
}
return gis;
}
uint16 Client::FindTraderItem(int32 SerialNumber, 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->GetSerialNumber() == SerialNumber &&
(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",
SerialNumber , Quantity, GetName());
return 0;
}
void Client::NukeTraderItem(
uint16 slot,
int16 charges,
int16 quantity,
Client *customer,
uint16 trader_slot,
int32 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 = serial_number;
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(int32 serial_number, int16 quantity, Client *customer, uint16 trader_slot)
{
const EQ::ItemInstance *item = nullptr;
bool stackable = false;
int16 charges = 0;
uint16 slot_id = FindTraderItem(serial_number, quantity);
if (slot_id > 0) {
item = GetInv().GetItem(slot_id);
if (!item) {
LogTrading("Could not find Item: [{}] on Trader: [{}]", serial_number, 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("`char_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 && trader_items.at(i).item_sn == serial_number) {
delete_queue.push_back(trader_items.at(i));
NukeTraderItem(
slot_id,
charges,
quantity,
customer,
trader_slot,
trader_items.at(i).item_sn,
trader_items.at(i).item_id
);
test_slot = false;
}
else if (trader_items.at(i).item_id > 0) {
count++;
}
}
TraderRepository::DeleteMany(database, delete_queue);
if (count == 0) {
TraderEndTrader();
}
return;
}
else {
TraderRepository::UpdateQuantity(database, CharacterID(), item->GetSerialNumber(), charges - quantity);
NukeTraderItem(slot_id, charges, quantity, customer, trader_slot, item->GetSerialNumber(), item->GetID());
return;
}
}
LogTrading("Could NOT find a match for Item: <red>[{}] with a quantity of: <red>[{}] on Trader: <red>[{}]\n",
serial_number,
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);
}
static void BazaarAuditTrail(const char *seller, const char *buyer, const char *itemName, int quantity, int totalCost, int tranType) {
const std::string& query = fmt::format(
"INSERT INTO `trader_audit` "
"(`time`, `seller`, `buyer`, `itemname`, `quantity`, `totalcost`, `trantype`) "
"VALUES (NOW(), '{}', '{}', '{}', {}, {}, {})",
seller,
buyer,
Strings::Escape(itemName),
quantity,
totalCost,
tranType
);
database.QueryDatabase(query);
}
void Client::BuyTraderItem(TraderBuy_Struct *tbs, Client *Trader, const EQApplicationPacket *app)
{
if (!Trader) {
return;
}
if (!Trader->IsTrader()) {
TradeRequestFailed(app);
return;
}
auto in = (TraderBuy_Struct *) app->pBuffer;
auto outapp = std::make_unique<EQApplicationPacket>(OP_Trader, sizeof(TraderBuy_Struct));
auto outtbs = (TraderBuy_Struct *) outapp->pBuffer;
outtbs->item_id = tbs->item_id;
const EQ::ItemInstance *buy_item = nullptr;
uint32 item_id = 0;
if (ClientVersion() >= EQ::versions::ClientVersion::RoF) {
tbs->item_id = Strings::ToUnsignedBigInt(tbs->serial_number);
}
buy_item = Trader->FindTraderItemBySerialNumber(tbs->item_id);
if (!buy_item) {
LogTrading("Unable to find item id <red>[{}] item_sn <red>[{}] on trader", tbs->item_id, tbs->serial_number);
TradeRequestFailed(app);
return;
}
LogTrading(
"Name: <green>[{}] IsStackable: <green>[{}] Requested Quantity: <green>[{}] Charges on Item <green>[{}]",
buy_item->GetItem()->Name,
buy_item->IsStackable(),
tbs->quantity,
buy_item->GetCharges()
);
// If the item is not stackable, then we can only be buying one of them.
if (!buy_item->IsStackable()) {
outtbs->quantity = 1; // normally you can't send more than 1 here
}
else {
// Stackable items, arrows, diamonds, etc
int32 item_charges = buy_item->GetCharges();
// ItemCharges for stackables should not be <= 0
if (item_charges <= 0) {
outtbs->quantity = 1;
// If the purchaser requested more than is in the stack, just sell them how many are actually in the stack.
}
else if (static_cast<uint32>(item_charges) < tbs->quantity) {
outtbs->quantity = item_charges;
}
else {
outtbs->quantity = tbs->quantity;
}
}
LogTrading("Actual quantity that will be traded is <green>[{}]", outtbs->quantity);
if ((tbs->price * outtbs->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>[{}]",
GetName(),
Trader->GetName(),
buy_item->GetItem()->Name,
buy_item->GetCharges(),
tbs->quantity,
tbs->price
);
TradeRequestFailed(app);
return;
}
uint64 total_transaction_value = static_cast<uint64>(tbs->price) * static_cast<uint64>(outtbs->quantity);
if (total_transaction_value > MAX_TRANSACTION_VALUE) {
Message(
Chat::Red,
"That would exceed the single transaction limit of %u platinum.",
MAX_TRANSACTION_VALUE / 1000
);
TradeRequestFailed(app);
return;
}
// This cannot overflow assuming MAX_TRANSACTION_VALUE, checked above, is the default of 2000000000
uint32 total_cost = tbs->price * outtbs->quantity;
if (Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) {
// RoF+ uses individual item price where older clients use total price
outtbs->price = tbs->price;
}
else {
outtbs->price = total_cost;
}
if (!TakeMoneyFromPP(total_cost)) {
RecordPlayerEventLog(
PlayerEvent::POSSIBLE_HACK,
PlayerEvent::PossibleHackEvent{
.message = "Attempted to buy something in bazaar but did not have enough money."
}
);
TradeRequestFailed(app);
return;
}
LogTrading("Customer Paid: <green>[{}] in Copper", total_cost);
uint32 platinum = total_cost / 1000;
total_cost -= (platinum * 1000);
uint32 gold = total_cost / 100;
total_cost -= (gold * 100);
uint32 silver = total_cost / 10;
total_cost -= (silver * 10);
uint32 copper = total_cost;
Trader->AddMoneyToPP(copper, silver, gold, platinum, true);
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
auto e = PlayerEvent::TraderPurchaseEvent{
.item_id = buy_item->GetID(),
.item_name = buy_item->GetItem()->Name,
.trader_id = Trader->CharacterID(),
.trader_name = Trader->GetCleanName(),
.price = tbs->price,
.charges = outtbs->quantity,
.total_cost = (tbs->price * outtbs->quantity),
.player_money_balance = GetCarriedMoney(),
};
RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e);
}
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_SELL)) {
auto e = PlayerEvent::TraderSellEvent{
.item_id = buy_item->GetID(),
.item_name = buy_item->GetItem()->Name,
.buyer_id = CharacterID(),
.buyer_name = GetCleanName(),
.price = tbs->price,
.charges = outtbs->quantity,
.total_cost = (tbs->price * outtbs->quantity),
.player_money_balance = Trader->GetCarriedMoney(),
};
RecordPlayerEventLogWithClient(Trader, PlayerEvent::TRADER_SELL, e);
}
LogTrading("Trader Received: [{}] Platinum, [{}] Gold, [{}] Silver, [{}] Copper", platinum, gold, silver, copper);
ReturnTraderReq(app, outtbs->quantity, item_id);
outtbs->trader_id = GetID();
outtbs->action = BazaarBuyItem;
strn0cpy(outtbs->seller_name, Trader->GetCleanName(), sizeof(outtbs->seller_name));
strn0cpy(outtbs->buyer_name, GetCleanName(), sizeof(outtbs->buyer_name));
strn0cpy(outtbs->item_name, buy_item->GetItem()->Name, sizeof(outtbs->item_name));
strn0cpy(
outtbs->serial_number,
fmt::format("{:016}", buy_item->GetSerialNumber()).c_str(),
sizeof(outtbs->serial_number)
);
TraderRepository::Trader t{};
t.item_charges = buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges();
t.item_id = buy_item->GetItem()->ID;
t.aug_slot_1 = buy_item->GetAugmentItemID(0);
t.aug_slot_2 = buy_item->GetAugmentItemID(1);
t.aug_slot_3 = buy_item->GetAugmentItemID(2);
t.aug_slot_4 = buy_item->GetAugmentItemID(3);
t.aug_slot_5 = buy_item->GetAugmentItemID(4);
t.aug_slot_6 = buy_item->GetAugmentItemID(5);
t.char_id = CharacterID();
t.slot_id = FindNextFreeParcelSlot(CharacterID());
SendTraderItem(
buy_item->GetItem()->ID,
buy_item->IsStackable() ? outtbs->quantity : buy_item->GetCharges(),
t
);
if (RuleB(Bazaar, AuditTrail)) {
BazaarAuditTrail(Trader->GetName(), GetName(), buy_item->GetItem()->Name, outtbs->quantity, outtbs->price, 0);
}
Trader->FindAndNukeTraderItem(tbs->item_id, outtbs->quantity, this, 0);
if (item_id > 0 && Trader->ClientVersion() >= EQ::versions::ClientVersion::RoF) {
// Convert Serial Number back to ItemID for RoF+
outtbs->item_id = item_id;
}
Trader->QueuePacket(outapp.get());
}
void Client::SendBazaarWelcome()
{
const auto results = TraderRepository::GetWelcomeData(database);
auto outapp = std::make_unique<EQApplicationPacket>(OP_BazaarSearch, 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.get());
}
void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria)
{
auto results = Bazaar::GetSearchResults(database, search_criteria, GetZoneID());
if (results.empty()) {
SendBazaarDone(GetID());
return;
}
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();
}
void Client::SendBazaarResults(
uint32 trader_id,
uint32 in_class,
uint32 in_race,
uint32 item_stat,
uint32 item_slot,
uint32 item_type,
char item_name[64],
uint32 min_price,
uint32 max_price
)
{
std::string search_values = " COUNT(item_id), trader.*, items.name ";
std::string search_criteria = " WHERE trader.item_id = items.id ";
if (trader_id > 0) {
Client *trader = entity_list.GetClientByID(trader_id);
if (trader) {
search_criteria.append(StringFormat(" AND trader.char_id = %i", trader->CharacterID()));
}
}
if (min_price != 0) {
search_criteria.append(StringFormat(" AND trader.item_cost >= %i", min_price));
}
if (max_price != 0) {
search_criteria.append(StringFormat(" AND trader.item_cost <= %i", max_price));
}
if (strlen(item_name) > 0) {
char *safeName = RemoveApostrophes(item_name);
search_criteria.append(StringFormat(" AND items.name LIKE '%%%s%%'", safeName));
safe_delete_array(safeName);
}
if (in_class != 0xFFFFFFFF) {
search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.classes)), %i, 1) = 1", in_class));
}
if (in_race != 0xFFFFFFFF) {
search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.races)), %i, 1) = 1", in_race));
}
if (item_slot != 0xFFFFFFFF) {
search_criteria.append(StringFormat(" AND MID(REVERSE(BIN(items.slots)), %i, 1) = 1", item_slot + 1));
}
switch (item_type) {
case 0xFFFFFFFF:
break;
case 0:
// 1H Slashing
search_criteria.append(" AND items.itemtype = 0 AND damage > 0");
break;
case 31:
search_criteria.append(" AND items.itemclass = 2");
break;
case 46:
search_criteria.append(" AND items.scrolleffect > 0 AND items.scrolleffect < 65000");
break;
case 47:
search_criteria.append(" AND items.worneffect = 998");
break;
case 48:
search_criteria.append(" AND items.worneffect >= 1298 AND items.worneffect <= 1307");
break;
case 49:
search_criteria.append(" AND items.focuseffect > 0");
break;
default:
search_criteria.append(StringFormat(" AND items.itemtype = %i", item_type));
}
switch (item_stat) {
case STAT_AC:
search_criteria.append(" AND items.ac > 0");
search_values.append(", items.ac");
break;
case STAT_AGI:
search_criteria.append(" AND items.aagi > 0");
search_values.append(", items.aagi");
break;
case STAT_CHA:
search_criteria.append(" AND items.acha > 0");
search_values.append(", items.acha");
break;
case STAT_DEX:
search_criteria.append(" AND items.adex > 0");
search_values.append(", items.adex");
break;
case STAT_INT:
search_criteria.append(" AND items.aint > 0");
search_values.append(", items.aint");
break;
case STAT_STA:
search_criteria.append(" AND items.asta > 0");
search_values.append(", items.asta");
break;
case STAT_STR:
search_criteria.append(" AND items.astr > 0");
search_values.append(", items.astr");
break;
case STAT_WIS:
search_criteria.append(" AND items.awis > 0");
search_values.append(", items.awis");
break;
case STAT_COLD:
search_criteria.append(" AND items.cr > 0");
search_values.append(", items.cr");
break;
case STAT_DISEASE:
search_criteria.append(" AND items.dr > 0");
search_values.append(", items.dr");
break;
case STAT_FIRE:
search_criteria.append(" AND items.fr > 0");
search_values.append(", items.fr");
break;
case STAT_MAGIC:
search_criteria.append(" AND items.mr > 0");
search_values.append(", items.mr");
break;
case STAT_POISON:
search_criteria.append(" AND items.pr > 0");
search_values.append(", items.pr");
break;
case STAT_HP:
search_criteria.append(" AND items.hp > 0");
search_values.append(", items.hp");
break;
case STAT_MANA:
search_criteria.append(" AND items.mana > 0");
search_values.append(", items.mana");
break;
case STAT_ENDURANCE:
search_criteria.append(" AND items.endur > 0");
search_values.append(", items.endur");
break;
case STAT_ATTACK:
search_criteria.append(" AND items.attack > 0");
search_values.append(", items.attack");
break;
case STAT_HP_REGEN:
search_criteria.append(" AND items.regen > 0");
search_values.append(", items.regen");
break;
case STAT_MANA_REGEN:
search_criteria.append(" AND items.manaregen > 0");
search_values.append(", items.manaregen");
break;
case STAT_HASTE:
search_criteria.append(" AND items.haste > 0");
search_values.append(", items.haste");
break;
case STAT_DAMAGE_SHIELD:
search_criteria.append(" AND items.damageshield > 0");
search_values.append(", items.damageshield");
break;
default:
search_values.append(", 0");
break;
}
std::string query = StringFormat(
"SELECT %s, SUM(charges), items.stackable "
"FROM trader, items %s GROUP BY items.id, charges, char_id LIMIT %i",
search_values.c_str(),
search_criteria.c_str(),
RuleI(Bazaar, MaxSearchResults)
);
auto results = database.QueryDatabase(query);
if (!results.Success()) {
return;
}
LogTrading("SRCH: [{}]", query.c_str());
int Size = 0;
uint32 ID = 0;
if (results.RowCount() == static_cast<unsigned long>(RuleI(Bazaar, MaxSearchResults))) {
Message(
Chat::Yellow,
"Your search reached the limit of %i results. Please narrow your search down by selecting more options.",
RuleI(Bazaar, MaxSearchResults));
}
if (results.RowCount() == 0) {
auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct));
BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer;
brds->TraderID = ID;
brds->Type = BazaarSearchDone;
brds->Unknown008 = 0xFFFFFFFF;
brds->Unknown012 = 0xFFFFFFFF;
brds->Unknown016 = 0xFFFFFFFF;
QueuePacket(outapp2);
safe_delete(outapp2);
return;
}
Size = results.RowCount() * sizeof(BazaarSearchResults_Struct);
auto buffer = new uchar[Size];
uchar *bufptr = buffer;
memset(buffer, 0, Size);
int Action = BazaarSearchResults;
uint32 Cost = 0;
int32 SerialNumber = 0;
char temp_buffer[64] = {0};
int Count = 0;
uint32 StatValue = 0;
for (auto &row = results.begin(); row != results.end(); ++row) {
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Action);
Count = Strings::ToInt(row[0]);
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Count);
SerialNumber = Strings::ToInt(row[3]);
VARSTRUCT_ENCODE_TYPE(int32, bufptr, SerialNumber);
Client *Trader2 = entity_list.GetClientByCharID(Strings::ToInt(row[1]));
if (Trader2) {
ID = Trader2->GetID();
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, ID);
}
else {
LogTrading("Unable to find trader: [{}]\n", Strings::ToInt(row[1]));
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, 0);
}
Cost = Strings::ToInt(row[5]);
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Cost);
StatValue = Strings::ToInt(row[8]);
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, StatValue);
bool Stackable = Strings::ToInt(row[10]);
if (Stackable) {
int Charges = Strings::ToInt(row[9]);
sprintf(temp_buffer, "%s(%i)", row[7], Charges);
}
else {
sprintf(temp_buffer, "%s(%i)", row[7], Count);
}
memcpy(bufptr, &temp_buffer, strlen(temp_buffer));
bufptr += 64;
// Extra fields for SoD+
//
if (Trader2) {
sprintf(temp_buffer, "%s", Trader2->GetName());
}
else {
sprintf(temp_buffer, "Unknown");
}
memcpy(bufptr, &temp_buffer, strlen(temp_buffer));
bufptr += 64;
VARSTRUCT_ENCODE_TYPE(uint32, bufptr, Strings::ToInt(row[1])); // ItemID
}
auto outapp = new EQApplicationPacket(OP_BazaarSearch, Size);
memcpy(outapp->pBuffer, buffer, Size);
QueuePacket(outapp);
safe_delete(outapp);
safe_delete_array(buffer);
auto outapp2 = new EQApplicationPacket(OP_BazaarSearch, sizeof(BazaarReturnDone_Struct));
BazaarReturnDone_Struct *brds = (BazaarReturnDone_Struct *) outapp2->pBuffer;
brds->TraderID = ID;
brds->Type = BazaarSearchDone;
brds->Unknown008 = 0xFFFFFFFF;
brds->Unknown012 = 0xFFFFFFFF;
brds->Unknown016 = 0xFFFFFFFF;
QueuePacket(outapp2);
safe_delete(outapp2);
}
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.aug_slot_1,
i.aug_slot_2,
i.aug_slot_3,
i.aug_slot_4,
i.aug_slot_5,
i.aug_slot_6
)
);
if (!inst) {
return;
}
inst->SetCharges(i.item_charges);
inst->SetPrice(i.item_cost);
inst->SetSerialNumber(i.item_sn);
inst->SetMerchantSlot(i.item_sn);
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_sn, 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;
}
else {
tdis->item_id = trader_items.at(i).item_sn;
}
tdis->item_id = trader_items.at(i).item_sn;
LogTrading("Telling customer to remove item [{}] with [{}] charges and S/N [{}]",
item_id, charges, trader_items.at(i).item_sn);
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->aug_slot_1,
it->aug_slot_2,
it->aug_slot_3,
it->aug_slot_4,
it->aug_slot_5,
it->aug_slot_6
)
);
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->SetSerialNumber(trader_items.at(i).item_sn);
inst->SetMerchantSlot(trader_items.at(i).item_sn);
LogTrading("Sending price update for [{}], Serial No. [{}] with [{}] charges",
item->Name, trader_items.at(i).item_sn, trader_items.at(i).item_charges);
customer->SendItemPacket(EQ::invslot::slotCursor, inst.get(), ItemPacketMerchant); // MainCursor??
}
// safe_delete(inst);
}
void Client::SendBuyerResults(char* searchString, uint32 searchID) {
// This method is called when a potential seller in the /barter window searches for matching buyers
//
LogDebug("[CLIENT] Client::SendBuyerResults [{}]\n", searchString);
auto escSearchString = new char[strlen(searchString) * 2 + 1];
database.DoEscapeString(escSearchString, searchString, strlen(searchString));
std::string query = StringFormat("SELECT * FROM buyer WHERE itemname LIKE '%%%s%%' ORDER BY charid LIMIT %i",
escSearchString, RuleI(Bazaar, MaxBarterSearchResults));
safe_delete_array(escSearchString);
auto results = database.QueryDatabase(query);
if (!results.Success()) {
return;
}
int numberOfRows = results.RowCount();
if(numberOfRows == RuleI(Bazaar, MaxBarterSearchResults))
Message(Chat::Yellow, "Your search found too many results; some are not displayed.");
else if(strlen(searchString) == 0)
Message(Chat::NPCQuestSay, "There are %i Buy Lines.", numberOfRows);
else
Message(Chat::NPCQuestSay, "There are %i Buy Lines that match the search string '%s'.", numberOfRows, searchString);
if(numberOfRows == 0)
return;
uint32 lastCharID = 0;
Client *buyer = nullptr;
for (auto &row = results.begin(); row != results.end(); ++row) {
char itemName[64];
uint32 charID = Strings::ToInt(row[0]);
uint32 buySlot = Strings::ToInt(row[1]);
uint32 itemID = Strings::ToInt(row[2]);
strcpy(itemName, row[3]);
uint32 quantity = Strings::ToInt(row[4]);
uint32 price = Strings::ToInt(row[5]);
// Each item in the search results is sent as a single fixed length packet, although the position of
// the fields varies due to the use of variable length strings. The reason the packet is so big, is
// to allow item compensation, e.g. a buyer could offer to buy a Blade Of Carnage for 10000pp plus
// other items in exchange. Item compensation is not currently supported in EQEmu.
//
auto outapp = new EQApplicationPacket(OP_Barter, 940);
char *buf = (char *)outapp->pBuffer;
const EQ::ItemData* item = database.GetItem(itemID);
if(!item) {
safe_delete(outapp);
continue;
}
// Save having to scan the client list when dealing with multiple buylines for the same Character.
if(charID != lastCharID) {
buyer = entity_list.GetClientByCharID(charID);
lastCharID = charID;
}
if(!buyer) {
safe_delete(outapp);
continue;
}
VARSTRUCT_ENCODE_TYPE(uint32, buf, Barter_BuyerSearchResults); // Command
VARSTRUCT_ENCODE_TYPE(uint32, buf, searchID); // Match up results with the request
VARSTRUCT_ENCODE_TYPE(uint32, buf, buySlot); // Slot in this Buyer's list
VARSTRUCT_ENCODE_TYPE(uint8, buf, 0x01); // Unknown - probably a flag field
VARSTRUCT_ENCODE_TYPE(uint32, buf, itemID); // ItemID
VARSTRUCT_ENCODE_STRING(buf, itemName); // Itemname
VARSTRUCT_ENCODE_TYPE(uint32, buf, item->Icon); // Icon
VARSTRUCT_ENCODE_TYPE(uint32, buf, quantity); // Quantity
VARSTRUCT_ENCODE_TYPE(uint8, buf, 0x01); // Unknown - probably a flag field
VARSTRUCT_ENCODE_TYPE(uint32, buf, price); // Price
VARSTRUCT_ENCODE_TYPE(uint32, buf, buyer->GetID()); // Entity ID
VARSTRUCT_ENCODE_TYPE(uint32, buf, 0); // Flag for + Items , probably ItemCount
VARSTRUCT_ENCODE_STRING(buf, buyer->GetName()); // Seller Name
QueuePacket(outapp);
safe_delete(outapp);
}
}
void Client::ShowBuyLines(const EQApplicationPacket *app) {
BuyerInspectRequest_Struct* bir = ( BuyerInspectRequest_Struct*)app->pBuffer;
Client *Buyer = entity_list.GetClientByID(bir->BuyerID);
if(!Buyer) {
bir->Approval = 0; // Tell the client that the Buyer is unavailable
QueuePacket(app);
Message(Chat::Red, "The Buyer has gone away.");
return;
}
bir->Approval = Buyer->WithCustomer(GetID());
QueuePacket(app);
if(bir->Approval == 0) {
MessageString(Chat::Yellow, TRADER_BUSY);
return;
}
const char *WelcomeMessagePointer = Buyer->GetBuyerWelcomeMessage();
if(strlen(WelcomeMessagePointer) > 0)
Message(Chat::NPCQuestSay, "%s greets you, '%s'.", Buyer->GetName(), WelcomeMessagePointer);
auto outapp = new EQApplicationPacket(OP_Barter, sizeof(BuyerBrowsing_Struct));
BuyerBrowsing_Struct* bb = (BuyerBrowsing_Struct*)outapp->pBuffer;
// This packet produces the SoandSo is browsing your Buy Lines message
bb->Action = Barter_SellerBrowsing;
sprintf(bb->PlayerName, "%s", GetName());
Buyer->QueuePacket(outapp);
safe_delete(outapp);
std::string query = StringFormat("SELECT * FROM buyer WHERE charid = %i", Buyer->CharacterID());
auto results = database.QueryDatabase(query);
if (!results.Success() || results.RowCount() == 0)
return;
for (auto &row = results.begin(); row != results.end(); ++row) {
char ItemName[64];
uint32 BuySlot = Strings::ToInt(row[1]);
uint32 ItemID = Strings::ToInt(row[2]);
strcpy(ItemName, row[3]);
uint32 Quantity = Strings::ToInt(row[4]);
uint32 Price = Strings::ToInt(row[5]);
auto outapp = new EQApplicationPacket(OP_Barter, 936);
char *Buf = (char *)outapp->pBuffer;
const EQ::ItemData* item = database.GetItem(ItemID);
if(!item) {
safe_delete(outapp);
continue;
}
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerInspectWindow);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Flag
VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID);
VARSTRUCT_ENCODE_STRING(Buf, ItemName);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Flag
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID());
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0);
VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName());
QueuePacket(outapp);
safe_delete(outapp);
}
}
void Client::SellToBuyer(const EQApplicationPacket *app) {
char* Buf = (char *)app->pBuffer;
char ItemName[64];
/*uint32 Action =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
uint32 Quantity = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint32 BuyerID = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint32 BuySlot = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint32 UnknownByte = VARSTRUCT_DECODE_TYPE(uint8, Buf);
uint32 ItemID = VARSTRUCT_DECODE_TYPE(uint32, Buf);
/* ItemName */ VARSTRUCT_DECODE_STRING(ItemName, Buf);
/*uint32 Unknown2 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
uint32 QtyBuyerWants = VARSTRUCT_DECODE_TYPE(uint32, Buf);
UnknownByte = VARSTRUCT_DECODE_TYPE(uint8, Buf);
uint32 Price = VARSTRUCT_DECODE_TYPE(uint32, Buf);
/*uint32 BuyerID2 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
/*uint32 Unknown3 =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
const EQ::ItemData *item = database.GetItem(ItemID);
if(!item || !Quantity || !Price || !QtyBuyerWants) return;
if (m_inv.HasItem(ItemID, Quantity, invWhereWorn | invWherePersonal | invWhereCursor) == INVALID_INDEX) {
Message(Chat::Red, "You do not have %i %s on you.", Quantity, item->Name);
return;
}
Client *Buyer = entity_list.GetClientByID(BuyerID);
if(!Buyer || !Buyer->IsBuyer()) {
Message(Chat::Red, "The Buyer has gone away.");
return;
}
// For Stackable items, HasSpaceForItem will try check if there is space to stack with existing stacks in
// the buyer inventory.
if(!(Buyer->GetInv().HasSpaceForItem(item, Quantity))) {
Message(Chat::Red, "The Buyer does not have space for %i %s", Quantity, item->Name);
return;
}
if((static_cast<uint64>(Quantity) * static_cast<uint64>(Price)) > MAX_TRANSACTION_VALUE) {
Message(Chat::Red, "That would exceed the single transaction limit of %u platinum.", MAX_TRANSACTION_VALUE / 1000);
return;
}
if(!Buyer->HasMoney(Quantity * Price)) {
Message(Chat::Red, "The Buyer does not have sufficient money to purchase that quantity of %s.", item->Name);
Buyer->Message(Chat::Red, "%s tried to sell you %i %s, but you have insufficient funds.", GetName(), Quantity, item->Name);
return;
}
if(Buyer->CheckLoreConflict(item)) {
Message(Chat::Red, "That item is LORE and the Buyer already has one.");
Buyer->Message(Chat::Red, "%s tried to sell you %s but this item is LORE and you already have one.",
GetName(), item->Name);
return;
}
if(item->NoDrop == 0) {
Message(Chat::Red, "That item is NODROP.");
return;
}
if(item->IsClassBag()) {
Message(Chat::Red, "That item is a Bag.");
return;
}
if(!item->Stackable) {
for(uint32 i = 0; i < Quantity; i++) {
int16 SellerSlot = m_inv.HasItem(ItemID, 1, invWhereWorn|invWherePersonal|invWhereCursor);
// This shouldn't happen, as we already checked there was space in the Buyer's inventory
if (SellerSlot == INVALID_INDEX) {
if(i > 0) {
// Set the Quantity to the actual number we successfully transferred.
Quantity = i;
break;
}
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
return;
}
EQ::ItemInstance* ItemToTransfer = m_inv.PopItem(SellerSlot);
if(!ItemToTransfer || !Buyer->MoveItemToInventory(ItemToTransfer, true)) {
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
if(ItemToTransfer)
safe_delete(ItemToTransfer);
return;
}
database.SaveInventory(CharacterID(), 0, SellerSlot);
safe_delete(ItemToTransfer);
// Remove the item from inventory, clientside
//
auto outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct));
MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer;
mis->from_slot = SellerSlot;
mis->to_slot = 0xFFFFFFFF;
mis->number_in_stack = 0xFFFFFFFF;
QueuePacket(outapp2);
safe_delete(outapp2);
}
}
else {
// Stackable
//
uint32 QuantityMoved = 0;
while(QuantityMoved < Quantity) {
// Find the slot on the seller that has a stack of at least 1 of the item
int16 SellerSlot = m_inv.HasItem(ItemID, 1, invWhereWorn|invWherePersonal|invWhereCursor);
if (SellerSlot == INVALID_INDEX) {
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
return;
}
EQ::ItemInstance* ItemToTransfer = m_inv.PopItem(SellerSlot);
if(!ItemToTransfer) {
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
return;
}
// If the stack we found has less than the quantity we are selling ...
if(ItemToTransfer->GetCharges() <= (Quantity - QuantityMoved)) {
// Transfer the entire stack
QuantityMoved += ItemToTransfer->GetCharges();
if(!Buyer->MoveItemToInventory(ItemToTransfer, true)) {
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
safe_delete(ItemToTransfer);
return;
}
// Delete the entire stack from the seller's inventory
database.SaveInventory(CharacterID(), 0, SellerSlot);
safe_delete(ItemToTransfer);
// and tell the client to do the same.
auto outapp2 = new EQApplicationPacket(OP_MoveItem, sizeof(MoveItem_Struct));
MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer;
mis->from_slot = SellerSlot;
mis->to_slot = 0xFFFFFFFF;
mis->number_in_stack = 0xFFFFFFFF;
QueuePacket(outapp2);
safe_delete(outapp2);
}
else {
//Move the amount we need, and put the rest of the stack back in the seller's inventory
//
int QuantityToRemoveFromStack = Quantity - QuantityMoved;
ItemToTransfer->SetCharges(ItemToTransfer->GetCharges() - QuantityToRemoveFromStack);
m_inv.PutItem(SellerSlot, *ItemToTransfer);
database.SaveInventory(CharacterID(), ItemToTransfer, SellerSlot);
ItemToTransfer->SetCharges(QuantityToRemoveFromStack);
if(!Buyer->MoveItemToInventory(ItemToTransfer, true)) {
LogError("Unexpected error while moving item from seller to buyer");
Message(Chat::Red, "Internal error while processing transaction.");
safe_delete(ItemToTransfer);
return;
}
safe_delete(ItemToTransfer);
auto outapp2 = new EQApplicationPacket(OP_DeleteItem, sizeof(MoveItem_Struct));
MoveItem_Struct* mis = (MoveItem_Struct*)outapp2->pBuffer;
mis->from_slot = SellerSlot;
mis->to_slot = 0xFFFFFFFF;
mis->number_in_stack = 0xFFFFFFFF;
for(int i = 0; i < QuantityToRemoveFromStack; i++)
QueuePacket(outapp2);
safe_delete(outapp2);
QuantityMoved = Quantity;
}
}
}
Buyer->TakeMoneyFromPP(Quantity * Price);
AddMoneyToPP(Quantity * Price);
if(RuleB(Bazaar, AuditTrail))
BazaarAuditTrail(GetName(), Buyer->GetName(), ItemName, Quantity, Quantity * Price, 1);
// We now send a packet to the Seller, which causes it to display 'You have sold <Qty> <Item> to <Player> for <money>'
//
// The PacketLength of 1016 is from the only instance of this packet I have seen, which is from Live, November 2008
// The Titanium/6.2 struct is slightly different in that it appears to use fixed length strings instead of variable
// length as used on Live. The extra space in the packet is also likely to be used for Item compensation, if we ever
// implement that.
//
uint32 PacketLength = 1016;
auto outapp = new EQApplicationPacket(OP_Barter, PacketLength);
Buf = (char *)outapp->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_SellerTransactionComplete);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity * Price);
if (ClientVersion() >= EQ::versions::ClientVersion::SoD)
{
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); // Think this is the upper 32 bits of a 64 bit price
}
sprintf(Buf, "%s", Buyer->GetName()); Buf += 64;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0x01);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00);
sprintf(Buf, "%s", ItemName); Buf += 64;
QueuePacket(outapp);
// This next packet goes to the Buyer and produces the 'You've bought <Qty> <Item> from <Seller> for <money>'
//
Buf = (char *)outapp->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerTransactionComplete);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity * Price);
if (Buyer->ClientVersion() >= EQ::versions::ClientVersion::SoD)
{
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0); // Think this is the upper 32 bits of a 64 bit price
}
sprintf(Buf, "%s", GetName()); Buf += 64;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0x01);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00);
sprintf(Buf, "%s", ItemName); Buf += 64;
Buyer->QueuePacket(outapp);
safe_delete(outapp);
// Next we update the buyer table in the database to reflect the reduced quantity the Buyer wants to buy.
//
database.UpdateBuyLine(Buyer->CharacterID(), BuySlot, QtyBuyerWants - Quantity);
// Next we update the Seller's Barter Window to reflect the reduced quantity the Buyer is now looking to buy.
//
auto outapp3 = new EQApplicationPacket(OP_Barter, 936);
Buf = (char *)outapp3->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerInspectWindow);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // Unknown
VARSTRUCT_ENCODE_TYPE(uint32, Buf,ItemID);
VARSTRUCT_ENCODE_STRING(Buf, ItemName);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, QtyBuyerWants - Quantity);
// If the amount we have just sold completely satisfies the quantity the Buyer was looking for,
// setting the next byte to 0 will remove the item from the Barter Window.
//
if(QtyBuyerWants - Quantity > 0) {
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // 0 = Toggle Off, 1 = Toggle On
}
else {
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // 0 = Toggle Off, 1 = Toggle On
}
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID());
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0);
VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName());
QueuePacket(outapp3);
safe_delete(outapp3);
// The next packet updates the /buyer window with the reduced quantity, and toggles the buy line off if the
// quantity they wanted to buy has been met.
//
auto outapp4 = new EQApplicationPacket(OP_Barter, 936);
Buf = (char*)outapp4->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerItemUpdate);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID);
VARSTRUCT_ENCODE_STRING(Buf, ItemName);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, item->Icon);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, QtyBuyerWants - Quantity);
if((QtyBuyerWants - Quantity) > 0) {
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 1); // 0 = Toggle Off, 1 = Toggle On
}
else {
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // 0 = Toggle Off, 1 = Toggle On
}
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x08f4); // Unknown
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0);
VARSTRUCT_ENCODE_STRING(Buf, Buyer->GetName());
Buyer->QueuePacket(outapp4);
safe_delete(outapp4);
return;
}
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 TurnOn) {
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, GetID());
if(TurnOn) {
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x01);
}
else {
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x00);
database.DeleteBuyLines(CharacterID());
SetCustomerID(0);
}
VARSTRUCT_ENCODE_STRING(Buf, GetName());
entity_list.QueueClients(this, outapp, false);
safe_delete(outapp);
Buyer = TurnOn;
}
void Client::UpdateBuyLine(const EQApplicationPacket *app) {
// This method is called when:
//
// /buyer mode is first turned on, once for each item
// A BuyLine is toggled on or off in the/buyer window.
//
char* Buf = (char*)app->pBuffer;
char ItemName[64];
/*uint32 Action =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
uint32 BuySlot = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint8 Unknown009 = VARSTRUCT_DECODE_TYPE(uint8, Buf);
uint32 ItemID = VARSTRUCT_DECODE_TYPE(uint32, Buf);
/* ItemName */ VARSTRUCT_DECODE_STRING(ItemName, Buf);
uint32 Icon = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint32 Quantity = VARSTRUCT_DECODE_TYPE(uint32, Buf);
uint8 ToggleOnOff = VARSTRUCT_DECODE_TYPE(uint8, Buf);
uint32 Price = VARSTRUCT_DECODE_TYPE(uint32, Buf);
/*uint32 UnknownZ =*/ VARSTRUCT_SKIP_TYPE(uint32, Buf); //unused
uint32 ItemCount = VARSTRUCT_DECODE_TYPE(uint32, Buf);
const EQ::ItemData *item = database.GetItem(ItemID);
if(!item) return;
bool LoreConflict = CheckLoreConflict(item);
LogTrading("UpdateBuyLine: Char: [{}] BuySlot: [{}] ItemID [{}] [{}] Quantity [{}] Toggle: [{}] Price [{}] ItemCount [{}] LoreConflict [{}]",
GetName(), BuySlot, ItemID, item->Name, Quantity, ToggleOnOff, Price, ItemCount, LoreConflict);
if((item->NoDrop != 0) && (!item->IsClassBag())&& !LoreConflict && (Quantity > 0) && HasMoney(Quantity * Price) && ToggleOnOff && (ItemCount == 0)) {
LogTrading("Adding to database");
database.AddBuyLine(CharacterID(), BuySlot, ItemID, ItemName, Quantity, Price);
QueuePacket(app);
}
else {
if(ItemCount > 0) {
Message(Chat::Red, "Buy line %s disabled as Item Compensation is not currently supported.", ItemName);
} else if(Quantity <= 0) {
Message(Chat::Red, "Buy line %s disabled as the quantity is invalid.", ItemName);
} else if(LoreConflict) {
Message(Chat::Red, "Buy line %s disabled as the item is LORE and you have one already.", ItemName);
} else if(item->NoDrop == 0) {
Message(Chat::Red, "Buy line %s disabled as the item is NODROP.", ItemName);
} else if(item->IsClassBag()) {
Message(Chat::Red, "Buy line %s disabled as the item is a Bag.", ItemName);
} else if(ToggleOnOff) {
Message(Chat::Red, "Buy line %s disabled due to insufficient funds.", ItemName);
} else {
database.RemoveBuyLine(CharacterID(), BuySlot);
}
auto outapp = new EQApplicationPacket(OP_Barter, 936);
Buf = (char*)outapp->pBuffer;
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerItemUpdate);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, BuySlot);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, Unknown009);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, ItemID);
VARSTRUCT_ENCODE_STRING(Buf, ItemName);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Icon);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Quantity);
VARSTRUCT_ENCODE_TYPE(uint8, Buf, 0); // Toggle the Buy Line off in the client
VARSTRUCT_ENCODE_TYPE(uint32, Buf, Price);
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x08f4); // Unknown
VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0);
VARSTRUCT_ENCODE_STRING(Buf, GetName());
QueuePacket(outapp);
safe_delete(outapp);
}
}
void Client::BuyerItemSearch(const EQApplicationPacket *app) {
BuyerItemSearch_Struct* bis = (BuyerItemSearch_Struct*)app->pBuffer;
auto outapp = new EQApplicationPacket(OP_Barter, sizeof(BuyerItemSearchResults_Struct));
BuyerItemSearchResults_Struct* bisr = (BuyerItemSearchResults_Struct*)outapp->pBuffer;
const EQ::ItemData* item = 0;
int Count=0;
char Name[64];
char Criteria[255];
strn0cpy(Criteria, bis->SearchString, sizeof(Criteria));
strupr(Criteria);
char* pdest;
uint32 it = 0;
while ((item = database.IterateItems(&it))) {
strn0cpy(Name, item->Name, sizeof(Name));
strupr(Name);
pdest = strstr(Name, Criteria);
if (pdest != nullptr) {
sprintf(bisr->Results[Count].ItemName, "%s", item->Name);
bisr->Results[Count].ItemID = item->ID;
bisr->Results[Count].Unknown068 = item->Icon;
bisr->Results[Count].Unknown072 = 0x00000000;
Count++;
}
if (Count == MAX_BUYER_ITEMSEARCH_RESULTS)
break;
}
if (Count == MAX_BUYER_ITEMSEARCH_RESULTS)
Message(Chat::Yellow, "Your search returned more than %i results. Only the first %i are displayed.",
MAX_BUYER_ITEMSEARCH_RESULTS, MAX_BUYER_ITEMSEARCH_RESULTS);
bisr->Action = Barter_BuyerSearch;
bisr->ResultCount = Count;
QueuePacket(outapp);
safe_delete(outapp);
}
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();
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();
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::TraderPriceUpdate(const EQApplicationPacket *app)
{
// Handle price updates from the Trader and update a customer browsing our stuff if necessary
// This method also handles removing items from sale and adding them back up whilst still in
// Trader mode.
//
auto tpus = (TraderPriceUpdate_Struct *) app->pBuffer;
LogTrading(
"Received Price Update for <green>[{}] Item Serial No. <green>[{}] New Price <green>[{}]",
GetName(),
tpus->SerialNumber,
tpus->NewPrice
);
// Pull the items this Trader currently has for sale from the trader table.
//
auto trader_items = TraderRepository::GetWhere(database, fmt::format("`char_id` = '{}'", CharacterID()));
uint32 item_limit = trader_items.size() >= GetInv().GetLookup()->InventoryTypeSize.Bazaar
? GetInv().GetLookup()->InventoryTypeSize.Bazaar
: trader_items.size();
// The client only sends a single update with the Serial Number of the item whose price has been updated.
// We must update the price for all the Trader's items that are identical to that one item, i.e.
// if it is a stackable item like arrows, update the price for all stacks. If it is not stackable, then
// update the prices for all items that have the same number of charges.
//
uint32 id_of_item_to_update = 0;
int32 charges_on_item_to_update = 0;
uint32 old_price = 0;
for (int i = 0; i < item_limit; i++) {
if ((trader_items.at(i).item_id > 0) && (trader_items.at(i).item_sn == tpus->SerialNumber)) {
// We found the item that the Trader wants to change the price of (or add back up for sale).
//
id_of_item_to_update = trader_items.at(i).item_id;
charges_on_item_to_update = trader_items.at(i).item_charges;
old_price = trader_items.at(i).item_cost;
LogTrading(
"ItemID is <green>[{}] Charges is <green>[{}]",
trader_items.at(i).item_id,
trader_items.at(i).item_charges
);
break;
}
}
if (id_of_item_to_update == 0) {
// If the item is not currently in the trader table for this Trader, then they must have removed it from sale while
// still in Trader mode. Check if the item is in their Trader Satchels, and if so, put it back up.
// Quick Sanity check. If the item is not currently up for sale, and the new price is zero, just ack the packet
// and do nothing.
if (tpus->NewPrice == 0) {
tpus->SubAction = BazaarPriceChange_RemoveItem;
QueuePacket(app);
return;
}
LogTrading("Unable to find item to update price for. Rechecking trader satchels");
// Find what is in their Trader Satchels
auto newgis = GetTraderItems();
uint32 id_of_item_to_add = 0;
int32 charges_on_item_to_add = 0;
for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) {
if ((newgis->items[i] > 0) && (newgis->serial_number[i] == tpus->SerialNumber)) {
id_of_item_to_add = newgis->items[i];
charges_on_item_to_add = newgis->charges[i];
LogTrading(
"Found new Item to Add, ItemID is <green>[{}] Charges is <green>[{}]",
newgis->items[i],
newgis->charges[i]
);
break;
}
}
const EQ::ItemData *item = nullptr;
if (id_of_item_to_add) {
item = database.GetItem(id_of_item_to_add);
}
if (!id_of_item_to_add || !item) {
tpus->SubAction = BazaarPriceChange_Fail;
QueuePacket(app);
TraderEndTrader();
safe_delete(newgis);
LogTrading("Item not found in Trader Satchels either");
return;
}
// It is a limitation of the client that if you have multiple of the same item, but with different charges,
// although you can set different prices for them before entering Trader mode. If you Remove them and then
// add them back whilst still in Trader mode, they all go up for the same price. We check for this situation
// and give the Trader a warning message.
//
if (!item->Stackable) {
bool same_item_with_differing_charges = false;
for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) {
if ((newgis->items[i] == id_of_item_to_add) && (newgis->charges[i] != charges_on_item_to_add)) {
same_item_with_differing_charges = true;
break;
}
}
if (same_item_with_differing_charges) {
Message(
Chat::Red,
"Warning: You have more than one %s with different charges. They have all been added for sale "
"at the same price.",
item->Name
);
}
}
// Now put all Items with a matching ItemID up for trade.
//
for (int i = 0; i < GetInv().GetLookup()->InventoryTypeSize.Bazaar; i++) {
if (newgis->items[i] == id_of_item_to_add) {
auto item_detail = FindTraderItemBySerialNumber(newgis->serial_number[i]);
TraderRepository::Trader trader_item{};
trader_item.id = 0;
trader_item.char_entity_id = GetID();
trader_item.char_id = CharacterID();
trader_item.char_zone_id = GetZoneID();
trader_item.item_charges = newgis->charges[i];
trader_item.item_cost = tpus->NewPrice;
trader_item.item_id = newgis->items[i];
trader_item.item_sn = newgis->serial_number[i];
if (item_detail->IsAugmented()) {
auto augs = item_detail->GetAugmentIDs();
trader_item.aug_slot_1 = augs.at(0);
trader_item.aug_slot_2 = augs.at(1);
trader_item.aug_slot_3 = augs.at(2);
trader_item.aug_slot_4 = augs.at(3);
trader_item.aug_slot_5 = augs.at(4);
trader_item.aug_slot_6 = augs.at(5);
}
trader_item.slot_id = i;
TraderRepository::ReplaceOne(database, trader_item);
trader_items.push_back(trader_item);
LogTrading(
"Adding new item for <green>[{}] ItemID <green>[{}] SerialNumber <green>[{}] Charges <green>[{}] "
"Price: <green>[{}] Slot <green>[{}]",
GetName(),
newgis->items[i],
newgis->serial_number[i],
newgis->charges[i],
tpus->NewPrice,
i
);
}
}
// If we have a customer currently browsing, update them with the new items.
//
if (GetCustomerID()) {
UpdateTraderCustomerItemsAdded(
GetCustomerID(),
trader_items,
id_of_item_to_add,
GetInv().GetLookup()->InventoryTypeSize.Bazaar
);
}
safe_delete(newgis);
// Acknowledge to the client.
tpus->SubAction = BazaarPriceChange_AddItem;
QueuePacket(app);
return;
}
// This is a safeguard against a Trader increasing the price of an item while a customer is browsing and
// unwittingly buying it at a higher price than they were expecting to.
//
if ((old_price != 0) && (tpus->NewPrice > old_price) && GetCustomerID()) {
tpus->SubAction = BazaarPriceChange_Fail;
QueuePacket(app);
TraderEndTrader();
Message(
Chat::Red,
"You must remove the item from sale before you can increase the price while a customer is browsing."
);
Message(Chat::Red, "Click 'Begin Trader' to restart Trader mode with the increased price for this item.");
return;
}
// Send Acknowledgement back to the client.
if (old_price == 0) {
tpus->SubAction = BazaarPriceChange_AddItem;
}
else if (tpus->NewPrice != 0) {
tpus->SubAction = BazaarPriceChange_UpdatePrice;
}
else {
tpus->SubAction = BazaarPriceChange_RemoveItem;
}
QueuePacket(app);
if (old_price == tpus->NewPrice) {
LogTrading("The new price is the same as the old one");
return;
}
// Update the price for all items we have for sale that have this ItemID and number of charges, or remove
// them from the trader table if the new price is zero.
//
database.UpdateTraderItemPrice(CharacterID(), id_of_item_to_update, charges_on_item_to_update, tpus->NewPrice);
// If a customer is browsing our goods, send them the updated prices / remove the items from the Merchant window
if (GetCustomerID()) {
UpdateTraderCustomerPriceChanged(
GetCustomerID(),
trader_items,
id_of_item_to_update,
charges_on_item_to_update,
tpus->NewPrice,
item_limit
);
}
}
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;
}
auto results = TraderRepository::GetDistinctTraders(database);
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(uint32, bufptr, t.zone_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(const BazaarInspect_Struct &in)
{
auto items = TraderRepository::GetWhere(database, fmt::format("item_sn = {}", in.serial_number));
if (items.empty()) {
LogInfo("Failed to find item with serial number [{}]", in.serial_number);
return;
}
auto &item = items.front();
std::unique_ptr<EQ::ItemInstance> inst(
database.CreateItem(
item.item_id,
item.item_charges,
item.aug_slot_1,
item.aug_slot_2,
item.aug_slot_3,
item.aug_slot_4,
item.aug_slot_5,
item.aug_slot_6
)
);
if (inst) {
SendItemPacket(0, inst.get(), ItemPacketViewLink);
}
}
void Client::SendBazaarDeliveryCosts()
{
auto outapp = std::make_unique<EQApplicationPacket>(OP_BazaarSearch, 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::BuyTraderItemOutsideBazaar(TraderBuy_Struct *tbs, const EQApplicationPacket *app)
{
auto in = (TraderBuy_Struct *) app->pBuffer;
auto trader_item = TraderRepository::GetItemBySerialNumber(database, tbs->serial_number);
if (!trader_item.id) {
LogTrading("Attempt to purchase an item outside of the Bazaar trader_id <red>[{}] item serial_number "
"<red>[{}] The Traders data was outdated.",
tbs->trader_id,
tbs->serial_number
);
in->method = ByParcel;
in->sub_action = DataOutDated;
TradeRequestFailed(app);
return;
}
if (trader_item.active_transaction) {
LogTrading("Attempt to purchase an item outside of the Bazaar trader_id <red>[{}] item serial_number "
"<red>[{}] The item is already within an active transaction.",
tbs->trader_id,
tbs->serial_number
);
in->method = ByParcel;
in->sub_action = DataOutDated;
TradeRequestFailed(app);
return;
}
TraderRepository::UpdateActiveTransaction(database, trader_item.id, true);
std::unique_ptr<EQ::ItemInstance> buy_item(
database.CreateItem(
trader_item.item_id,
trader_item.item_charges,
trader_item.aug_slot_1,
trader_item.aug_slot_2,
trader_item.aug_slot_3,
trader_item.aug_slot_4,
trader_item.aug_slot_5,
trader_item.aug_slot_6
)
);
if (!buy_item) {
LogTrading("Unable to find item id <red>[{}] item_sn <red>[{}] on trader",
trader_item.item_id,
trader_item.item_sn
);
in->method = ByParcel;
in->sub_action = Failed;
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
LogTrading(
"Name: <green>[{}] IsStackable: <green>[{}] Requested Quantity: <green>[{}] Charges on Item <green>[{}]",
buy_item->GetItem()->Name,
buy_item->IsStackable(),
tbs->quantity,
buy_item->GetCharges()
);
// Determine the actual quantity for the purchase
if (!buy_item->IsStackable()) {
tbs->quantity = 1;
}
else {
int32 item_charges = buy_item->GetCharges();
if (item_charges <= 0) {
tbs->quantity = 1;
}
else if (static_cast<uint32>(item_charges) < tbs->quantity) {
tbs->quantity = item_charges;
}
}
LogTrading("Actual quantity that will be traded is <green>[{}]", tbs->quantity);
uint64 total_transaction_value = static_cast<uint64>(tbs->price) * static_cast<uint64>(tbs->quantity);
if (total_transaction_value > MAX_TRANSACTION_VALUE) {
Message(
Chat::Red,
"That would exceed the single transaction limit of %u platinum.",
MAX_TRANSACTION_VALUE / 1000
);
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
uint32 total_cost = tbs->price * tbs->quantity;
uint32 fee = static_cast<uint32>(std::round((uint32) total_cost * RuleR(Bazaar, ParcelDeliveryCostMod)));
if (!TakeMoneyFromPP(total_cost + fee)) {
RecordPlayerEventLog(
PlayerEvent::POSSIBLE_HACK,
PlayerEvent::PossibleHackEvent{
.message = fmt::format(
"{} attempted to buy {} in bazaar but did not have enough money.",
GetCleanName(),
buy_item->GetItem()->Name
)
}
);
in->method = ByParcel;
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());
LogTrading("Customer <green>[{}] Paid: <green>[{}] in Copper", CharacterID(), total_cost);
if (player_event_logs.IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) {
auto e = PlayerEvent::TraderPurchaseEvent{
.item_id = buy_item->GetID(),
.item_name = buy_item->GetItem()->Name,
.trader_id = tbs->trader_id,
.trader_name = tbs->seller_name,
.price = tbs->price,
.charges = tbs->quantity,
.total_cost = total_cost,
.player_money_balance = GetCarriedMoney(),
};
RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e);
}
CharacterParcelsRepository::CharacterParcels parcel_out{};
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(),
buy_item->GetItem()->Name
);
in->method = ByParcel;
in->sub_action = TooManyParcels;
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
parcel_out.from_name = tbs->seller_name;
parcel_out.note = "Delivered from a Bazaar Purchase";
parcel_out.sent_date = time(nullptr);
parcel_out.quantity = buy_item->IsStackable() ? tbs->quantity : buy_item->GetCharges();
parcel_out.item_id = buy_item->GetItem()->ID;
parcel_out.aug_slot_1 = buy_item->GetAugmentItemID(0);
parcel_out.aug_slot_2 = buy_item->GetAugmentItemID(1);
parcel_out.aug_slot_3 = buy_item->GetAugmentItemID(2);
parcel_out.aug_slot_4 = buy_item->GetAugmentItemID(3);
parcel_out.aug_slot_5 = buy_item->GetAugmentItemID(4);
parcel_out.aug_slot_6 = buy_item->GetAugmentItemID(5);
parcel_out.char_id = CharacterID();
parcel_out.slot_id = next_slot;
parcel_out.id = 0;
auto result = CharacterParcelsRepository::InsertOne(database, parcel_out);
if (!result.id) {
LogError("Failed to add parcel to database. From {} to {} item {} quantity {}",
parcel_out.from_name,
GetCleanName(),
parcel_out.item_id,
parcel_out.quantity
);
Message(Chat::Yellow, "Unable to save parcel to the database. Please contact an administrator.");
in->method = ByParcel;
in->sub_action = Failed;
TraderRepository::UpdateActiveTransaction(database, trader_item.id, false);
TradeRequestFailed(app);
return;
}
ReturnTraderReq(app, tbs->quantity, buy_item->GetID());
if (player_event_logs.IsEventEnabled(PlayerEvent::PARCEL_SEND)) {
PlayerEvent::ParcelSend e{};
e.from_player_name = parcel_out.from_name;
e.to_player_name = GetCleanName();
e.item_id = parcel_out.item_id;
e.aug_slot_1 = parcel_out.aug_slot_1;
e.aug_slot_2 = parcel_out.aug_slot_2;
e.aug_slot_3 = parcel_out.aug_slot_3;
e.aug_slot_4 = parcel_out.aug_slot_4;
e.aug_slot_5 = parcel_out.aug_slot_5;
e.aug_slot_6 = parcel_out.aug_slot_6;
e.quantity = parcel_out.quantity;
e.sent_date = parcel_out.sent_date;
RecordPlayerEventLog(PlayerEvent::PARCEL_SEND, e);
}
Parcel_Struct ps{};
ps.item_slot = parcel_out.slot_id;
strn0cpy(ps.send_to, GetCleanName(), sizeof(ps.send_to));
if (trader_item.item_charges == tbs->quantity) {
TraderRepository::DeleteOne(database, trader_item.id);
} else {
TraderRepository::UpdateQuantity(
database,
trader_item.char_id,
trader_item.item_sn,
trader_item.item_charges - tbs->quantity
);
}
SendParcelDeliveryToWorld(ps);
if (RuleB(Bazaar, AuditTrail)) {
BazaarAuditTrail(tbs->seller_name, GetName(), buy_item->GetItem()->Name, tbs->quantity, tbs->price, 0);
}
auto out_server = std::make_unique<ServerPacket>(ServerOP_BazaarPurchase, sizeof(BazaarPurchaseMessaging_Struct));
auto out_data = (BazaarPurchaseMessaging_Struct *) out_server->pBuffer;
out_data->trader_buy_struct = *tbs;
out_data->buyer_id = CharacterID();
out_data->item_aug_1 = buy_item->GetAugmentItemID(0);
out_data->item_aug_2 = buy_item->GetAugmentItemID(1);
out_data->item_aug_3 = buy_item->GetAugmentItemID(2);
out_data->item_aug_4 = buy_item->GetAugmentItemID(3);
out_data->item_aug_5 = buy_item->GetAugmentItemID(4);
out_data->item_aug_6 = buy_item->GetAugmentItemID(5);
out_data->item_quantity_available = trader_item.item_charges;
out_data->id = trader_item.id;
strn0cpy(out_data->trader_buy_struct.buyer_name, GetCleanName(), sizeof(out_data->trader_buy_struct.buyer_name));
worldserver.SendPacket(out_server.release());
}