/* EQEmu: EQEmulator Copyright (C) 2001-2026 EQEmu Development Team 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; either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 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, see . */ #include "client.h" #include "common/bazaar.h" #include "common/eq_packet_structs.h" #include "common/eqemu_logsys.h" #include "common/events/player_event_logs.h" #include "common/misc_functions.h" #include "common/repositories/buyer_buy_lines_repository.h" #include "common/repositories/buyer_repository.h" #include "common/repositories/trader_repository.h" #include "common/rulesys.h" #include "common/strings.h" #include "zone/entity.h" #include "zone/mob.h" #include "zone/quest_parser_collection.h" #include "zone/string_ids.h" #include "zone/worldserver.h" #include 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* event_details) { if (!tradingWith) { return; } if (tradingWith->IsClient()) { Client * other = tradingWith->CastToClient(); if(other) { LogTrading("Finishing trade with client [{}]", other->GetName()); AddMoneyToPP(other->trade->cp, other->trade->sp, other->trade->gp, other->trade->pp, true); // step 1: process bags for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) { const EQ::ItemInstance* inst = m_inv[trade_slot]; if (inst && inst->IsClassBag()) { LogTrading("Giving container [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName()); // TODO: need to check bag items/augments for no drop..everything for attuned... if ( inst->GetItem()->NoDrop != 0 || CanTradeFVNoDropItem() || other == this ) { int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst); if (free_slot != INVALID_INDEX) { if (other->PutItemInInventory(free_slot, *inst, true)) { inst->TransferOwnership(database, other->CharacterID()); LogTrading("Container [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID); } else { LogTrading("Transfer of container [{}] ([{}]) to [{}] failed, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID, other->GetName()); PushItemOnCursor(*inst, true); } } else { LogTrading("[{}]'s inventory is full, returning container [{}] ([{}]) to giver", other->GetName(), inst->GetItem()->Name, inst->GetItem()->ID); PushItemOnCursor(*inst, true); } } else { LogTrading("Container [{}] ([{}]) is NoDrop, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID); PushItemOnCursor(*inst, true); } DeleteItemInInventory(trade_slot); } } // step 2a: process stackables for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) { EQ::ItemInstance* inst = GetInv().GetItem(trade_slot); if (inst && inst->IsStackable()) { while (true) { // there's no built-in safety check against an infinite loop..but, it should break on one of the conditional checks int16 partial_slot = other->GetInv().FindFreeSlotForTradeItem(inst); if ((partial_slot == EQ::invslot::slotCursor) || (partial_slot == INVALID_INDEX)) break; EQ::ItemInstance* partial_inst = other->GetInv().GetItem(partial_slot); if (!partial_inst) break; if (partial_inst->GetID() != inst->GetID()) { LogTrading("[CLIENT] Client::ResetTrade() - an incompatible location reference was returned by Inventory::FindFreeSlotForTradeItem()"); break; } int16 old_charges = inst->GetCharges(); int16 partial_charges = partial_inst->GetCharges(); if ((partial_inst->GetCharges() + inst->GetCharges()) > partial_inst->GetItem()->StackSize) { int16 new_charges = (partial_inst->GetCharges() + inst->GetCharges()) - partial_inst->GetItem()->StackSize; partial_inst->SetCharges(partial_inst->GetItem()->StackSize); inst->SetCharges(new_charges); } else { partial_inst->SetCharges(partial_inst->GetCharges() + inst->GetCharges()); inst->SetCharges(0); } LogTrading("Transferring partial stack [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName()); if (other->PutItemInInventory(partial_slot, *partial_inst, true)) { LogTrading( "Partial stack [{}] ([{}]) successfully transferred, deleting [{}] charges from trade slot", inst->GetItem()->Name, inst->GetItem()->ID, (old_charges - inst->GetCharges()) ); inst->TransferOwnership(database, other->CharacterID()); } else { LogTrading("Transfer of partial stack [{}] ([{}]) to [{}] failed, returning [{}] charges to trade slot", inst->GetItem()->Name, inst->GetItem()->ID, other->GetName(), (old_charges - inst->GetCharges())); inst->SetCharges(old_charges); partial_inst->SetCharges(partial_charges); break; } if (inst->GetCharges() == 0) { DeleteItemInInventory(trade_slot); break; } } } } // step 2b: adjust trade stack bias // (if any partial stacks exist before the final stack, FindFreeSlotForTradeItem() will return that slot in step 3 and an overwrite will occur) for (int16 trade_slot = EQ::invslot::TRADE_END; trade_slot >= EQ::invslot::TRADE_BEGIN; --trade_slot) { EQ::ItemInstance* inst = GetInv().GetItem(trade_slot); if (inst && inst->IsStackable()) { for (int16 bias_slot = EQ::invslot::TRADE_BEGIN; bias_slot <= EQ::invslot::TRADE_END; ++bias_slot) { if (bias_slot >= trade_slot) break; EQ::ItemInstance* bias_inst = GetInv().GetItem(bias_slot); if (!bias_inst || (bias_inst->GetID() != inst->GetID()) || (bias_inst->GetCharges() >= bias_inst->GetItem()->StackSize)) continue; int16 old_charges = inst->GetCharges(); if ((bias_inst->GetCharges() + inst->GetCharges()) > bias_inst->GetItem()->StackSize) { int16 new_charges = (bias_inst->GetCharges() + inst->GetCharges()) - bias_inst->GetItem()->StackSize; bias_inst->SetCharges(bias_inst->GetItem()->StackSize); inst->SetCharges(new_charges); } else { bias_inst->SetCharges(bias_inst->GetCharges() + inst->GetCharges()); inst->SetCharges(0); } if (inst->GetCharges() == 0) { DeleteItemInInventory(trade_slot); break; } } } } // step 3: process everything else for (int16 trade_slot = EQ::invslot::TRADE_BEGIN; trade_slot <= EQ::invslot::TRADE_END; ++trade_slot) { const EQ::ItemInstance* inst = m_inv[trade_slot]; if (inst) { LogTrading("Giving item [{}] ([{}]) in slot [{}] to [{}]", inst->GetItem()->Name, inst->GetItem()->ID, trade_slot, other->GetName()); // TODO: need to check bag items/augments for no drop..everything for attuned... if (inst->GetItem()->NoDrop != 0 || CanTradeFVNoDropItem() || other == this) { int16 free_slot = other->GetInv().FindFreeSlotForTradeItem(inst); if (free_slot != INVALID_INDEX) { if (other->PutItemInInventory(free_slot, *inst, true)) { inst->TransferOwnership(database, other->CharacterID()); LogTrading("Item [{}] ([{}]) successfully transferred, deleting from trade slot", inst->GetItem()->Name, inst->GetItem()->ID); } else { LogTrading("Transfer of Item [{}] ([{}]) to [{}] failed, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID, other->GetName()); PushItemOnCursor(*inst, true); } } else { LogTrading("[{}]'s inventory is full, returning item [{}] ([{}]) to giver", other->GetName(), inst->GetItem()->Name, inst->GetItem()->ID); PushItemOnCursor(*inst, true); } } else { LogTrading("Item [{}] ([{}]) is NoDrop, returning to giver", inst->GetItem()->Name, inst->GetItem()->ID); PushItemOnCursor(*inst, true); } DeleteItemInInventory(trade_slot); } } //Do not reset the trade here, done by the caller. } } else if(tradingWith->IsNPC()) { bool quest_npc = false; if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE)) { quest_npc = true; } // take ownership of all trade slot items EQ::ItemInstance* insts[4] = { 0 }; for (int i = EQ::invslot::TRADE_BEGIN; i <= EQ::invslot::TRADE_NPC_END; ++i) { insts[i - EQ::invslot::TRADE_BEGIN] = m_inv.PopItem(i); database.SaveInventory(CharacterID(), nullptr, i); } // copy to be filtered by task updates, null trade slots preserved for quest event arg std::vector items(insts, insts + std::size(insts)); if (RuleB(TaskSystem, EnableTaskSystem)) { if (UpdateTasksOnDeliver(items, *trade, tradingWith->CastToNPC())) { if (!tradingWith->IsMoving()) { tradingWith->FaceTarget(this); } } } if (!quest_npc) { for (auto &inst: items) { if (!inst || !inst->GetItem()) { continue; } // remove delivered task items if (RuleB(TaskSystem, EnableTaskSystem) && inst->GetTaskDeliveredCount() > 0) { int remaining = inst->RemoveTaskDeliveredItems(); if (remaining <= 0) { inst = nullptr; continue; // all items in trade slot consumed by task update } } auto with = tradingWith->CastToNPC(); const EQ::ItemData *item = inst->GetItem(); const bool is_pet = with->IsPetOwnerOfClientBot() || with->IsCharmedPet(); if (is_pet && with->CanPetTakeItem(inst)) { // pets need to look inside bags and try to equip items found there if (item->IsClassBag() && item->BagSlots > 0) { // if an item inside the bag can't be given to the pet, keep the bag bool keep_bag = false; int item_count = 0; for (int16 bslot = EQ::invbag::SLOT_BEGIN; bslot < item->BagSlots; bslot++) { const EQ::ItemInstance *baginst = inst->GetItem(bslot); if (baginst && baginst->GetItem() && with->CanPetTakeItem(baginst)) { // add item to pet's inventory auto lde = LootdropEntriesRepository::NewNpcEntity(); lde.equip_item = 1; lde.item_charges = static_cast(baginst->GetCharges()); with->AddLootDrop(baginst->GetItem(), lde, true); inst->DeleteItem(bslot); item_count++; } else { keep_bag = true; } } // add item to pet's inventory if (!keep_bag || item_count == 0) { auto lde = LootdropEntriesRepository::NewNpcEntity(); lde.equip_item = 1; lde.item_charges = static_cast(inst->GetCharges()); with->AddLootDrop(item, lde, true); inst = nullptr; } } else { // add item to pet's inventory auto lde = LootdropEntriesRepository::NewNpcEntity(); lde.equip_item = 1; lde.item_charges = static_cast(inst->GetCharges()); with->AddLootDrop(item, lde, true); inst = nullptr; } } } } std::string currencies[] = {"copper", "silver", "gold", "platinum"}; int32 amounts[] = {trade->cp, trade->sp, trade->gp, trade->pp}; for (int i = 0; i < 4; ++i) { parse->AddVar( fmt::format("{}.{}", currencies[i], tradingWith->GetNPCTypeID()), fmt::format("{}", amounts[i]) ); } if (tradingWith->GetAppearance() != eaDead) { tradingWith->FaceTarget(this); } // we cast to any to pass through the quest event system std::vector item_list(items.begin(), items.end()); for (EQ::ItemInstance *inst: items) { if (!inst || !inst->GetItem()) { continue; } item_list.emplace_back(inst); } auto handin_npc = tradingWith->CastToNPC(); m_external_handin_money_returned = {}; m_external_handin_items_returned = {}; bool has_aggro = tradingWith->CheckAggro(this); if (parse->HasQuestSub(tradingWith->GetNPCTypeID(), EVENT_TRADE) && !has_aggro) { // This CheckHandin call enables eq.handin and quest::handin to recognize the hand-in context. // It initializes the first hand-in bucket, which is then reused for the EVENT_TRADE subroutine. std::map handin = { {"copper", trade->cp}, {"silver", trade->sp}, {"gold", trade->gp}, {"platinum", trade->pp} }; for (EQ::ItemInstance *inst: items) { if (!inst || !inst->GetItem()) { continue; } std::string item_id = fmt::format("{}", inst->GetItem()->ID); handin[item_id] += inst->GetCharges(); } handin_npc->CheckHandin(this, handin, {}, items); parse->EventNPC(EVENT_TRADE, tradingWith->CastToNPC(), this, "", 0, &item_list); LogNpcHandinDetail("EVENT_TRADE triggered for NPC [{}]", tradingWith->GetNPCTypeID()); } // this is a catch-all return for items that weren't consumed by the EVENT_TRADE subroutine // it's possible we have a quest NPC that doesn't have an EVENT_TRADE subroutine // we can't double fire the ReturnHandinItems() event, so we need to check if it's already been processed from EVENT_TRADE if (!handin_npc->HasProcessedHandinReturn()) { if (!handin_npc->HandinStarted()) { LogNpcHandinDetail("EVENT_TRADE did not process handin, calling ReturnHandinItems() for NPC [{}]", tradingWith->GetNPCTypeID()); std::map handin = { {"copper", trade->cp}, {"silver", trade->sp}, {"gold", trade->gp}, {"platinum", trade->pp} }; for (EQ::ItemInstance *inst: items) { if (!inst || !inst->GetItem()) { continue; } std::string item_id = fmt::format("{}", inst->GetItem()->ID); handin[item_id] += inst->GetCharges(); } handin_npc->CheckHandin(this, handin, {}, items); } if (RuleB(Items, AlwaysReturnHandins)) { handin_npc->ReturnHandinItems(this); LogNpcHandin("ReturnHandinItems called for NPC [{}]", handin_npc->GetNPCTypeID()); } } handin_npc->ResetHandin(); for (auto &inst: insts) { if (inst) { safe_delete(inst); } } } } bool Client::CheckTradeLoreConflict(Client* other) { if (!other) { return true; } bool has_lore_item = false; std::vector 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 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.char_zone_instance_id = GetInstanceID(); 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; trader_item.listing_date = time(nullptr); 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 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 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 [{}] Charges [{}] Quantity [{}]", 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 [{}] charges [{}] quantity [{}]", 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 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: [{}] with a quantity of: [{}] on Trader: [{}]\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(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 outapp = std::make_unique(OP_Trader, static_cast(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 [{}] item_sn [{}] on trader", tbs->item_id, tbs->serial_number); TradeRequestFailed(app); return; } LogTrading( "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", 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(item_charges) < tbs->quantity) { outtbs->quantity = item_charges; } else { outtbs->quantity = tbs->quantity; } } LogTrading("Actual quantity that will be traded is [{}]", 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 [{}] and [{}] aborted. Item: [{}] Charges: " "[{}] Qty [{}] Price: [{}]", GetName(), Trader->GetName(), buy_item->GetItem()->Name, buy_item->GetCharges(), tbs->quantity, tbs->price ); TradeRequestFailed(app); return; } uint64 total_transaction_value = static_cast(tbs->price) * static_cast(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: [{}] 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 (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { auto e = PlayerEvent::TraderPurchaseEvent{ .item_id = buy_item->GetID(), .augment_1_id = buy_item->GetAugmentItemID(0), .augment_2_id = buy_item->GetAugmentItemID(1), .augment_3_id = buy_item->GetAugmentItemID(2), .augment_4_id = buy_item->GetAugmentItemID(3), .augment_5_id = buy_item->GetAugmentItemID(4), .augment_6_id = buy_item->GetAugmentItemID(5), .item_name = buy_item->GetItem()->Name, .trader_id = Trader->CharacterID(), .trader_name = Trader->GetCleanName(), .price = tbs->price, .quantity = outtbs->quantity, .charges = buy_item->GetCharges(), .total_cost = (tbs->price * outtbs->quantity), .player_money_balance = GetCarriedMoney(), }; RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); } if (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_SELL)) { auto e = PlayerEvent::TraderSellEvent{ .item_id = buy_item->GetID(), .augment_1_id = buy_item->GetAugmentItemID(0), .augment_2_id = buy_item->GetAugmentItemID(1), .augment_3_id = buy_item->GetAugmentItemID(2), .augment_4_id = buy_item->GetAugmentItemID(3), .augment_5_id = buy_item->GetAugmentItemID(4), .augment_6_id = buy_item->GetAugmentItemID(5), .item_name = buy_item->GetItem()->Name, .buyer_id = CharacterID(), .buyer_name = GetCleanName(), .price = tbs->price, .quantity = outtbs->quantity, .charges = buy_item->GetCharges(), .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); EQApplicationPacket outapp(OP_BazaarSearch, static_cast(sizeof(BazaarWelcome_Struct))); auto data = (BazaarWelcome_Struct *) outapp.pBuffer; data->action = BazaarWelcome; data->traders = results.count_of_traders; data->items = results.count_of_items; QueuePacket(&outapp); } void Client::SendBarterWelcome() { const auto results = BuyerBuyLinesRepository::GetWelcomeData(database); MessageString(Chat::White, BUYER_WELCOME, std::to_string(results.count_of_buyers).c_str()); } void Client::DoBazaarSearch(BazaarSearchCriteria_Struct search_criteria) { std::vector results = Bazaar::GetSearchResults( database, content_db, search_criteria, GetZoneID(), GetInstanceID() ); if (results.empty()) { SendBazaarDone(GetID()); return; } SetTraderTransactionDate(); std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); ar(results); uint32 packet_size = ss.str().length() + sizeof(BazaarSearchMessaging_Struct); auto out = new EQApplicationPacket(OP_BazaarSearch, packet_size); auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; data->action = BazaarSearch; memcpy(data->payload, ss.str().data(), ss.str().length()); FastQueuePacket(&out); SendBazaarDone(GetID()); SendBazaarDeliveryCosts(); } static void UpdateTraderCustomerItemsAdded( uint32 customer_id, std::vector 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 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 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 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(BarterSearchRequest_Struct& bsr) { if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { std::string search_string(bsr.search_string); BuyerLineSearch_Struct results{}; SetBarterTime(); if (bsr.search_scope == 1) { // Local Buyers results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, 0, GetZoneID(), GetInstanceID()); } else if (bsr.buyer_id) { // Specific Buyer results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string, bsr.buyer_id); } else { // All Buyers results = BuyerBuyLinesRepository::SearchBuyLines(database, search_string); } if (results.buy_line.empty()) { Message(Chat::White, "No buylines could be found."); return; } std::string buyer_name = "ID {} not in zone."; if (search_string.empty()) { search_string = "*"; } results.search_string = std::move(search_string); results.transaction_id = bsr.transaction_id; std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); { ar(results); } auto packet = std::make_unique( OP_BuyerItems, static_cast(ss.str().length()) + static_cast(sizeof(BuyerGeneric_Struct)) ); auto emu = (BuyerGeneric_Struct *) packet->pBuffer; emu->action = Barter_BuyerSearch; memcpy(emu->payload, ss.str().data(), ss.str().length()); QueuePacket(packet.get()); ss.str(""); ss.clear(); } } void Client::ShowBuyLines(const EQApplicationPacket *app) { auto bir = (BuyerInspectRequest_Struct *) app->pBuffer; auto buyer = entity_list.GetClientByID(bir->buyer_id); if (!buyer || buyer->GetCustomerID()) { bir->approval = 0; // Tell the client that the Buyer is unavailable QueuePacket(app); MessageString(Chat::Yellow, TRADER_BUSY); return; } if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { SetBarterTime(); bir->approval = buyer->WithCustomer(GetID()); QueuePacket(app); auto results = BuyerBuyLinesRepository::GetBuyLines(database, buyer->CharacterID()); auto greeting = BuyerRepository::GetWelcomeMessage(database, buyer->GetBuyerID()); if (greeting.length() == 0) { greeting = "Welcome!"; } MessageString(Chat::NPCQuestSay, BUYER_GREETING, buyer->GetName(), greeting.c_str()); const std::string name(GetName()); buyer->SendSellerBrowsing(name); std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); for (auto l : results) { const EQ::ItemData *item = database.GetItem(l.item_id); l.enabled = 1; l.item_icon = item->Icon; l.item_toggle = 1; { ar(l); } auto packet = std::make_unique( OP_BuyerItems, static_cast(ss.str().length()) + static_cast(sizeof(BuyerGeneric_Struct)) ); auto emu = (BuyerGeneric_Struct *) packet->pBuffer; emu->action = Barter_BuyerInspectBegin; memcpy(emu->payload, ss.str().data(), ss.str().length()); QueuePacket(packet.get()); ss.str(""); ss.clear(); } return; } } void Client::SellToBuyer(const EQApplicationPacket *app) { if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { BuyerLineSellItem_Struct sell_line{}; auto in = (BuyerGeneric_Struct *) app->pBuffer; EQ::Util::MemoryStreamReader ss_in( reinterpret_cast(in->payload), app->size - sizeof(BuyerGeneric_Struct)); cereal::BinaryInputArchive ar(ss_in); ar(sell_line); sell_line.seller_name = GetCleanName(); switch (sell_line.purchase_method) { case BarterInBazaar: case BarterByVendor: { auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id); if (!buyer) { SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); break; } if (sell_line.purchase_method == BarterInBazaar && buyer->IsThereACustomer()) { auto customer = entity_list.GetClientByID(buyer->GetCustomerID()); if (customer) { customer->CancelBuyerTradeWindow(); } } if (!DoBarterBuyerChecks(sell_line)) { return; }; if (!DoBarterSellerChecks(sell_line)) { return; }; BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr)); if (!FindNumberOfFreeInventorySlotsWithSizeCheck(sell_line.trade_items)) { LogTradingDetail("Seller {} has insufficient inventory space for {} compensation items.", GetCleanName(), sell_line.trade_items.size() ); Message(Chat::Red, "Insufficient inventory space for the compensation items."); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); return; } for (auto const &ti: sell_line.trade_items) { std::unique_ptr inst( database.CreateItem( ti.item_id, ti.item_quantity * sell_line.seller_quantity ) ); if (inst.get()->GetItem()) { buyer->RemoveItem(ti.item_id, ti.item_quantity * sell_line.seller_quantity); if (!PutItemInInventoryWithStacking(inst.get())) { Message(Chat::Red, "Error putting item in your inventory."); buyer->PutItemInInventoryWithStacking(inst.get()); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); return; } } } std::unique_ptr buy_inst( database.CreateItem( sell_line.item_id, sell_line.seller_quantity ) ); RemoveItem(sell_line.item_id, sell_line.seller_quantity); if (buy_inst->IsStackable()) { if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) { buyer->Message(Chat::Red, "Error putting item in your inventory."); PutItemInInventoryWithStacking(buy_inst.get()); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); return; } } else { for (int i = 1; i <= sell_line.seller_quantity; i++) { buy_inst->SetCharges(1); if (!buyer->PutItemInInventoryWithStacking(buy_inst.get())) { buyer->Message(Chat::Red, "Error putting item in your inventory."); PutItemInInventoryWithStacking(buy_inst.get()); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); return; } } } uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; AddMoneyToPP(total_cost, false); buyer->TakeMoneyFromPP(total_cost, false); if (PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::BARTER_TRANSACTION)) { PlayerEvent::BarterTransaction e{}; e.status = "Successful Barter Transaction"; e.item_id = sell_line.item_id; e.item_quantity = sell_line.seller_quantity; e.item_name = sell_line.item_name; e.trade_items = sell_line.trade_items; for (auto &t: e.trade_items) { t *= sell_line.seller_quantity; } e.total_cost = total_cost; e.buyer_name = buyer->GetCleanName(); e.seller_name = GetCleanName(); RecordPlayerEventLog(PlayerEvent::BARTER_TRANSACTION, e); } SendWindowUpdatesToSellerAndBuyer(sell_line); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Success, Barter_Success ); buyer->SendBarterBuyerClientMessage( sell_line, Barter_BuyerTransactionComplete, Barter_Success, Barter_Success ); break; } case BarterOutsideBazaar: { bool seller_error = false; auto buyer_time = BuyerRepository::GetTransactionDate(database, sell_line.buyer_id); if (buyer_time > GetBarterTime()) { SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_DataOutOfDate ); return; } if (sell_line.trade_items.size() > 0) { Message(Chat::Red, "You must visit the buyer directly when receiving compensation items."); seller_error = true; } auto buy_item_slot_id = GetInv().HasItem( sell_line.item_id, sell_line.seller_quantity, invWherePersonal ); auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); if (!buy_item) { SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_SellerDoesNotHaveItem ); break; } if (seller_error) { LogTradingDetail("Seller Error [{}] Sell/Buy Transaction Failed.", seller_error ); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure ); return; } BuyerRepository::UpdateTransactionDate(database, sell_line.buyer_id, time(nullptr)); auto server_packet = std::make_unique( ServerOP_BuyerMessaging, static_cast(sizeof(BuyerMessaging_Struct)) ); auto data = (BuyerMessaging_Struct *) server_packet->pBuffer; data->action = Barter_SellItem; data->buyer_entity_id = sell_line.buyer_entity_id; data->buyer_id = sell_line.buyer_id; data->seller_entity_id = GetID(); data->buy_item_id = sell_line.item_id; data->buy_item_qty = sell_line.item_quantity; data->buy_item_cost = sell_line.item_cost; data->buy_item_icon = sell_line.item_icon; data->zone_id = GetZoneID(); data->slot = sell_line.slot; data->seller_quantity = sell_line.seller_quantity; data->purchase_method = sell_line.purchase_method; strn0cpy(data->item_name, sell_line.item_name, sizeof(data->item_name)); strn0cpy(data->buyer_name, sell_line.buyer_name.c_str(), sizeof(data->buyer_name)); strn0cpy(data->seller_name, GetCleanName(), sizeof(data->seller_name)); worldserver.SendPacket(server_packet.get()); break; } } } } void Client::SendBuyerPacket(Client* Buyer) { // This is the Buyer Appearance packet. This method is called for each Buyer when a Client connects to the zone. // auto outapp = new EQApplicationPacket(OP_Barter, 13 + strlen(GetName())); char* Buf = (char*)outapp->pBuffer; VARSTRUCT_ENCODE_TYPE(uint32, Buf, Barter_BuyerAppearance); VARSTRUCT_ENCODE_TYPE(uint32, Buf, Buyer->GetID()); VARSTRUCT_ENCODE_TYPE(uint32, Buf, 0x01); VARSTRUCT_ENCODE_STRING(Buf, GetName()); QueuePacket(outapp); safe_delete(outapp); } void Client::ToggleBuyerMode(bool status) { auto outapp = std::make_unique( OP_Barter, static_cast(sizeof(BuyerSetAppearance_Struct)) ); auto data = (BuyerSetAppearance_Struct *) outapp->pBuffer; data->action = Barter_BuyerAppearance; data->entity_id = GetID(); if (status && IsInBuyerSpace()) { SetBuyerID(CharacterID()); BuyerRepository::Buyer b{}; b.id = 0; b.char_id = GetBuyerID(); b.char_entity_id = GetID(); b.char_zone_id = GetZoneID(); b.char_zone_instance_id = GetInstanceID(); b.char_name = GetCleanName(); b.transaction_date = time(nullptr); BuyerRepository::DeleteBuyer(database, GetBuyerID()); BuyerRepository::InsertOne(database, b); data->status = BuyerBarter::On; SetCustomerID(0); SendBuyerMode(true); SendBuyerToBarterWindow(this, Barter_AddToBarterWindow); Message(Chat::Yellow, "Barter Mode ON."); } else { data->status = BuyerBarter::Off; BuyerRepository::DeleteBuyer(database, GetBuyerID()); SetCustomerID(0); SendBuyerToBarterWindow(this, Barter_RemoveFromBarterWindow); SendBuyerMode(false); SetBuyerID(0); if (!IsInBuyerSpace()) { Message(Chat::Red, "You must be in a Barter Stall to start Barter Mode."); } Message(Chat::Yellow, fmt::format("Barter Mode OFF. Buy lines deactivated.").c_str()); } entity_list.QueueClients(this, outapp.get(), false); } void Client::ModifyBuyLine(const EQApplicationPacket *app) { if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { BuyerBuyLines_Struct bl{}; auto in = (BuyerGeneric_Struct *) app->pBuffer; EQ::Util::MemoryStreamReader ss_in( reinterpret_cast(in->payload), app->size - sizeof(BuyerGeneric_Struct) ); cereal::BinaryInputArchive ar(ss_in); ar(bl); if (bl.buy_lines.empty()) { return; } BuyerRepository::UpdateTransactionDate(database, GetBuyerID(), time(nullptr)); int64 current_total_cost = 0; bool pass = false; auto current_buy_lines = BuyerBuyLinesRepository::GetBuyLines(database, CharacterID()); std::map item_map; BuildBuyLineMapFromVector(item_map, current_buy_lines); current_total_cost = ValidateBuyLineCost(item_map); auto buy_line = bl.buy_lines.front(); auto it = std::find_if( current_buy_lines.cbegin(), current_buy_lines.cend(), [&](BuyerLineItems_Struct bl) { return bl.slot == buy_line.slot; } ); if (buy_line.item_toggle) { current_total_cost += buy_line.item_cost * buy_line.item_quantity; if (it != std::end(current_buy_lines)) { current_total_cost -= it->item_cost * it->item_quantity; if (current_total_cost > GetCarriedMoney()) { buy_line.item_cost = it->item_cost; buy_line.item_quantity = it->item_quantity; Message( Chat::Red, fmt::format( "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", DetermineMoneyString(GetCarriedMoney()), DetermineMoneyString(current_total_cost)).c_str() ); SendBuyLineUpdate(buy_line); return; } else { RemoveItemFromBuyLineMap(item_map, *it); BuildBuyLineMapFromVector(item_map, bl.buy_lines); } } else { BuildBuyLineMapFromVector(item_map, bl.buy_lines); } } else { current_total_cost -= static_cast(buy_line.item_cost) * static_cast(buy_line.item_quantity); std::map item_map_tmp; BuildBuyLineMapFromVector(item_map_tmp, bl.buy_lines); if (ValidateBuyLineItems(item_map_tmp)) { pass = true; } } if (current_total_cost > static_cast(GetCarriedMoney())) { Message( Chat::Red, fmt::format( "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", DetermineMoneyString(GetCarriedMoney()), DetermineMoneyString(current_total_cost)).c_str() ); buy_line.item_toggle = 0; SendBuyLineUpdate(buy_line); return; } bool buyer_error = false; if (!ValidateBuyLineItems(item_map)) { buy_line.item_toggle = 0; } buy_line.item_icon = database.GetItem(buy_line.item_id)->Icon; if ((buy_line.item_toggle && it != std::end(current_buy_lines)) || pass) { BuyerBuyLinesRepository::ModifyBuyLine(database, buy_line, GetBuyerID()); Message(Chat::Yellow, fmt::format("Buy line for {} modified.", buy_line.item_name).c_str()); } else if (buy_line.item_toggle && it == std::end(current_buy_lines)) { BuyerBuyLinesRepository::CreateBuyLine(database, buy_line, GetBuyerID()); Message(Chat::Yellow, fmt::format("Buy line for {} enabled.", buy_line.item_name).c_str()); } else if (!buy_line.item_toggle) { BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot); Message(Chat::Yellow, fmt::format("Buy line for {} disabled.", buy_line.item_name).c_str()); } else { BuyerBuyLinesRepository::DeleteBuyLine(database, GetBuyerID(), buy_line.slot); Message( Chat::Yellow, fmt::format("Unhandled modification. Buy line for {} disabled.", buy_line.item_name).c_str()); } SendBuyLineUpdate(buy_line); if (IsThereACustomer()) { auto customer = entity_list.GetClientByID(GetCustomerID()); if (!customer) { return; } auto it = std::find_if( current_buy_lines.cbegin(), current_buy_lines.cend(), [&](BuyerLineItems_Struct bl) { return bl.slot == buy_line.slot; } ); if (it == std::end(current_buy_lines) && !buy_line.item_toggle) { return; } std::stringstream ss_customer{}; cereal::BinaryOutputArchive ar_customer(ss_customer); BuyerLineItems_Struct blis{}; blis.enabled = buy_line.enabled; blis.item_cost = buy_line.item_cost; blis.item_icon = buy_line.item_icon; blis.item_id = buy_line.item_id; blis.item_quantity = buy_line.item_quantity; blis.item_toggle = buy_line.item_toggle; blis.slot = buy_line.slot; blis.item_name = buy_line.item_name; for (auto const &i: buy_line.trade_items) { BuyerLineTradeItems_Struct bltis{}; bltis.item_icon = i.item_icon; bltis.item_id = i.item_id; bltis.item_quantity = i.item_quantity; bltis.item_name = i.item_name; blis.trade_items.push_back(bltis); } { ar_customer(blis); } auto packet = std::make_unique( OP_BuyerItems, static_cast(ss_customer.str().length()) + static_cast(sizeof(BuyerGeneric_Struct)) ); auto emu = (BuyerGeneric_Struct *) packet->pBuffer; emu->action = Barter_BuyerInspectBegin; memcpy(emu->payload, ss_customer.str().data(), ss_customer.str().length()); customer->QueuePacket(packet.get()); ss_customer.str(""); ss_customer.clear(); } } return; } void Client::BuyerItemSearch(const EQApplicationPacket *app) { auto bis = (BuyerItemSearch_Struct *) app->pBuffer; const EQ::ItemData *item = 0; uint32 it = 0; BuyerItemSearchResults_Struct bisr{}; while ((item = database.IterateItems(&it)) && bisr.results.size() < RuleI(Bazaar, MaxBuyerInventorySearchResults)) { if (!item->NoDrop) { continue; } auto item_name_match = std::strstr( Strings::ToLower(item->Name).c_str(), Strings::ToLower(bis->search_string).c_str() ); if (item_name_match) { BuyerItemSearchResultEntry_Struct bisre{}; bisre.item_id = item->ID; bisre.item_icon = item->Icon; strn0cpy(bisre.item_name, item->Name, sizeof(bisre.item_name)); bisr.results.push_back(bisre); } } bisr.action = Barter_BuyerSearchResults; bisr.result_count = bisr.results.size(); std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); { ar(bisr); } uint32 packet_size = sizeof(BuyerGeneric_Struct) + ss.str().length(); auto outapp = std::make_unique(OP_Barter, packet_size); auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = Barter_BuyerSearchResults; memcpy(emu->payload, ss.str().data(), ss.str().length()); QueuePacket(outapp.get()); ss.str(""); ss.clear(); } const std::string &Client::GetMailKeyFull() const { return m_mail_key_full; } const std::string &Client::GetMailKey() const { return m_mail_key; } void Client::SendBecomeTraderToWorld(Client *trader, BazaarTraderBarterActions action) { auto outapp = new ServerPacket(ServerOP_TraderMessaging, sizeof(TraderMessaging_Struct)); auto data = (TraderMessaging_Struct *) outapp->pBuffer; data->action = action; data->entity_id = trader->GetID(); data->trader_id = trader->CharacterID(); data->zone_id = trader->GetZoneID(); data->instance_id = trader->GetInstanceID(); strn0cpy(data->trader_name, trader->GetName(), sizeof(data->trader_name)); worldserver.SendPacket(outapp); safe_delete(outapp); } void Client::SendBecomeTrader(BazaarTraderBarterActions action, uint32 entity_id) { if (entity_id <= 0) { return; } auto trader = entity_list.GetClientByID(entity_id); if (!trader) { return; } auto outapp = new EQApplicationPacket(OP_BecomeTrader, sizeof(BecomeTrader_Struct)); auto data = (BecomeTrader_Struct *) outapp->pBuffer; data->action = action; data->entity_id = trader->GetID(); data->trader_id = trader->CharacterID(); data->zone_id = trader->GetZoneID(); data->zone_instance_id = trader->GetInstanceID(); strn0cpy(data->trader_name, trader->GetCleanName(), sizeof(data->trader_name)); QueuePacket(outapp); safe_delete(outapp); } void Client::SendTraderMode(BazaarTraderBarterActions status) { auto outapp = new EQApplicationPacket(OP_Trader, sizeof(Trader_ShowItems_Struct)); auto data = (Trader_ShowItems_Struct *) outapp->pBuffer; data->action = status; data->entity_id = GetID(); QueuePacket(outapp); safe_delete(outapp); } void Client::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 [{}] Item Serial No. [{}] New Price [{}]", 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 [{}] Charges is [{}]", 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 [{}] Charges is [{}]", 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.char_zone_instance_id = GetInstanceID(); 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]; trader_item.listing_date = time(nullptr); 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 [{}] ItemID [{}] SerialNumber [{}] Charges [{}] " "Price: [{}] Slot [{}]", 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; } TraderRepository::BulkTraders_Struct results{}; if (RuleB(Bazaar, UseAlternateBazaarSearch)) { if (GetZoneID() == Zones::BAZAAR) { results = TraderRepository::GetDistinctTraders(database, GetInstanceID()); } uint32 number = 1; auto shards = CharacterDataRepository::GetInstanceZonePlayerCounts(database, Zones::BAZAAR); for (auto const &shard: shards) { if (GetZoneID() != Zones::BAZAAR || (GetZoneID() == Zones::BAZAAR && GetInstanceID() != shard.instance_id)) { TraderRepository::DistinctTraders_Struct t{}; t.entity_id = 0; t.trader_id = TraderRepository::TRADER_CONVERT_ID + shard.instance_id; t.trader_name = fmt::format("Bazaar Shard {}", number); t.zone_id = Zones::BAZAAR; t.zone_instance_id = shard.instance_id; results.count += 1; results.name_length += t.trader_name.length() + 1; results.traders.push_back(t); } number++; } } else { results = TraderRepository::GetDistinctTraders( database, GetInstanceID(), EQ::constants::StaticLookup(ClientVersion())->BazaarTraderLimit ); } SetTraderCount(results.count); auto p_size = 4 + 12 * results.count + results.name_length; auto buffer = std::make_unique(p_size); memset(buffer.get(), 0, p_size); char *bufptr = buffer.get(); VARSTRUCT_ENCODE_TYPE(uint32, bufptr, results.count); for (auto t : results.traders) { VARSTRUCT_ENCODE_TYPE(uint16, bufptr, t.zone_id); VARSTRUCT_ENCODE_TYPE(uint16, bufptr, t.zone_instance_id); VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.trader_id); VARSTRUCT_ENCODE_TYPE(uint32, bufptr, t.entity_id); VARSTRUCT_ENCODE_STRING(bufptr, t.trader_name.c_str()); } auto outapp = std::make_unique(OP_TraderBulkSend, p_size); memcpy(outapp->pBuffer, buffer.get(), p_size); QueuePacket(outapp.get()); } void Client::DoBazaarInspect(BazaarInspect_Struct &in) { if (RuleB(Bazaar, UseAlternateBazaarSearch)) { if (in.trader_id >= TraderRepository::TRADER_CONVERT_ID) { auto trader = TraderRepository::GetTraderByInstanceAndSerialnumber( database, in.trader_id - TraderRepository::TRADER_CONVERT_ID, fmt::format("{}", in.serial_number).c_str() ); if (!trader.trader_id) { LogTrading("Unable to convert trader id for {} and serial number {}. Trader Buy aborted.", in.trader_id - TraderRepository::TRADER_CONVERT_ID, in.serial_number ); return; } in.trader_id = trader.trader_id; } } auto items = TraderRepository::GetWhere( database, fmt::format("`char_id` = '{}' AND `item_sn` = '{}'", in.trader_id, 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 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( OP_BazaarSearch, static_cast(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, tbs->trader_id); if (!trader_item.id || GetTraderTransactionDate() < trader_item.listing_date) { LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " "[{}] The Traders data was outdated.", tbs->trader_id, tbs->serial_number ); in->method = BazaarByParcel; in->sub_action = DataOutDated; TradeRequestFailed(app); return; } if (trader_item.active_transaction) { LogTrading("Attempt to purchase an item outside of the Bazaar trader_id [{}] item serial_number " "[{}] The item is already within an active transaction.", tbs->trader_id, tbs->serial_number ); in->method = BazaarByParcel; in->sub_action = DataOutDated; TradeRequestFailed(app); return; } TraderRepository::UpdateActiveTransaction(database, trader_item.id, true); std::unique_ptr 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 [{}] item_sn [{}] on trader", trader_item.item_id, trader_item.item_sn ); in->method = BazaarByParcel; in->sub_action = Failed; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } auto next_slot = FindNextFreeParcelSlot(CharacterID()); if (next_slot == INVALID_INDEX) { LogTrading( "{} attempted to purchase {} from the bazaar with parcel delivery. Unfortunately their parcel limit was reached. " "Purchase unsuccessful.", GetCleanName(), buy_item->GetItem()->Name ); in->method = BazaarByParcel; in->sub_action = TooManyParcels; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } LogTrading( "Name: [{}] IsStackable: [{}] Requested Quantity: [{}] Charges on Item [{}]", buy_item->GetItem()->Name, buy_item->IsStackable(), tbs->quantity, buy_item->GetCharges() ); // Determine the actual quantity for the purchase int32 charges = static_cast(tbs->quantity); if (!buy_item->IsStackable()) { if (buy_item->GetCharges() <= 0) { charges = 1; } else { charges = buy_item->GetCharges(); } } else { if (charges <= 0) { LogTrading("Rejecting purchase with zero/negative quantity [{}] for stackable item [{}]", charges, buy_item->GetItem()->Name); in->method = BazaarByParcel; in->sub_action = Failed; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } if (charges > trader_item.item_charges) { charges = trader_item.item_charges; } tbs->quantity = static_cast(charges); } LogTrading( "Actual quantity that will be traded is [{}] {}", tbs->quantity, buy_item->GetCharges() ? fmt::format("with {} charges", buy_item->GetCharges()) : "" ); uint64 total_cost = static_cast(tbs->price) * static_cast(tbs->quantity); if (total_cost > 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; } uint64 fee = std::round(total_cost * RuleR(Bazaar, ParcelDeliveryCostMod)); if (!TakeMoneyFromPP(total_cost + fee, false)) { 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 = BazaarByParcel; in->sub_action = InsufficientFunds; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } Message(Chat::Red, fmt::format("You paid {} for the parcel delivery.", DetermineMoneyString(fee)).c_str()); LogTrading("Customer [{}] Paid: [{}] in Copper", CharacterID(), total_cost); if (buy_item && PlayerEventLogs::Instance()->IsEventEnabled(PlayerEvent::TRADER_PURCHASE)) { auto e = PlayerEvent::TraderPurchaseEvent{ .item_id = buy_item->GetID(), .augment_1_id = buy_item->GetAugmentItemID(0), .augment_2_id = buy_item->GetAugmentItemID(1), .augment_3_id = buy_item->GetAugmentItemID(2), .augment_4_id = buy_item->GetAugmentItemID(3), .augment_5_id = buy_item->GetAugmentItemID(4), .augment_6_id = buy_item->GetAugmentItemID(5), .item_name = buy_item->GetItem()->Name, .trader_id = tbs->trader_id, .trader_name = tbs->seller_name, .price = tbs->price, .quantity = tbs->quantity, .charges = buy_item->IsStackable() ? 1 : charges, .total_cost = total_cost, .player_money_balance = GetCarriedMoney(), }; RecordPlayerEventLog(PlayerEvent::TRADER_PURCHASE, e); } CharacterParcelsRepository::CharacterParcels parcel_out{}; parcel_out.from_name = tbs->seller_name; parcel_out.note = "Delivered from a Bazaar Purchase"; parcel_out.sent_date = time(nullptr); parcel_out.quantity = charges; 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 = BazaarByParcel; in->sub_action = Failed; TraderRepository::UpdateActiveTransaction(database, trader_item.id, false); TradeRequestFailed(app); return; } ReturnTraderReq(app, tbs->quantity, buy_item->GetID()); if (PlayerEventLogs::Instance()->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.augment_1_id = parcel_out.aug_slot_1; e.augment_2_id = parcel_out.aug_slot_2; e.augment_3_id = parcel_out.aug_slot_3; e.augment_4_id = parcel_out.aug_slot_4; e.augment_5_id = parcel_out.aug_slot_5; e.augment_6_id = parcel_out.aug_slot_6; e.quantity = tbs->quantity; e.charges = buy_item->IsStackable() ? 1 : charges; 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 <= static_cast(tbs->quantity) || !buy_item->IsStackable()) { 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( ServerOP_BazaarPurchase, static_cast(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.get()); SendMoneyUpdate(); } void Client::SetBuyerWelcomeMessage(const char *welcome_message) { BuyerRepository::UpdateWelcomeMessage(database, CharacterID(), welcome_message); } void Client::SendBuyerGreeting(uint32 buyer_id) { auto buyer = BuyerRepository::GetWhere(database, fmt::format("`char_id` = '{}'", buyer_id)); if (buyer.empty()) { Message(Chat::White, "Welcome!"); return; } Message(Chat::White, buyer.front().welcome_message.c_str()); } void Client::SendSellerBrowsing(const std::string &browser) { auto outapp = std::make_unique(OP_Barter, static_cast(sizeof(BuyerBrowsing_Struct))); auto eq = (BuyerBrowsing_Struct *) outapp->pBuffer; eq->action = Barter_SellerBrowsing; strn0cpy(eq->char_name, browser.c_str(), sizeof(eq->char_name)); QueuePacket(outapp.get()); } void Client::SendBuyerMode(bool status) { auto outapp = std::make_unique(OP_Barter, 4); auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = status ? Barter_BuyerModeOn : Barter_BuyerModeOff; QueuePacket(outapp.get()); } bool Client::IsInBuyerSpace() { #define BUYER_DOOR_ARC_RADIUS_HIGH 91 #define BUYER_DOOR_ARC_RADIUS_LOW 71 #define BUYER_DOOR_OPEN_TYPE 155 #define TRADER_DOOR_OPEN_TYPE 153 struct BuyerDoorDataStruct { uint32 door_id; uint32 arc_offset; }; std::vector buyer_door_data = { {.door_id = 2}, {.arc_offset = 90},{.door_id = 3} ,{.arc_offset = 0} ,{.door_id = 4}, {.arc_offset = 0}, {.door_id = 5}, {.arc_offset = 0} ,{.door_id = 6} ,{.arc_offset = 90},{.door_id = 7}, {.arc_offset = 0}, {.door_id = 8}, {.arc_offset = 0} ,{.door_id = 9} ,{.arc_offset = 0} ,{.door_id = 10}, {.arc_offset = 0}, {.door_id = 11},{.arc_offset = 0} ,{.door_id = 12},{.arc_offset = 0} ,{.door_id = 13}, {.arc_offset = 0}, {.door_id = 14},{.arc_offset = 0} ,{.door_id = 15},{.arc_offset = 0} ,{.door_id = 16}, {.arc_offset = 90}, {.door_id = 17},{.arc_offset = 0} ,{.door_id = 18},{.arc_offset = 0} ,{.door_id = 19}, {.arc_offset = 0}, {.door_id = 20},{.arc_offset = 0} ,{.door_id = 21},{.arc_offset = 0} ,{.door_id = 22}, {.arc_offset = 0}, {.door_id = 23},{.arc_offset = 0} ,{.door_id = 24},{.arc_offset = 0} ,{.door_id = 25}, {.arc_offset = 0}, {.door_id = 26},{.arc_offset = 0} ,{.door_id = 27},{.arc_offset = 0} ,{.door_id = 28}, {.arc_offset = 0}, {.door_id = 29},{.arc_offset = 90},{.door_id = 30},{.arc_offset = 0} ,{.door_id = 31}, {.arc_offset = 0}, {.door_id = 32},{.arc_offset = 0} ,{.door_id = 33},{.arc_offset = 0} ,{.door_id = 34}, {.arc_offset = 0}, {.door_id = 35},{.arc_offset = 0} ,{.door_id = 36},{.arc_offset = 90},{.door_id = 37}, {.arc_offset = 0}, {.door_id = 38},{.arc_offset = 0} ,{.door_id = 39},{.arc_offset = 0} ,{.door_id = 40}, {.arc_offset = 0}, {.door_id = 41},{.arc_offset = 0} ,{.door_id = 42},{.arc_offset = 0} ,{.door_id = 43}, {.arc_offset = 90}, {.door_id = 44},{.arc_offset = 0} ,{.door_id = 45},{.arc_offset = 0} ,{.door_id = 46}, {.arc_offset = 0}, {.door_id = 47},{.arc_offset = 0} ,{.door_id = 48},{.arc_offset = 0} ,{.door_id = 49}, {.arc_offset = 0}, {.door_id = 50},{.arc_offset = 90},{.door_id = 51},{.arc_offset = 90},{.door_id = 52}, {.arc_offset = 0}, {.door_id = 53},{.arc_offset = 0} ,{.door_id = 54},{.arc_offset = 0}, {.door_id = 55}, {.arc_offset = 0}, {.door_id = 56},{.arc_offset = 0} ,{.door_id = 57},{.arc_offset = 0}, {.door_id = 122},{.arc_offset = 0} }; auto m_location = GetPosition(); for (auto const &d: buyer_door_data) { auto door = entity_list.GetDoorsByDoorID(d.door_id); if (door && IsWithinCircularArc( door->GetPosition(), m_location, d.arc_offset, BUYER_DOOR_ARC_RADIUS_HIGH, BUYER_DOOR_ARC_RADIUS_LOW ) ) { return true; } } for (auto const& d:entity_list.GetDoorsList()) { if (d.second->GetOpenType() == DoorType::BuyerStall) { if (IsWithinSquare(d.second->GetPosition(), d.second->GetSize(), GetPosition())) { return true; } } } return false; } void Client::CreateStartingBuyLines(const EQApplicationPacket *app) { if (ClientVersion() >= EQ::versions::ClientVersion::RoF) { BuyerBuyLines_Struct bl{}; auto in = (BuyerGeneric_Struct *) app->pBuffer; EQ::Util::MemoryStreamReader ss_in( reinterpret_cast(in->payload), app->size - sizeof(BuyerGeneric_Struct)); cereal::BinaryInputArchive ar(ss_in); ar(bl); if (bl.buy_lines.empty()) { return; } std::map item_map{}; if (!BuildBuyLineMap(item_map, bl)) { ToggleBuyerMode(false); return; } auto proposed_total_cost = ValidateBuyLineCost(item_map); if (proposed_total_cost == 0) { ToggleBuyerMode(false); return; } if (!ValidateBuyLineItems(item_map)) { ToggleBuyerMode(false); return; } std::stringstream ss_out{}; cereal::BinaryOutputArchive ar_out(ss_out); for (auto &b: bl.buy_lines) { BuyerBuyLinesRepository::CreateBuyLine(database, b, CharacterID()); { ar_out(b); } uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct); auto out = std::make_unique(OP_BuyerItems, packet_size); auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; data->action = Barter_BuyerItemUpdate; memcpy(data->payload, ss_out.str().data(), ss_out.str().length()); QueuePacket(out.get()); ss_out.str(""); ss_out.clear(); } Message(Chat::Yellow, fmt::format("{} buy lines enabled.", bl.buy_lines.size()).c_str()); } } void Client::SendBuyLineUpdate(const BuyerLineItems_Struct &buy_line) { std::stringstream ss_out{}; cereal::BinaryOutputArchive ar_out(ss_out); { ar_out(buy_line); } uint32 packet_size = ss_out.str().length() + sizeof(BuyerGeneric_Struct); auto out = std::make_unique(OP_BuyerItems, packet_size); auto data = (BazaarSearchMessaging_Struct *) out->pBuffer; data->action = Barter_BuyerItemUpdate; memcpy(data->payload, ss_out.str().data(), ss_out.str().length()); QueuePacket(out.get()); ss_out.str(""); ss_out.clear(); } void Client::CheckIfMovedItemIsPartOfBuyLines(uint32 item_id) { auto b_trade_items = BuyerTradeItemsRepository::GetTradeItems(database, GetBuyerID()); if (b_trade_items.empty()) { return; } auto it = std::find_if( b_trade_items.cbegin(), b_trade_items.cend(), [&](const BaseBuyerTradeItemsRepository::BuyerTradeItems bti) { return bti.item_id == item_id; } ); if (it != std::end(b_trade_items)) { auto item = GetInv().GetItem(GetInv().HasItem(item_id, 1, invWherePersonal)); if (!item) { return; } Message( Chat::Red, fmt::format( "You moved an item ({}) that is part of an active buy line.", item->GetItem()->Name ).c_str() ); ToggleBuyerMode(false); } } void Client::SendWindowUpdatesToSellerAndBuyer(BuyerLineSellItem_Struct &blsi) { auto buyer = entity_list.GetClientByID(blsi.buyer_entity_id); auto seller = this; if (!buyer || !seller) { return; } if (blsi.item_quantity - blsi.seller_quantity <= 0) { auto outapp = std::make_unique( OP_BuyerItems, static_cast(sizeof(BuyerRemoveItemFromMerchantWindow_Struct)) ); auto data = (BuyerRemoveItemFromMerchantWindow_Struct *) outapp->pBuffer; data->action = Barter_RemoveFromMerchantWindow; data->buy_slot_id = blsi.slot; QueuePacket(outapp.get()); std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); BuyerLineItems_Struct bl{}; bl.enabled = 0; bl.item_cost = blsi.item_cost; bl.item_icon = blsi.item_icon; bl.item_id = blsi.item_id; bl.item_quantity = blsi.item_quantity - blsi.seller_quantity; bl.item_name = blsi.item_name; bl.item_toggle = 0; bl.slot = blsi.slot; for (auto const &b: blsi.trade_items) { BuyerLineTradeItems_Struct blti{}; blti.item_icon = b.item_icon; blti.item_id = b.item_id; blti.item_quantity = b.item_quantity; blti.item_name = b.item_name; bl.trade_items.push_back(blti); } { ar(bl); } uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); outapp = std::make_unique(OP_BuyerItems, packet_size); auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = Barter_BuyerItemUpdate; memcpy(emu->payload, ss.str().data(), ss.str().length()); buyer->QueuePacket(outapp.get()); BuyerBuyLinesRepository::DeleteBuyLine(database, buyer->CharacterID(), blsi.slot); } else { std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); BuyerLineItems_Struct bli{}; bli.enabled = 1; bli.item_cost = blsi.item_cost; bli.item_icon = blsi.item_icon; bli.item_id = blsi.item_id; bli.item_quantity = blsi.item_quantity - blsi.seller_quantity; bli.item_toggle = 1; bli.slot = blsi.slot; bli.item_name = blsi.item_name; for (auto const &b: blsi.trade_items) { BuyerLineTradeItems_Struct blti{}; blti.item_id = b.item_id; blti.item_icon = b.item_icon; blti.item_quantity = b.item_quantity; blti.item_name = b.item_name; bli.trade_items.push_back(blti); } { ar(bli); } uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); auto outapp = std::make_unique(OP_BuyerItems, packet_size); auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = Barter_BuyerInspectBegin; memcpy(emu->payload, ss.str().data(), ss.str().length()); QueuePacket(outapp.get()); outapp = std::make_unique(OP_BuyerItems, packet_size); emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = Barter_BuyerItemUpdate; memcpy(emu->payload, ss.str().data(), ss.str().length()); buyer->QueuePacket(outapp.get()); BuyerBuyLinesRepository::ModifyBuyLine(database, bli, buyer->GetBuyerID()); } } void Client::SendBuyerToBarterWindow(Client *buyer, uint32 action) { auto server_packet = std::make_unique( ServerOP_BuyerMessaging, static_cast(sizeof(BuyerMessaging_Struct)) ); auto data = (BuyerMessaging_Struct *) server_packet->pBuffer; data->action = action; data->zone_id = buyer->GetZoneID(); data->buyer_id = buyer->GetBuyerID(); data->buyer_entity_id = buyer->GetID(); strn0cpy(data->buyer_name, buyer->GetCleanName(), sizeof(data->buyer_name)); worldserver.SendPacket(server_packet.get()); } void Client::SendBulkBazaarBuyers() { auto results = BuyerRepository::All(database); if (results.empty()) { return; } auto outapp = std::make_unique( OP_Barter, static_cast(sizeof(BuyerAddBuyertoBarterWindow_Struct)) ); auto emu = (BuyerAddBuyertoBarterWindow_Struct *) outapp->pBuffer; for (auto const &b: results) { auto buyer = entity_list.GetClientByCharID(b.char_id); emu->action = Barter_AddToBarterWindow; emu->buyer_id = b.char_id; emu->buyer_entity_id = buyer ? buyer->GetID() : 0; emu->zone_id = buyer ? buyer->GetZoneID() : 0; strn0cpy(emu->buyer_name, b.char_name.c_str(), sizeof(emu->buyer_name)); QueuePacket(outapp.get()); } } void Client::SendBarterBuyerClientMessage( BuyerLineSellItem_Struct &blsi, BarterBuyerActions action, BarterBuyerSubActions sub_action, BarterBuyerSubActions error_code ) { std::stringstream ss{}; cereal::BinaryOutputArchive ar(ss); blsi.sub_action = sub_action; blsi.error_code = error_code; { ar(blsi); } uint32 packet_size = ss.str().length() + sizeof(BuyerGeneric_Struct); auto outapp = std::make_unique(OP_BuyerItems, packet_size); auto emu = (BuyerGeneric_Struct *) outapp->pBuffer; emu->action = action; memcpy(emu->payload, ss.str().data(), ss.str().length()); QueuePacket(outapp.get()); } bool Client::BuildBuyLineMap(std::map &item_map, BuyerBuyLines_Struct &bl) { bool buyer_error = false; for (auto const &b: bl.buy_lines) { if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) { Message( Chat::Red, fmt::format( "You cannot have two buy lines for the same item {}. Buy line not possible.", b.item_name ).c_str() ); buyer_error = true; break; } BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity}; item_map.emplace(b.item_id, t); for (auto const &i: b.trade_items) { if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) { Message( Chat::Red, fmt::format( "You cannot buy {} and offer the same item as compensation. Buy line not possible.", i.item_name ).c_str() ); buyer_error = true; break; } if (item_map.contains(i.item_id)) { item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity; continue; } t = {0, i.item_quantity * b.item_quantity}; item_map.emplace(i.item_id, t); } } if (buyer_error) { return false; } return true; } bool Client::BuildBuyLineMapFromVector( std::map &item_map, std::vector &bl ) { bool buyer_error = false; for (auto const &b: bl) { if (item_map.contains(b.item_id) && item_map[b.item_id].item_cost > 0) { Message( Chat::Red, fmt::format( "You cannot have two buy lines for the same item {}. Buy line not possible.", b.item_name ).c_str() ); buyer_error = true; break; } BuylineItemDetails_Struct t = {b.item_quantity * b.item_cost, b.item_quantity}; item_map.emplace(b.item_id, t); for (auto const &i: b.trade_items) { if (item_map.contains(i.item_id) && item_map[i.item_id].item_cost > 0) { Message( Chat::Red, fmt::format( "You cannot buy {} and offer the same item as compensation. Buy line not possible.", i.item_name ).c_str() ); buyer_error = true; break; } if (item_map.contains(i.item_id)) { item_map[i.item_id].item_quantity += i.item_quantity * b.item_quantity; continue; } t = {0, i.item_quantity * b.item_quantity}; item_map.emplace(i.item_id, t); } } if (buyer_error) { return false; } return true; } void Client::RemoveItemFromBuyLineMap(std::map &item_map, const BuyerLineItems_Struct &bl) { if (item_map.contains(bl.item_id) && item_map[bl.item_id].item_cost > 0) { item_map.erase(bl.item_id); } for (auto const &i: bl.trade_items) { if (item_map.contains(i.item_id) && (item_map[i.item_id].item_quantity - (i.item_quantity * bl.item_quantity)) == 0) { item_map.erase(i.item_id); } else if (item_map.contains(i.item_id)) { item_map[i.item_id].item_quantity -= i.item_quantity * bl.item_quantity; } } } bool Client::ValidateBuyLineItems(std::map &item_map) { bool buyer_error = false; for (auto const &i: item_map) { auto item = database.GetItem(i.first); if (!item) { buyer_error = true; break; } if (i.second.item_cost > 0) { auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal); auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); if (buy_item && CheckLoreConflict(buy_item->GetItem())) { Message( Chat::Red, fmt::format( "You already have a {}. Purchasing another will cause a lore conflict. Buy line not possible.", buy_item->GetItem()->Name ).c_str() ); buyer_error = true; break; } } if (i.second.item_cost == 0) { if (i.second.item_quantity > 1 && CheckLoreConflict(item)) { Message( Chat::Red, fmt::format( "Your buy line requires {} {}s however the item is LORE. Buy line not possible.", i.second.item_quantity, item->Name ).c_str() ); buyer_error = true; break; } auto buy_item_slot_id = GetInv().HasItem(i.first, i.second.item_quantity, invWherePersonal); auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(buy_item_slot_id); if (!buy_item) { Message( Chat::Red, fmt::format( "Your buy line(s) require a total of {} {}{} which could not be found. Buy line not possible.", i.second.item_quantity, item->Name, i.second.item_quantity > 1 ? "s" : "" ).c_str() ); buyer_error = true; break; } if (buy_item->IsAugmentable() && buy_item->IsAugmented()) { Message( Chat::Red, fmt::format( "You cannot offer {} because it is augmented. Buy line not possible.", buy_item->GetItem()->Name ).c_str() ); buyer_error = true; break; } if (!buy_item->IsDroppable()) { Message( Chat::Red, fmt::format( "You cannot offer {} because it is NoTrade. Buy line not possible.", buy_item->GetItem()->Name ).c_str()); buyer_error = true; break; } buyer_error = false; } } return !buyer_error; } int64 Client::ValidateBuyLineCost(std::map &item_map) { uint64 proposed_total_cost = std::accumulate( item_map.cbegin(), item_map.cend(), static_cast(0), [](uint64 prev_sum, const std::pair &x) { return prev_sum + x.second.item_cost; } ); if (proposed_total_cost > GetCarriedMoney()) { Message( Chat::Red, fmt::format( "You currently do not have sufficient funds to support your buy lines. You have {} and need {}", DetermineMoneyString(GetCarriedMoney()), DetermineMoneyString(proposed_total_cost)).c_str() ); return 0; } return proposed_total_cost; } bool Client::DoBarterBuyerChecks(BuyerLineSellItem_Struct &sell_line) { bool buyer_error = false; auto buyer = entity_list.GetClientByID(sell_line.buyer_entity_id); if (!buyer) { return false; } auto buyer_time = BuyerRepository::GetTransactionDate(database, buyer->CharacterID()); if (buyer_time > GetBarterTime()) { if (sell_line.purchase_method == BarterByVendor) { SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Success, Barter_DataOutOfDate ); return false; } SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_DataOutOfDate); return false; } for (auto const &ti: sell_line.trade_items) { auto ti_slot_id = buyer->GetInv().HasItem( ti.item_id, ti.item_quantity * sell_line.seller_quantity, invWherePersonal ); if (ti_slot_id == INVALID_INDEX) { LogTradingDetail( "Seller attempting to sell item [{}] to buyer [{}] though buyer no longer has compensation item [{}]", sell_line.item_name, buyer->GetCleanName(), ti.item_name ); buyer->Message( Chat::Red, fmt::format( "{} wanted to sell you {} however you no longer have compensation item {}", sell_line.seller_name, sell_line.item_name, ti.item_name ).c_str()); buyer_error = true; break; } } uint64 total_cost = (uint64) sell_line.item_cost * (uint64) sell_line.seller_quantity; if (!buyer->HasMoney(total_cost)) { LogTradingDetail( "Seller attempting to sell item [{}] to buyer [{}] though buyer does not have enough money [{}]", sell_line.item_name, buyer->GetCleanName(), total_cost ); buyer->Message( Chat::Red, fmt::format( "{} wanted to sell you {} however you have insufficient funds.", sell_line.seller_name, sell_line.item_name ).c_str() ); buyer_error = true; } auto buy_item_slot_id = buyer->GetInv().HasItem( sell_line.item_id, sell_line.seller_quantity, invWherePersonal ); auto buy_item = buy_item_slot_id == INVALID_INDEX ? nullptr : buyer->GetInv().GetItem(buy_item_slot_id); if (buy_item && buyer->CheckLoreConflict(buy_item->GetItem())) { LogTradingDetail( "Seller attempting to sell item [{}] to buyer [{}] though buyer already has the item which is LORE.", sell_line.item_name, buyer->GetCleanName() ); buyer->Message( Chat::Red, fmt::format( "{} wanted to sell you {} however you already have the LORE item.", sell_line.seller_name, sell_line.item_name ).c_str() ); buyer_error = true; } if (buyer_error) { LogTradingDetail("Buyer error [{}] Barter Sell/Buy Transaction Failed.", buyer_error); SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure); return false; } return true; } bool Client::DoBarterSellerChecks(BuyerLineSellItem_Struct &sell_line) { bool seller_error = false; auto sell_item_slot_id = GetInv().HasItem(sell_line.item_id, sell_line.seller_quantity, invWherePersonal); auto sell_item = sell_item_slot_id == INVALID_INDEX ? nullptr : GetInv().GetItem(sell_item_slot_id); if (!sell_item) { seller_error = true; LogTradingDetail("Seller no longer has item [{}] to sell to buyer [{}]", sell_line.item_name, sell_line.buyer_name ); SendBarterBuyerClientMessage( sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_SellerDoesNotHaveItem ); } if (sell_item && sell_item->IsAugmentable() && sell_item->IsAugmented()) { seller_error = true; LogTradingDetail("Seller item [{}] is augmented therefore cannot be sold.", sell_line.item_name ); Message(Chat::Red, "The item that you are trying to sell is augmented. Please remove augments first"); } if (sell_item && !sell_item->IsDroppable()) { seller_error = true; LogTradingDetail("Seller item [{}] is non-tradeable therefore cannot be sold.", sell_line.item_name ); Message(Chat::Red, "The item that you are trying to sell is non-tradeable and therefore cannot be sold."); } if (seller_error) { LogTradingDetail("Seller Error [{}] Barter Sell/Buy Transaction Failed.", seller_error); SendBarterBuyerClientMessage(sell_line, Barter_SellerTransactionComplete, Barter_Failure, Barter_Failure); return false; } return true; } void Client::CancelBuyerTradeWindow() { auto end_session = new EQApplicationPacket(OP_Barter, sizeof(BuyerRemoveItemFromMerchantWindow_Struct)); auto data = reinterpret_cast(end_session->pBuffer); data->action = Barter_BuyerInspectBegin; FastQueuePacket(&end_session); } void Client::CancelTraderTradeWindow() { auto end_session = new EQApplicationPacket(OP_ShopEnd); FastQueuePacket(&end_session); }