Emu/source/WorldServer/Broker/BrokerManager.cpp
Emagi f60d261f00 Vault, Broker support for DoF, KoS, AoM. Shop support for DoF, KoS.
Broker/Vault (Fix #10):
- Traditional broker integrated (/frombroker), GM itemsearch is still available via /itemsearch.  AoM client cannot sell/use shop, can only buy from broker.
- DoF and KoS client now support House Shop (sell inventory, vault to broker).
- House containers now function like actual containers and can be stored in the vault which will allow placement in the home (to serve as a merchant).
- Sales crate merchant windows in player houses implemented for broker.
- Peering URI's added for broker/shop support: /addseller, /removeseller, /additemsale, /removeitemsale
- House vault and vault slots implemented for DoF, KoS and AoM clients.

Stability and Functionality:
- World Database now has seq_character_items (Sequence) to sync self and peers instantiating new unique IDs for character items.  This is to avoid conflict/overriding another unique id.
- spawn and spawn_houses tables have a lua_script that can now be defined outside the existing spawn_scripts table for a singular spawn.
- Fixed a watchdog/hangup when clearing hate lists inside a spawn list lock.
- Item unique id is being transitioned to int64 (although older clients still only support int32, clients later on are int64).
- Fixed so that spells that have no duration will no longer be added to maintained effects.
- Fixed spell cleanup, maintained effect does not remain on the Player causing a crash.
- Fixed spell conflicts to that check all targets are applicable for the spell.
- Fixed issues with maintained effects or spell effects stacking repeatedly.
- Fixed assigning items to non inventory slots when 'adding' an item to the Player.
- Fixed locking orders between maintained effects and spell effects to avoid deadlocks.
- Fixed entering house and visiting houses, targetting of the house door is now enforced server side.
- Item locking is now enforced by the type of locking (eg. house placement, crafting, shop list for sale).  Locks no longer override/interfere with each other.
- Additional logging around spell casting and targets.
- Spawns/Objects/Widgets so on related to houses now have their own sub tables _houses, eg. spawn_houses, spawn_npc_houses, spawn_object_houses, so on to avoid conflicting with existing tables non-house.

- new LUA Functions:
	ShowShopWindow(Player, FromSpawn) - opens shop window for player (if in their house) for listing, pricing items, retrieving sales log and coin, etc.
	SetSpawnHouseScript(Target, LuaScript) - Utilized in the item script 'placed' function to set the spawned house item's lua script.
	SendBook(Target, ItemID) - Sends the book to the target player based on the item id.
	GetPickupItemID(Spawn) - Gets the item id that the house spawn would represent
	SetHouseCharacterID(Spawn, CharID) - Sets the house spawn character id (should be used on Spawn).  Set CharID to 0 to set to the current houses character id.

- Updated LUA Functions:
	StartHeroicOpportunity(Caster, ClassID, OverrideTarget)  - OverrideTarget now available to change the heroic opportunity target
	HasItem(Player, ItemID, IncludeBank) - No parameter change, include bank, which serves as 'all' should now work correctly (previously did not check bank/other negative slots),  default is false.

- Slash Commands Added:
	/sle - Fix #41, Set Location Entry (DB command for setting spawn location entry values such as offsets and overrides)
	/store_list_item - used to list an item for broker shop
	/store_set_price - used for setting items price for shop
	/store_set_price_local - used for inventory items price for shop
	/store_start_selling - Begin selling from inventory for shop
	/store_stop_selling - Stop selling from inventory
	/store_unlist_item - used to unlist an item from broker shop
	/close_store_keep_selling - Closes store shop window, but keep selling from inventory while in house
	/cancel_store - cancel selling from inventory
2025-07-26 07:10:07 -04:00

954 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
EQ2Emulator: Everquest II Server Emulator
Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net)
This file is part of EQ2Emulator.
EQ2Emulator 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.
EQ2Emulator 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 EQ2Emulator. If not, see <http://www.gnu.org/licenses/>.
*/
#include "BrokerManager.h"
#include "../Items/Items.h"
#include "../../common/Log.h"
#include "../WorldDatabase.h"
#include "../World.h"
#include "../Web/PeerManager.h"
#include <cstdlib>
extern WorldDatabase database;
extern ZoneList zone_list;
extern PeerManager peer_manager;
BrokerManager::BrokerManager() {
}
void BrokerManager::AddSeller(int32 cid,
const std::string& name,
int32 hid,
bool enabled,
bool invFlag)
{
int64 prevSession = 0, prevTotal = 0;
{
std::shared_lock lock(mtx_);
auto sit = players_.find(cid);
if (sit != players_.end()) {
prevSession = sit->second.coin_session;
prevTotal = sit->second.coin_total;
}
}
SellerInfo info{cid, name, hid, enabled, invFlag, prevSession, prevTotal};
{
std::unique_lock lock(mtx_);
players_[cid] = info;
}
SavePlayerToDB(info);
peer_manager.sendPeersAddSeller(cid, hid, name, enabled, invFlag);
}
void BrokerManager::LoadSeller(int32 cid, const std::string& name,
int32 hid, bool enabled, bool invFlag,
int64 coin_session, int64 coin_total)
{
SellerInfo info{cid, name, hid, enabled, invFlag, coin_session, coin_total};
{
std::unique_lock lock(mtx_);
players_[cid] = info;
}
}
int64 BrokerManager::ResetSellerSessionCoins(int32 cid) {
database.ClearSellerSession(cid);
int64 session_coin = 0;
{
std::unique_lock lock(mtx_);
auto it = players_.find(cid);
if (it == players_.end()) return 0;
session_coin = it->second.coin_session;
it->second.coin_total += it->second.coin_session;
it->second.coin_session = 0;
}
return session_coin;
}
void BrokerManager::AddSellerSessionCoins(int32 cid, uint64 session) {
database.AddToSellerSession(cid, session);
{
std::unique_lock lock(mtx_);
auto it = players_.find(cid);
if (it == players_.end()) return;
it->second.coin_session += session;
}
}
void BrokerManager::RemoveSeller(int32 cid, bool peerCacheOnly)
{
{
std::unique_lock lock(mtx_);
players_.erase(cid);
active_items_by_char_.erase(cid);
inactive_items_by_char_.erase(cid);
}
if(!peerCacheOnly)
peer_manager.sendPeersRemoveSeller(cid);
}
void BrokerManager::AddItem(const SaleItem& item, bool peerCacheOnly)
{
{
std::unique_lock lock(mtx_);
auto& a = active_items_by_char_[item.character_id];
auto& i = inactive_items_by_char_[item.character_id];
a.erase(item.unique_id);
i.erase(item.unique_id);
if (item.for_sale) a[item.unique_id] = item;
else i[item.unique_id] = item;
}
SaveItemToDB(item);
if(!peerCacheOnly)
peer_manager.sendPeersAddItemSale(item.character_id, item.house_id, item.item_id, item.unique_id, item.cost_copper, item.inv_slot_id,
item.slot_id, item.count, item.from_inventory, item.for_sale, item.creator);
}
void BrokerManager::LoadItem(const SaleItem& item)
{
std::unique_lock lock(mtx_);
if (item.for_sale)
active_items_by_char_[item.character_id][item.unique_id] = item;
else
inactive_items_by_char_[item.character_id][item.unique_id] = item;
}
void BrokerManager::SetSaleStatus(int32 cid, int64 uid, bool for_sale)
{
std::optional<SaleItem> toUpdate;
{
std::unique_lock lock(mtx_);
auto& a = active_items_by_char_[cid];
auto& i = inactive_items_by_char_[cid];
if (for_sale) {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSaleStatus: %u (%u), for_sale=%u",
cid, uid, for_sale
);
if (auto it = i.find(uid); it != i.end()) {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSaleStatusFuckOff: %u (%u), for_sale=%u",
cid, uid, for_sale
);
SaleItem copy = it->second;
copy.for_sale = true;
i.erase(it);
a[uid] = copy;
toUpdate = copy;
}
} else {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSaleStatusInactive: %u (%u), for_sale=%u",
cid, uid, for_sale
);
if (auto it = a.find(uid); it != a.end()) {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSaleStatusFuckyou: %u (%u), for_sale=%u",
cid, uid, for_sale
);
SaleItem copy = it->second;
copy.for_sale = false;
a.erase(it);
i[uid] = copy;
toUpdate = copy;
}
}
}
if (toUpdate) {
SaveItemToDB(*toUpdate);
peer_manager.sendPeersAddItemSale(toUpdate->character_id, toUpdate->house_id, toUpdate->item_id, toUpdate->unique_id, toUpdate->cost_copper, toUpdate->inv_slot_id,
toUpdate->slot_id, toUpdate->count, toUpdate->from_inventory, toUpdate->for_sale, toUpdate->creator);
}
}
bool BrokerManager::IsItemListed(int32 cid, int64 uid) {
std::shared_lock lock(mtx_);
auto& active_map = active_items_by_char_[cid];
auto& inactive_map = inactive_items_by_char_[cid];
auto it = inactive_map.find(uid);
if (it != inactive_map.end()) {
return true;
}
auto it2 = active_map.find(uid);
if (it2 != active_map.end()) {
return true;
}
return false;
}
void BrokerManager::SetSalePrice(int32 cid, int64 uid, int64 price)
{
std::optional<SaleItem> toUpdate;
{
std::unique_lock lock(mtx_);
if (auto ait = active_items_by_char_.find(cid); ait != active_items_by_char_.end()) {
if (auto it = ait->second.find(uid); it != ait->second.end()) {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSalePriceActive: %u (%u), cost=%u",
cid, uid, price
);
it->second.cost_copper = price;
toUpdate = it->second;
}
}
if (auto iit = inactive_items_by_char_.find(cid); iit != inactive_items_by_char_.end()) {
if (auto it = iit->second.find(uid); it != iit->second.end()) {
LogWrite(PLAYER__ERROR, 5, "Broker",
"--SetSalePriceInactive: %u (%u), cost=%u",
cid, uid, price
);
it->second.cost_copper = price;
toUpdate = it->second;
}
}
}
if (toUpdate) {
SaveItemToDB(*toUpdate);
peer_manager.sendPeersAddItemSale(toUpdate->character_id, toUpdate->house_id, toUpdate->item_id, toUpdate->unique_id, toUpdate->cost_copper, toUpdate->inv_slot_id,
toUpdate->slot_id, toUpdate->count, toUpdate->from_inventory, toUpdate->for_sale, toUpdate->creator);
}
}
void BrokerManager::RemoveItem(int32 cid, int64 uid, int16 qty)
{
bool didDelete = false;
SaleItem snapshot;
{
std::unique_lock lock(mtx_);
if (auto ait = active_items_by_char_.find(cid); ait != active_items_by_char_.end()) {
auto& m = ait->second;
if (auto it = m.find(uid); it != m.end()) {
it->second.count -= qty;
if (it->second.count <= 0) {
didDelete = true;
snapshot = it->second;
m.erase(it);
} else {
snapshot = it->second;
}
if (m.empty())
active_items_by_char_.erase(ait);
}
}
}
if (didDelete) {
DeleteItemFromDB(cid, uid);
peer_manager.sendPeersRemoveItemSale(cid, uid);
}
else if (snapshot.count > 0) {
SaveItemToDB(snapshot);
peer_manager.sendPeersAddItemSale(snapshot.character_id, snapshot.house_id, snapshot.item_id, snapshot.unique_id, snapshot.cost_copper, snapshot.inv_slot_id,
snapshot.slot_id, snapshot.count, snapshot.from_inventory, snapshot.for_sale, snapshot.creator);
}
}
bool BrokerManager::BuyItem(Client* buyer, int32 seller_cid, int64 uid, int32 quantity)
{
Client* seller = zone_list.GetClientByCharID(seller_cid); // establish if seller is online
if(buyer && buyer->GetCharacterID() == seller->GetCharacterID()) {
buyer->Message(CHANNEL_COLOR_RED, "You cannot buy from yourself!");
return false;
}
if (quantity <= 0 || !IsSaleEnabled(seller_cid) || !IsItemForSale(seller_cid, uid)) {
if(buyer)
buyer->Message(CHANNEL_COLOR_RED, "Quantity not provided (%u), sale is not enabled (sale enabled? %u) or item is not for sale (itemforsale? %u).", quantity, IsSaleEnabled(seller_cid), IsItemForSale(seller_cid, uid));
return false;
}
int64 price = GetSalePrice(seller_cid, uid) * quantity;
if(!buyer || !buyer->GetPlayer() || !buyer->GetPlayer()->RemoveCoins(price)) {
if(buyer)
buyer->Message(CHANNEL_COLOR_RED, "You do not have enough coin to purchase.");
return false;
}
if (!database.RemoveBrokerItem(seller_cid, uid, quantity)) {
buyer->GetPlayer()->AddCoins(price);
buyer->Message(CHANNEL_COLOR_RED, "Failed to remove broker item from database.");
return false;
}
Item* giveItem = nullptr;
int16 result_count = 0;
std::string creator;
bool deleteItem = false;
int16 quantity_left = 0;
// 2) Mirror in-memory
std::optional<SaleItem> toUpdate;
{
std::unique_lock lock(mtx_);
if (auto sit = active_items_by_char_.find(seller_cid); sit != active_items_by_char_.end()) {
auto& m = sit->second;
if (auto it = m.find(uid); it != m.end()) {
creator = it->second.creator;
giveItem = master_item_list.GetItem(it->second.item_id);
SaleItem copy = it->second;
toUpdate = copy;
if (it->second.count > quantity) {
it->second.count -= quantity;
quantity_left = it->second.count;
result_count = quantity;
if(seller && seller->GetPlayer()) {
seller->GetPlayer()->item_list.SetVaultItemUniqueIDCount(seller, uid, it->second.count);
}
}
else {
result_count = it->second.count;
if(seller && seller->GetPlayer()) {
seller->GetPlayer()->item_list.RemoveVaultItemFromUniqueID(seller, uid);
}
m.erase(it);
deleteItem = true;
}
if (m.empty())
active_items_by_char_.erase(sit);
}
}
}
if(!giveItem) {
buyer->GetPlayer()->AddCoins(price);
buyer->Message(CHANNEL_COLOR_RED, "Failed to find item on broker memory.");
if(toUpdate)
AddItem(*toUpdate);
return false;
}
Item* resultItem = new Item(giveItem);
resultItem->details.count = result_count;
resultItem->creator = creator;
if (buyer->GetPlayer()->item_list.HasFreeSlot() || buyer->GetPlayer()->item_list.CanStack(resultItem, false)) {
if(!buyer->AddItem(resultItem, nullptr, AddItemType::BUY_FROM_BROKER)) {
buyer->GetPlayer()->AddCoins(price);
safe_delete(resultItem);
if(toUpdate)
AddItem(*toUpdate);
return false;
}
}
else {
buyer->Message(CHANNEL_COLOR_RED, "You have no free slot available.");
buyer->GetPlayer()->AddCoins(price);
safe_delete(resultItem);
if(toUpdate)
AddItem(*toUpdate);
return false;
}
if(deleteItem) {
DeleteItemFromDB(seller_cid, uid);
DeleteCharacterItemFromDB(seller_cid, uid);
}
else if(quantity_left) {
UpdateCharacterItemDB(seller_cid, uid, quantity_left);
}
AddSellerSessionCoins(seller_cid, price);
LogSale(seller_cid, std::string(buyer->GetPlayer()->GetName()), std::string(resultItem->name), result_count, price);
if(seller) {
std::string logMsg = GetShopPurchaseMessage(buyer->GetPlayer()->GetName(), std::string(resultItem->name), result_count, price);
auto seller_info = GetSellerInfo(seller_cid);
seller->SendHouseSaleLog(logMsg, 0, seller_info ? seller_info->coin_total : 0, 0);
seller->OpenShopWindow(nullptr);
}
return true;
}
void BrokerManager::OnPeerRemoveItem(int32 cid, int64 uid)
{
std::unique_lock lock(mtx_);
if (auto ait = active_items_by_char_.find(cid); ait != active_items_by_char_.end())
ait->second.erase(uid);
if (auto iit = inactive_items_by_char_.find(cid); iit != inactive_items_by_char_.end())
iit->second.erase(uid);
}
bool BrokerManager::IsItemForSale(int32 cid, int64 uid) const
{
std::shared_lock lock(mtx_);
if (auto pit = active_items_by_char_.find(cid); pit != active_items_by_char_.end())
return pit->second.find(uid) != pit->second.end();
return false;
}
int64 BrokerManager::GetSalePrice(int32 cid, int64 uid)
{
std::shared_lock lock(mtx_);
if (auto pit = active_items_by_char_.find(cid); pit != active_items_by_char_.end()) {
if (auto it = pit->second.find(uid); it != pit->second.end())
return it->second.cost_copper;
}
if (auto pit2 = inactive_items_by_char_.find(cid); pit2 != inactive_items_by_char_.end()) {
if (auto it = pit2->second.find(uid); it != pit2->second.end())
return it->second.cost_copper;
}
return 0;
}
std::vector<SaleItem> BrokerManager::GetActiveForSaleItems(int32 cid) const
{
std::shared_lock lock(mtx_);
std::vector<SaleItem> out;
if (auto pit = active_items_by_char_.find(cid); pit != active_items_by_char_.end()) {
for (auto const& kv : pit->second)
out.push_back(kv.second);
}
return out;
}
std::optional<SaleItem> BrokerManager::GetActiveItem(int32 cid, int64 uid) const
{
std::shared_lock lock(mtx_);
auto pit = active_items_by_char_.find(cid);
if (pit == active_items_by_char_.end())
return std::nullopt;
auto it = pit->second.find(uid);
if (it == pit->second.end())
return std::nullopt;
return it->second; // copy out the SaleItem
}
bool BrokerManager::IsSellingItems(int32 cid, bool vaultOnly) const
{
// Grab shared lock for threadsafe read
std::shared_lock lock(mtx_);
// Find this characters active sales
auto pit = active_items_by_char_.find(cid);
if (pit == active_items_by_char_.end())
return false; // no items => not selling from vault
// Scan through each SaleItem; if any has sell_from_inventory == false,
// that means its coming from the vault
for (auto const& kv : pit->second) {
const SaleItem& item = kv.second;
if ((item.for_sale && (!item.from_inventory || !vaultOnly)))
return true;
}
return false;
}
std::vector<SaleItem> BrokerManager::GetInactiveItems(int32 cid) const
{
std::shared_lock lock(mtx_);
std::vector<SaleItem> out;
if (auto pit = inactive_items_by_char_.find(cid); pit != inactive_items_by_char_.end()) {
for (auto const& kv : pit->second)
out.push_back(kv.second);
}
return out;
}
std::vector<std::pair<int64,int32>>
BrokerManager::GetUniqueIDsAndCost(int32 cid) const
{
std::shared_lock lock(mtx_);
std::vector<std::pair<int64,int32>> out;
if (auto pit = active_items_by_char_.find(cid); pit != active_items_by_char_.end()) {
for (auto const& kv : pit->second)
out.emplace_back(kv.second.unique_id, kv.second.cost_copper);
}
return out;
}
vector<Item*>* BrokerManager::GetItems(
const std::string& name,
int64 itype,
int64 ltype,
int64 btype,
int64 minprice,
int64 maxprice,
int8 minskill,
int8 maxskill,
const std::string& seller,
const std::string& adornment,
int8 mintier,
int8 maxtier,
int16 minlevel,
int16 maxlevel,
int8 itemclass
) const {
vector<Item*>* ret = new vector<Item*>;
string lower_name = ::ToLower(string(name.c_str()));
std::shared_lock lock(mtx_);
std::vector<SaleItem> out;
for (auto const& char_pair : active_items_by_char_) {
int32 cid = char_pair.first;
auto pit_player = players_.find(cid);
if (pit_player == players_.end())
continue;
bool allowInv = pit_player->second.sell_from_inventory;
for (auto const& kv : char_pair.second) {
auto const& itm = kv.second;
LogWrite(PLAYER__ERROR, 5, "Broker",
"--GetItems: %u (selling: %u), allowinv: %u",
itm.unique_id, itm.for_sale, allowInv
);
if (!itm.for_sale) continue;
if (!allowInv && itm.from_inventory) continue;
Item* def = master_item_list.GetItem(itm.item_id);
if (!def) continue;
LogWrite(PLAYER__ERROR, 5, "Broker",
"--GetItems#1: %u (selling: %u), allowinv: %u",
itm.unique_id, itm.for_sale, allowInv
);
if (!name.empty() && def->lowername.find(lower_name) == std::string::npos) continue;
if (itype!=ITEM_BROKER_TYPE_ANY && itype !=ITEM_BROKER_TYPE_ANY64BIT && !master_item_list.ShouldAddItemBrokerType(def, itype)) continue;
if (ltype!=ITEM_BROKER_SLOT_ANY && !master_item_list.ShouldAddItemBrokerSlot(def, ltype)) continue;
if (btype!=0xFFFFFFFF && !master_item_list.ShouldAddItemBrokerStat(def, btype)) continue;
LogWrite(PLAYER__ERROR, 5, "Broker",
"--GetItems#2: %u (cost_copper: %u), seller: %s",
itm.unique_id, itm.cost_copper, seller.c_str()
);
//if (itm.cost_copper < minprice || itm.cost_copper > maxprice) continue;
//if (!seller.empty() && pit_player->second.seller_name.find(seller)==std::string::npos) continue;
/*if(mintier > 1 && def->details.tier < mintier)
continue;
if(maxtier > 0 && def->details.tier > maxtier)
continue;*/
LogWrite(PLAYER__ERROR, 5, "Broker",
"--GetItems#3: %u (selling: %u), allowinv: %u",
itm.unique_id, itm.for_sale, allowInv
);
if(def->generic_info.adventure_default_level == 0 && def->generic_info.tradeskill_default_level == 0 && minlevel > 0 && maxlevel > 0){
if(def->details.recommended_level < minlevel)
continue;
if(def->details.recommended_level > maxlevel)
continue;
}
else{
if(minlevel > 0 && ((def->generic_info.adventure_default_level == 0 && def->generic_info.tradeskill_default_level == 0) || (def->generic_info.adventure_default_level > 0 && def->generic_info.adventure_default_level < minlevel) || (def->generic_info.tradeskill_default_level > 0 && def->generic_info.tradeskill_default_level < minlevel)))
continue;
if(maxlevel > 0 && ((def->generic_info.adventure_default_level > 0 && def->generic_info.adventure_default_level > maxlevel) || (def->generic_info.tradeskill_default_level > 0 && def->generic_info.tradeskill_default_level > maxlevel)))
continue;
}
if (itemclass>0) {
int64 bit = ((int64)2 << (itemclass-1));
if (!(def->generic_info.adventure_classes & bit) &&
!(def->generic_info.tradeskill_classes & bit))
continue;
}
LogWrite(PLAYER__ERROR, 5, "Broker",
"--GetItemsPass: %u (selling: %u), allowinv: %u",
itm.unique_id, itm.for_sale, allowInv
);
ret->push_back(new Item(def, itm.unique_id, itm.creator, pit_player->second.seller_name, cid, itm.cost_copper, itm.count, pit_player->second.house_id));
}
}
return ret;
}
std::string BrokerManager::GetSellerName(int32 cid) const {
std::shared_lock lock(mtx_);
auto it = players_.find(cid);
return (it != players_.end()) ? it->second.seller_name : std::string();
}
bool BrokerManager::IsSaleEnabled(int32 cid) const {
std::shared_lock lock(mtx_);
auto it = players_.find(cid);
return (it != players_.end()) ? it->second.sale_enabled : false;
}
bool BrokerManager::CanSellFromInventory(int32 cid) const {
std::shared_lock lock(mtx_);
auto it = players_.find(cid);
return (it != players_.end()) ? it->second.sell_from_inventory : false;
}
int32 BrokerManager::GetHouseID(int32 cid) const {
std::shared_lock lock(mtx_);
auto it = players_.find(cid);
return (it != players_.end()) ? it->second.house_id : -1;
}
std::optional<SellerInfo> BrokerManager::GetSellerInfo(int32 cid) const {
std::shared_lock lock(mtx_);
auto it = players_.find(cid);
if (it == players_.end())
return std::nullopt;
return it->second;
}
void BrokerManager::SavePlayerToDB(const SellerInfo& p) {
Query q;
std::ostringstream oss;
oss << "INSERT INTO broker_sellers "
"(character_id,seller_name,house_id,sale_enabled,sell_from_inventory,coin_session,coin_total) "
"VALUES("
<< p.character_id << ",'"
<< EscapeSQLString(p.seller_name) << "',"
<< p.house_id << ","
<< (p.sale_enabled ? 1 : 0) << ","
<< (p.sell_from_inventory ? 1 : 0) << ","
<< p.coin_session << ","
<< p.coin_total
<< ") ON DUPLICATE KEY UPDATE "
"seller_name=VALUES(seller_name),"
"house_id=VALUES(house_id),"
"sale_enabled=VALUES(sale_enabled),"
"sell_from_inventory=VALUES(sell_from_inventory),"
"coin_session=VALUES(coin_session), "
"coin_total=VALUES(coin_total)";
std::string sql = oss.str();
q.AddQueryAsync(p.character_id, &database, Q_INSERT,
sql.c_str()
);
}
#include <sstream>
void BrokerManager::SaveItemToDB(const SaleItem& i) {
// 2) Build full SQL
std::ostringstream oss;
oss
<< "INSERT INTO broker_items "
"(unique_id,character_id,house_id,item_id,cost_copper,for_sale,"
"inv_slot_id,slot_id,`count`,from_inventory,creator) "
"VALUES("
<< i.unique_id << ","
<< i.character_id << ","
<< i.house_id << ","
<< i.item_id << ","
<< i.cost_copper << ","
<< (i.for_sale ? 1 : 0) << ","
<< i.inv_slot_id << ","
<< i.slot_id << ","
<< i.count << ","
<< (i.from_inventory ? 1 : 0) << ",'"
<< EscapeSQLString(i.creator) << "') "
"ON DUPLICATE KEY UPDATE "
"house_id=VALUES(house_id),"
"item_id=VALUES(item_id),"
"cost_copper=VALUES(cost_copper),"
"for_sale=VALUES(for_sale),"
"inv_slot_id=VALUES(inv_slot_id),"
"slot_id=VALUES(slot_id),"
"`count`=VALUES(`count`),"
"from_inventory=VALUES(from_inventory),"
"creator=VALUES(creator)";
std::string sql = oss.str();
Query q;
q.AddQueryAsync(i.character_id, &database, Q_INSERT, sql.c_str());
}
void BrokerManager::UpdateItemInDB(const SaleItem& i) {
Query q;
std::ostringstream oss;
oss << "UPDATE broker_items SET "
<< "house_id=" << i.house_id
<< ",item_id=" << i.item_id
<< ",cost_copper=" << i.cost_copper
<< ",for_sale=" << (i.for_sale ? 1 : 0)
<< ",inv_slot_id=" << i.inv_slot_id
<< ",slot_id=" << i.slot_id
<< ",count=" << i.count
<< ",from_inventory=" << (i.from_inventory ? 1 : 0)
<< ",creator='" << EscapeSQLString(i.creator) << "' "
<< "WHERE unique_id=" << i.unique_id
<< " AND character_id=" << i.character_id;
std::string sql = oss.str();
q.AddQueryAsync(i.character_id, &database, Q_UPDATE,
sql.c_str()
);
}
void BrokerManager::DeleteItemFromDB(int32 cid, int64 uid) {
Query q;
q.AddQueryAsync(cid, &database, Q_DELETE,
"DELETE FROM broker_items WHERE unique_id=%llu AND character_id=%u",
uid, cid
);
}
void BrokerManager::DeleteCharacterItemFromDB(int32 cid, int64 uid) {
Query q;
q.AddQueryAsync(cid, &database, Q_DELETE,
"DELETE FROM character_items WHERE id=%llu AND char_id=%u",
uid, cid
);
}
void BrokerManager::UpdateCharacterItemDB(int32 cid, int64 uid, int16 count) {
Query q;
q.AddQueryAsync(cid, &database, Q_UPDATE,
"UPDATE character_items set count=%u WHERE id=%llu AND char_id=%u",
count, uid, cid
);
}
void BrokerManager::DeletePlayerFromDB(int32 cid) {
Query q;
// delete from broker_sellers
q.AddQueryAsync(cid, &database, Q_DELETE,
"DELETE FROM broker_sellers WHERE character_id=%u", cid);
// delete all their items
q.AddQueryAsync(cid, &database, Q_DELETE,
"DELETE FROM broker_items WHERE character_id=%u", cid);
}
bool BrokerManager::IsItemFromInventory(int32 cid, int64 uid) const {
std::shared_lock lock(mtx_);
// 1) Check active items for that character
auto pit = active_items_by_char_.find(cid);
if (pit != active_items_by_char_.end()) {
auto it = pit->second.find(uid);
if (it != pit->second.end())
return it->second.from_inventory;
}
// 2) Otherwise check inactive items
auto iit = inactive_items_by_char_.find(cid);
if (iit != inactive_items_by_char_.end()) {
auto it = iit->second.find(uid);
if (it != iit->second.end())
return it->second.from_inventory;
}
// 3) Not found → false
return false;
}
void BrokerManager::LockActiveItemsForClient(Client* client) const {
int32 cid = client->GetCharacterID();
auto infoOpt = GetSellerInfo(cid);
if (!infoOpt)
return;
const SellerInfo& info = *infoOpt;
{
auto items = GetActiveForSaleItems(cid);
for (auto const& itm : items) {
if (!itm.for_sale) {
client->GetPlayer()->item_list.SetVaultItemLockUniqueID(client, itm.unique_id, false, true);
continue;
}
LogWrite(PLAYER__ERROR, 5, "Broker",
"--LockActiveItemsForClient: %u (selling: %u), allowinv: %u, sellfrominv: %u",
itm.unique_id, itm.for_sale, itm.from_inventory, info.sell_from_inventory
);
if (!info.sell_from_inventory && itm.from_inventory) {
client->GetPlayer()->item_list.SetVaultItemLockUniqueID(client, itm.unique_id, false, true);
continue;
}
client->GetPlayer()->item_list.SetVaultItemLockUniqueID(client, itm.unique_id, true, true);
}
}
}
void WorldDatabase::ClearSellerSession(int32 character_id) {
Query q;
std::ostringstream sql;
sql << "UPDATE broker_sellers SET coin_session = 0"
<< " WHERE character_id = " << character_id;
std::string full = sql.str();
q.AddQueryAsync(character_id, &database, Q_INSERT, full.c_str());
}
void WorldDatabase::AddToSellerSession(int32 character_id, int64 amount) {
Query q;
std::ostringstream sql;
sql << "UPDATE broker_sellers SET coin_session = coin_session + "
<< amount
<< " WHERE character_id = " << character_id;
std::string full = sql.str();
q.AddQueryAsync(character_id, &database, Q_INSERT, full.c_str());
}
int64 WorldDatabase::GetSellerSession(int32 character_id) {
Query query;
MYSQL_RES* result = query.RunQuery2(
Q_SELECT,
"SELECT "
"coin_session "
"FROM broker_sellers "
"WHERE character_id = %d "
"LIMIT 1",
character_id
);
if (result) {
MYSQL_ROW row;
if ((row = mysql_fetch_row(result))) {
return strtoull(row[0], NULL, 0);
}
}
return 0;
}
std::string BrokerManager::GetShopPurchaseMessage(const std::string& buyer_name, const std::string& item_desc, int16 quantity, int64 coin) {
int64 platinum = static_cast<int64>(coin / 1000000);
// 1) Break totalCopper into denominations
int64 rem = coin % 1000000;
int64 gold = static_cast<int64>(rem / 10000);
rem %= 10000;
int64 silver = static_cast<int64>(rem / 100);
int64 copper = static_cast<int64>(rem % 100);
// 1) Timestamp
auto now = std::time(nullptr);
std::tm tm;
localtime_r(&now, &tm);
char timebuf[64];
std::strftime(timebuf, sizeof(timebuf),
"%B %d, %Y, %I:%M:%S %p", &tm);
// 2) Build the sale line
std::ostringstream msg;
msg << timebuf
<< " " << buyer_name << " buys ";
if (quantity > 1)
msg << quantity << " ";
msg << item_desc
<< " for ";
// 3) Denominations
std::vector<std::string> parts;
if (platinum > 0) parts.push_back(std::to_string(platinum) + " Platinum");
if (gold > 0) parts.push_back(std::to_string(gold) + " Gold");
if (silver > 0) parts.push_back(std::to_string(silver) + " Silver");
if (copper > 0) parts.push_back(std::to_string(copper) + " Copper");
// If all are zero (unlikely), still print "0 Copper"
if (parts.empty())
parts.push_back("0 Copper");
// Join with ", "
for (size_t i = 0; i < parts.size(); ++i) {
if (i) msg << ", ";
msg << parts[i];
}
return msg.str();
}
void BrokerManager::LogSale(int32 cid,
const std::string& buyer_name,
const std::string& item_desc,
int16 quantity,
int64 coin)
{
std::string msg = GetShopPurchaseMessage(buyer_name, item_desc, quantity, coin);
std::string esc = EscapeSQLString(msg.c_str());
std::ostringstream sql;
sql << "INSERT INTO broker_seller_log "
"(character_id,log_time,message) VALUES ("
<< cid << ",NOW(),'"
<< esc << "')";
std::string full = sql.str();
Query q;
q.AddQueryAsync(cid, &database, Q_INSERT, full.c_str());
}
void BrokerManager::LogSaleMessage(int32 cid,
const std::string& log_message)
{
auto now = std::time(nullptr);
std::tm tm;
localtime_r(&now, &tm);
char timebuf[64];
std::strftime(timebuf, sizeof(timebuf),
"%B %d, %Y, %I:%M:%S %p", &tm);
std::ostringstream msg;
msg << timebuf
<< " " << log_message;
std::string esc = EscapeSQLString(msg.str());
std::ostringstream sql;
sql << "INSERT INTO broker_seller_log "
"(character_id,log_time,message) VALUES ("
<< cid << ",NOW(),'"
<< esc << "')";
std::string full = sql.str();
Query q;
q.AddQueryAsync(cid, &database, Q_INSERT, full.c_str());
}
std::vector<SellerLog> BrokerManager::GetSellerLog(int32 cid) const {
std::vector<SellerLog> out;
Query query;
MYSQL_RES* result = query.RunQuery2(
Q_SELECT,
"SELECT "
"log_id, "
"DATE_FORMAT(log_time, '%%M %%d, %%Y, %%r') as ts, "
"message "
"FROM broker_seller_log "
"WHERE character_id=%u "
"ORDER BY log_time ASC",
cid
);
if (result) {
MYSQL_ROW row;
while ((row = mysql_fetch_row(result))) {
SellerLog entry;
entry.log_id = row[0] ? atoll(row[0]) : 0;
entry.timestamp = row[1] ? row[1] : "";
entry.message = row[2] ? row[2] : "";
out.push_back(std::move(entry));
}
}
return out;
}