diff --git a/server/WorldStructs.xml b/server/WorldStructs.xml index 5f7bfad..27a8e94 100644 --- a/server/WorldStructs.xml +++ b/server/WorldStructs.xml @@ -10469,15 +10469,21 @@ to zero and treated like placeholders." /> - + - - - + + + - + + + + + + + diff --git a/source/WorldServer/Broker/BrokerManager.cpp b/source/WorldServer/Broker/BrokerManager.cpp new file mode 100644 index 0000000..830ae44 --- /dev/null +++ b/source/WorldServer/Broker/BrokerManager.cpp @@ -0,0 +1,954 @@ +/* + 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 . +*/ + +#include "BrokerManager.h" +#include "../Items/Items.h" +#include "../../common/Log.h" +#include "../WorldDatabase.h" +#include "../World.h" +#include "../Web/PeerManager.h" +#include + +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 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 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 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 BrokerManager::GetActiveForSaleItems(int32 cid) const +{ + std::shared_lock lock(mtx_); + std::vector 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 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 thread‐safe read + std::shared_lock lock(mtx_); + + // Find this character’s 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 it’s 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 BrokerManager::GetInactiveItems(int32 cid) const +{ + std::shared_lock lock(mtx_); + std::vector 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> +BrokerManager::GetUniqueIDsAndCost(int32 cid) const +{ + std::shared_lock lock(mtx_); + std::vector> 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* 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* ret = new vector; + string lower_name = ::ToLower(string(name.c_str())); + std::shared_lock lock(mtx_); + std::vector 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 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 + +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(coin / 1000000); + // 1) Break totalCopper into denominations + int64 rem = coin % 1000000; + int64 gold = static_cast(rem / 10000); + rem %= 10000; + int64 silver = static_cast(rem / 100); + int64 copper = static_cast(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 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 BrokerManager::GetSellerLog(int32 cid) const { + std::vector 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; +} \ No newline at end of file diff --git a/source/WorldServer/Broker/BrokerManager.h b/source/WorldServer/Broker/BrokerManager.h new file mode 100644 index 0000000..d291a1c --- /dev/null +++ b/source/WorldServer/Broker/BrokerManager.h @@ -0,0 +1,182 @@ +/* + 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 . +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "../../common/types.h" + +// Key = (character_id, unique_id) +using Key = std::pair; + +class Item; +class Client; + +struct SaleItem { + int64 unique_id; + int32 character_id; + int32 house_id; + int64 item_id; + int64 cost_copper; + bool for_sale; + sint32 inv_slot_id; + int16 slot_id; + int16 count; + bool from_inventory; + string creator; +}; + +struct SellerInfo { + int32 character_id; + std::string seller_name; + int64 house_id; + bool sale_enabled; + bool sell_from_inventory; + int64 coin_session; + int64 coin_total; +}; + +struct SellerLog { + int64 log_id; // auto_increment PK + std::string timestamp; // e.g. "October 20, 2005, 05:29:33 AM" + std::string message; // the human-readable text +}; + +class BrokerManager { +public: + BrokerManager(); + + //–– Player management + void AddSeller(int32 cid, + const std::string& name, + int32 house_id, + bool sale_enabled, + bool sell_from_inventory); + + void LoadSeller(int32 cid, + const std::string& name, + int32 house_id, + bool sale_enabled, + bool sell_from_inventory, + int64 coin_session, + int64 coin_total); + + int64 ResetSellerSessionCoins(int32 cid); + void AddSellerSessionCoins(int32 cid, uint64 session); + void RemoveSeller(int32 character_id, bool peerCacheOnly = false); + + //–– Item management + void AddItem(const SaleItem& item, bool peerCacheOnly = false); + void LoadItem(const SaleItem& item); + + //–– Activate / deactivate sale flag + void SetSaleStatus(int32 cid, int64 uid, bool for_sale); + bool IsItemListed(int32 cid, int64 uid); + void SetSalePrice(int32 cid, int64 uid, int64 price); + int64 GetSalePrice(int32 cid, int64 uid); + + //–– Remove quantity + void RemoveItem(int32 cid, int64 uid, int16 quantity = 1); + + //–– Attempt to buy (atomic DB + in-memory + broadcast) + bool BuyItem(Client* buyer, int32 seller_cid, int64 uid, int32 quantity); + bool IsItemForSale(int32 seller_cid, int64 uid) const; + + //–– Called when a peer notifies that an item was sold/removed (in-memory only) + void OnPeerRemoveItem(int32 character_id, int64 unique_id); + + //–– Queries + std::vector GetActiveForSaleItems(int32 cid) const; + std::optional GetActiveItem(int32 cid, int64 uid) const; + bool IsSellingItems(int32 cid, bool vaultOnly = false) const; + std::vector GetInactiveItems(int32 cid) const; + //–– Global search API (only active_items_) + vector* 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; + + //–– UI helper: get (unique_id, cost) for active items + std::vector> GetUniqueIDsAndCost(int32 cid) const; + + std::optional GetSellerInfo(int32 character_id) const; + //–– Lookup seller name + std::string GetSellerName(int32 cid) const; + bool IsSaleEnabled(int32 cid) const; + bool CanSellFromInventory(int32 cid) const; + int32 GetHouseID(int32 cid) const; + + bool IsItemFromInventory(int32 cid, int64 uid) const; + void LockActiveItemsForClient(Client* client) const; + + std::string GetShopPurchaseMessage(const std::string& buyer_name, const std::string& item_desc, int16 quantity, int64 coin); + void LogSale(int32 character_id, const std::string& buyer_name,const std::string& item_desc, int16 quantity, int64 coin); + void LogSaleMessage(int32 character_id,const std::string& log_message); + std::vector GetSellerLog(int32 character_id) const; + static std::string EscapeSQLString(const std::string& s) { + std::string out; + out.reserve(s.size() * 2); + for (char c : s) { + if (c == '\'') out += "''"; + else out += c; + } + return out; + } +private: + mutable std::shared_mutex mtx_; + std::unordered_map players_; + std::unordered_map< + int32, + std::unordered_map + > active_items_by_char_; + std::unordered_map< + int32, + std::unordered_map + > inactive_items_by_char_; + + //–– DB sync (async writes) + void SavePlayerToDB(const SellerInfo& p); + void SaveItemToDB(const SaleItem& i); + void UpdateItemInDB(const SaleItem& i); + void DeleteItemFromDB(int32 character_id, int64 unique_id); + void DeleteCharacterItemFromDB(int32 cid, int64 uid); + void UpdateCharacterItemDB(int32 cid, int64 uid, int16 count); + void DeletePlayerFromDB(int32 cid); +}; \ No newline at end of file diff --git a/source/WorldServer/Commands/Commands.cpp b/source/WorldServer/Commands/Commands.cpp index 3cb9d9c..f969dc5 100644 --- a/source/WorldServer/Commands/Commands.cpp +++ b/source/WorldServer/Commands/Commands.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -52,6 +52,7 @@ #include "../Bots/Bot.h" #include "../Web/PeerManager.h" #include "../../common/GlobalHeaders.h" +#include "../Broker/BrokerManager.h" extern WorldDatabase database; extern MasterSpellList master_spell_list; @@ -76,6 +77,7 @@ extern MasterAAList master_aa_list; extern MasterRaceTypeList race_types_list; extern Classes classes; extern PeerManager peer_manager; +extern BrokerManager broker; //devn00b: Fix for linux builds since we dont use stricmp we use strcasecmp #if defined(__GNUC__) @@ -2193,14 +2195,35 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie LogWrite(COMMAND__ERROR, 0, "Command", "/info appearance: Unknown Index: %u", item_index); } else if(strcmp(sep->arg[0], "item") == 0 || strcmp(sep->arg[0], "merchant") == 0 || strcmp(sep->arg[0], "store") == 0 || strcmp(sep->arg[0], "buyback") == 0 || strcmp(sep->arg[0], "consignment") == 0){ - int32 item_id = atoul(sep->arg[1]); - Item* item = master_item_list.GetItem(item_id); - if(item){ + int64 item_id = strtoull(sep->arg[1], NULL, 0); + Item* item = nullptr; + + if (strcmp(sep->arg[0], "store") == 0) + item = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(item_id, true); + else + item = master_item_list.GetItem(item_id); + + if(!item && client->GetMerchantTransactionID() && strcmp(sep->arg[0], "merchant") == 0) { + Spawn* merchant = client->GetPlayer()->GetZone()->GetSpawnByID(client->GetMerchantTransactionID()); + if(merchant && merchant->GetHouseCharacterID() && merchant->GetPickupUniqueItemID()) { + if(auto itemInfo = broker.GetActiveItem(merchant->GetHouseCharacterID(), item_id)) { + item = master_item_list.GetItem(itemInfo->item_id); + if(item) { + EQ2Packet* app = item->serialize(client->GetVersion(), true, client->GetPlayer()); + client->QueuePacket(app); + } + } + } + } + else if(!item && strcmp(sep->arg[0], "consignment") == 0) { + client->SendSellerItemByItemUniqueId(item_id); + } + else if(item){ EQ2Packet* app = item->serialize(client->GetVersion(), true, client->GetPlayer()); client->QueuePacket(app); } else - LogWrite(COMMAND__ERROR, 0, "Command", "/info item|merchant|store|buyback|consignment: Unknown Item ID: %u", item_id); + LogWrite(COMMAND__ERROR, 0, "Command", "/info item|merchant|store|buyback|consignment: Unknown Item ID: %u (full arguments %s)", item_id, sep->argplus[0]); } else if (strcmp(sep->arg[0], "spell") == 0) { sint32 spell_id = atol(sep->arg[1]); @@ -3900,8 +3923,12 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie if (!spawn || !client->HasOwnerOrEditAccess() || !spawn->GetPickupItemID()) break; - - if(client->AddItem(spawn->GetPickupItemID(), 1)) { + Item* tmpItem = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(spawn->GetPickupUniqueItemID()); + if((tmpItem && tmpItem->generic_info.item_type == ITEM_TYPE_HOUSE_CONTAINER) || client->AddItem(spawn->GetPickupItemID(), 1)) { + if ( tmpItem && tmpItem->generic_info.item_type == ITEM_TYPE_HOUSE_CONTAINER ) { + tmpItem->TryUnlockItem(LockReason::LockReason_House); + client->QueuePacket(client->GetPlayer()->SendInventoryUpdate(client->GetVersion())); + } Query query; query.RunQuery2(Q_INSERT, "delete from spawn_instance_data where spawn_id = %u and spawn_location_id = %u and pickup_item_id = %u", spawn->GetDatabaseID(), spawn->GetSpawnLocationID(), spawn->GetPickupItemID()); @@ -4030,12 +4057,16 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie case COMMAND_PLACE_HOUSE_ITEM: { if (sep && sep->IsNumber(0)) { - int32 uniqueid = atoi(sep->arg[0]); + int64 uniqueid = strtoull(sep->arg[0], NULL, 0); - Item* item = client->GetPlayer()->item_list.GetItemFromUniqueID(uniqueid); - //Item* item = player->GetEquipmentList()->GetItem(slot); + Item* item = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(uniqueid); + if (item && (item->IsHouseItem() || item->IsHouseContainer())) { + if(item->IsHouseContainer() && item->details.inv_slot_id != InventorySlotType::HOUSE_VAULT) { // must be in base slot of vault in house for house containers + client->SimpleMessage(CHANNEL_COLOR_RED, "Must be in vault to place this item."); + break; + } if (!client->HasOwnerOrEditAccess()) { client->SimpleMessage(CHANNEL_COLOR_RED, "This is not your home!"); @@ -4062,7 +4093,7 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie Object* obj = new Object(); Spawn* spawn = (Spawn*)obj; memset(&spawn->appearance, 0, sizeof(spawn->appearance)); - strcpy(spawn->appearance.name, "temp"); + strcpy(spawn->appearance.name, item->name.c_str()); spawn->SetX(client->GetPlayer()->GetX()); spawn->SetY(client->GetPlayer()->GetY()); spawn->SetZ(client->GetPlayer()->GetZ()); @@ -4403,20 +4434,49 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie } case COMMAND_BUY_FROM_BROKER:{ if(sep && sep->arg[1][0] && sep->IsNumber(0) && sep->IsNumber(1)){ - int32 item_id = atoul(sep->arg[0]); + int64 item_id = strtoull(sep->arg[0], NULL, 0); int16 quantity = atoul(sep->arg[1]); - Item* item = master_item_list.GetItem(item_id); - if(item && item->generic_info.max_charges > 1) - quantity = item->generic_info.max_charges; - client->AddItem(item_id, quantity, AddItemType::BUY_FROM_BROKER); + if(client->IsGMStoreSearch()) { + Item* item = master_item_list.GetItem(item_id); + if(item && item->generic_info.max_charges > 1) + quantity = item->generic_info.max_charges; + client->AddItem(item_id, quantity, AddItemType::BUY_FROM_BROKER); + } + else { + client->BuySellerItemByItemUniqueId(item_id, quantity); + LogWrite(COMMAND__ERROR, 0, "Command", "BUY_FROM_BROKER. Item ID %u, Quantity %u, full args %s.", item_id, quantity, sep->argplus[0]); + } } break; } case COMMAND_SEARCH_STORES_PAGE:{ + LogWrite(COMMAND__ERROR, 0, "Command", "SearchStores: %s", sep && sep->arg[0] ? sep->argplus[0] : ""); if(sep && sep->arg[0][0] && sep->IsNumber(0)){ int32 page = atoul(sep->arg[0]); client->SearchStore(page); } + else { + client->SetGMStoreSearch(false); + PacketStruct* packet = configReader.getStruct("WS_StartBroker", client->GetVersion()); + if (packet) { + packet->setDataByName("spawn_id", client->GetPlayer()->GetIDWithPlayerSpawn(client->GetPlayer())); + //packet->setDataByName("unknown", 1); + packet->setDataByName("unknown2", 5, 0); + packet->setDataByName("unknown2", 20, 1); + packet->setDataByName("unknown2", 58, 3); + packet->setDataByName("unknown2", 40, 4); + client->QueuePacket(packet->serialize()); + if(client->GetVersion() > 561) { + PacketStruct* packet2 = configReader.getStruct("WS_BrokerBags", client->GetVersion()); + if (packet2) { + packet2->setDataByName("char_id", client->GetCharacterID()); + client->QueuePacket(packet2->serialize()); //send this for now, needed to properly clear data + safe_delete(packet2); + } + safe_delete(packet); + } + } + } break; } case COMMAND_SEARCH_STORES:{ @@ -4424,11 +4484,11 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie const char* values = sep->argplus[0]; if(values){ LogWrite(ITEM__WARNING, 0, "Item", "SearchStores: %s", values); - map str_values = TranslateBrokerRequest(values); vector* items = master_item_list.GetItems(str_values, client); if(items){ - client->SetItemSearch(items); + client->SetItemSearch(items, str_values); + client->SetSearchPage(0); client->SearchStore(0); } } @@ -5424,6 +5484,13 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie } case COMMAND_ITEMSEARCH: case COMMAND_FROMBROKER:{ + + if(command->handler == COMMAND_ITEMSEARCH) { + client->SetGMStoreSearch(true); + } + else { + client->SetGMStoreSearch(false); + } PacketStruct* packet = configReader.getStruct("WS_StartBroker", client->GetVersion()); if (packet) { packet->setDataByName("spawn_id", client->GetPlayer()->GetIDWithPlayerSpawn(client->GetPlayer())); @@ -5433,13 +5500,15 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie packet->setDataByName("unknown2", 58, 3); packet->setDataByName("unknown2", 40, 4); client->QueuePacket(packet->serialize()); - PacketStruct* packet2 = configReader.getStruct("WS_BrokerBags", client->GetVersion()); - if (packet2) { - packet2->setDataByName("char_id", client->GetCharacterID()); - client->QueuePacket(packet2->serialize()); //send this for now, needed to properly clear data - safe_delete(packet2); + if(client->GetVersion() > 561) { + PacketStruct* packet2 = configReader.getStruct("WS_BrokerBags", client->GetVersion()); + if (packet2) { + packet2->setDataByName("char_id", client->GetCharacterID()); + client->QueuePacket(packet2->serialize()); //send this for now, needed to properly clear data + safe_delete(packet2); + } + safe_delete(packet); } - safe_delete(packet); } break; } @@ -5838,6 +5907,15 @@ void Commands::Process(int32 index, EQ2_16BitString* command_parms, Client* clie case COMMAND_SPLIT: { Command_Split(client, sep); break; } case COMMAND_RAIDSAY: { Command_RaidSay(client, sep); break; } case COMMAND_RELOAD_ZONEINFO: { Command_ReloadZoneInfo(client, sep); break; } + case COMMAND_SLE: { Command_SetLocationEntry(client, sep); break; } + case COMMAND_STORE_LIST_ITEM: { Command_StoreListItem(client, sep); break; } + case COMMAND_STORE_SET_PRICE: { Command_StoreSetPrice(client, sep); break; } + case COMMAND_STORE_SET_PRICE_LOCAL: { Command_StoreSetPriceLocal(client, sep); break; } + case COMMAND_STORE_START_SELLING: { Command_StoreStartSelling(client, sep); break; } + case COMMAND_STORE_STOP_SELLING: { Command_StoreStopSelling(client, sep); break; } + case COMMAND_STORE_UNLIST_ITEM: { Command_StoreUnlistItem(client, sep); break; } + case COMMAND_CLOSE_STORE_KEEP_SELLING: { Command_CloseStoreKeepSelling(client, sep); break; } + case COMMAND_CANCEL_STORE: { Command_CancelStore(client, sep); break; } default: { LogWrite(COMMAND__WARNING, 0, "Command", "Unhandled command: %s", command->command.data.c_str()); @@ -7024,7 +7102,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma if(item) { - if(item->details.item_locked) { + if(item->IsItemLocked()) { client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot destroy the item in use."); return; } @@ -7036,11 +7114,15 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma client->SimpleMessage(CHANNEL_COLOR_RED, "You can't destroy this item."); return; } + if(client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT) || client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::BASE_INVENTORY)) { + broker.RemoveItem(client->GetPlayer()->GetCharacterID(), item->details.unique_id, item->details.count); + } if(item->GetItemScript() && lua_interface) lua_interface->RunItemScript(item->GetItemScript(), "destroyed", item, client->GetPlayer()); //reobtain item make sure it wasn't removed item = player->item_list.GetItemFromIndex(index); + int32 bag_id = 0; if(item){ bag_id = item->details.inv_slot_id; @@ -7049,6 +7131,8 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma client->GetPlayer()->item_list.DestroyItem(index); client->GetPlayer()->UpdateInventory(bag_id); client->GetPlayer()->CalculateApplyWeight(); + + client->OpenShopWindow(nullptr); // update the window if it is open } } else if(sep->arg[4][0] && strncasecmp("move", sep->arg[0], 4) == 0 && sep->IsNumber(1) && sep->IsNumber(2) && sep->IsNumber(3) && sep->IsNumber(4)) @@ -7058,33 +7142,34 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma sint32 bag_id = atol(sep->arg[3]); int8 charges = atoi(sep->arg[4]); Item* item = client->GetPlayer()->item_list.GetItemFromIndex(from_index); - + int64 unique_id = 0; + int16 count = 0; if(!item) { client->SimpleMessage(CHANNEL_COLOR_RED, "You have no item."); return; } - - if(to_slot == item->details.slot_id && (bag_id < 0 || bag_id == item->details.inv_slot_id)) { + unique_id = item->details.unique_id; + count = item->details.count; + if(to_slot == item->details.slot_id && (bag_id == item->details.inv_slot_id)) { return; } - if(item->details.item_locked) + if(item->IsItemLocked()) { client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot move the item in use."); return; } - if(bag_id == -4 && !client->GetPlayer()->item_list.SharedBankAddAllowed(item)) + if(bag_id == InventorySlotType::SHARED_BANK && !client->GetPlayer()->item_list.SharedBankAddAllowed(item)) { client->SimpleMessage(CHANNEL_COLOR_RED, "That item (or an item inside) cannot be shared."); return; } - sint32 old_inventory_id = 0; if(item) old_inventory_id = item->details.inv_slot_id; //autobank - if (bag_id == -3 && to_slot == -1) + if (bag_id == InventorySlotType::BANK && to_slot == -1) { if (player->HasFreeBankSlot()) to_slot = player->FindFreeBankSlot(); @@ -7129,6 +7214,16 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma } client->GetPlayer()->CalculateApplyWeight(); + if(item) { + if(!client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT) && + !client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::BASE_INVENTORY)) { + broker.RemoveItem(client->GetPlayer()->GetCharacterID(), unique_id, charges); + } + } + else { + broker.RemoveItem(client->GetPlayer()->GetCharacterID(), unique_id, count); + } + client->OpenShopWindow(nullptr); // update the window if it is open } else if(sep->arg[1][0] && strncasecmp("equip", sep->arg[0], 5) == 0 && sep->IsNumber(1)) { @@ -7159,6 +7254,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma client->QueuePacket(characterSheetPackets); client->GetPlayer()->CalculateBonuses(); + client->OpenShopWindow(nullptr); // update the window if it is open } else if (sep->arg[1][0] && strncasecmp("unpack", sep->arg[0], 6) == 0 && sep->IsNumber(1)) { @@ -7168,7 +7264,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma int16 index = atoi(sep->arg[1]); Item* item = client->GetPlayer()->item_list.GetItemFromIndex(index); if (item) { - if(item->details.item_locked) + if(item->IsItemLocked()) { client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot unpack the item in use."); return; @@ -7186,6 +7282,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma } client->RemoveItem(item, 1); + client->OpenShopWindow(nullptr); // update the window if it is open } } @@ -7224,6 +7321,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma client->UnequipItem(index, bag_id, to_slot, appearance_equip); client->GetPlayer()->CalculateBonuses(); + client->OpenShopWindow(nullptr); // update the window if it is open } else if(sep->arg[2][0] && strncasecmp("swap_equip", sep->arg[0], 10) == 0 && sep->IsNumber(1) && sep->IsNumber(2)) { @@ -7276,9 +7374,10 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma // Send the inventory update packet client->QueuePacket(player->item_list.serialize(player, client->GetVersion())); + client->OpenShopWindow(nullptr); // update the window if it is open return; } - else if (bag_id == -3 && to_slot == -1) { + else if (bag_id == InventorySlotType::BANK && to_slot == -1) { // Auto Bank if (!player->item_list.GetFirstFreeBankSlot(&bag_id, &to_slot)) { client->SimpleMessage(CHANNEL_STATUS, "You do not have any free bank slots."); @@ -7291,14 +7390,15 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma player->item_list.RemoveOverflowItem(item); } client->QueuePacket(player->item_list.serialize(player, client->GetVersion())); + client->OpenShopWindow(nullptr); // update the window if it is open } - else if (bag_id == -4) { + else if (bag_id == InventorySlotType::SHARED_BANK) { // Shared Bank if (!player->item_list.SharedBankAddAllowed(item)) { client->SimpleMessage(CHANNEL_STATUS, "That item (or an item inside) cannot be shared."); return; } - Item* tmp_item = player->item_list.GetItem(-4, to_slot); + Item* tmp_item = player->item_list.GetItem(bag_id, to_slot); if (tmp_item) { client->SimpleMessage(CHANNEL_STATUS, "You can not place an overflow item into an occupied slot"); return; @@ -7311,6 +7411,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma player->item_list.RemoveOverflowItem(item); } client->QueuePacket(player->item_list.serialize(player, client->GetVersion())); + client->OpenShopWindow(nullptr); // update the window if it is open return; } } @@ -7330,6 +7431,7 @@ void Commands::Command_Inventory(Client* client, Seperator* sep, EQ2_RemoteComma player->item_list.RemoveOverflowItem(item); } client->QueuePacket(player->item_list.serialize(player, client->GetVersion())); + client->OpenShopWindow(nullptr); // update the window if it is open return; } } @@ -9976,14 +10078,18 @@ void Commands::Command_TradeAddItem(Client* client, Seperator* sep) int32 index = atoi(sep->arg[0]); item = client->GetPlayer()->GetPlayerItemList()->GetItemFromIndex(index); if (item) { - if(item->details.item_locked || item->details.equip_slot_id) { + if(item->IsItemLocked() || item->details.equip_slot_id) { client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot trade an item currently in use."); return; } - else if(item->details.inv_slot_id == -3 || item->details.inv_slot_id == -4) { + else if(item->details.inv_slot_id == InventorySlotType::BANK || item->details.inv_slot_id == InventorySlotType::SHARED_BANK) { client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot trade an item in the bank."); return; } + else if(client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT)) { + client->SimpleMessage(CHANNEL_COLOR_RED, "You cannot trade an item in the house vault."); + return; + } int8 result = client->GetPlayer()->trade->AddItemToTrade(client->GetPlayer(), item, atoi(sep->arg[2]), atoi(sep->arg[1])); if (result == 1) @@ -11041,6 +11147,23 @@ void Commands::Command_Test(Client* client, EQ2_16BitString* command_parms) { else if(atoi(sep->arg[0]) == 38) { client->GetPlayer()->GetZone()->SendFlightPathsPackets(client); } + else if(atoi(sep->arg[0]) == 39) { + client->OpenShopWindow(nullptr, true); + } + else if(atoi(sep->arg[0]) == 40) { + + PacketStruct* packet2 = configReader.getStruct("WS_HouseStoreLog", client->GetVersion()); + if (packet2) { + packet2->setDataByName("data", sep->arg[1]); + packet2->setDataByName("coin_gain_session", atoul(sep->arg[2])); + packet2->setDataByName("coin_gain_alltime", atoul(sep->arg[3])); + packet2->setDataByName("sales_log_open", atoi(sep->arg[4])); + EQ2Packet* outapp = packet2->serialize(); + DumpPacket(outapp); + client->QueuePacket(outapp); + safe_delete(packet2); + } + } } else { PacketStruct* packet2 = configReader.getStruct("WS_ExaminePartialSpellInfo", client->GetVersion()); @@ -11503,37 +11626,8 @@ void Commands::Command_Wind(Client* client, Seperator* sep) { void Commands::Command_SendMerchantWindow(Client* client, Seperator* sep, bool sell) { Spawn* spawn = client->GetPlayer()->GetTarget(); - if(client->GetVersion() < 561) { - sell = false; // doesn't support in the same way as AoM just open the normal buy/sell window - } - if(spawn) { - client->SetMerchantTransaction(spawn); - if (spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(client)){ - client->SendHailCommand(spawn); - //MerchantFactionMultiplier* multiplier = world.GetMerchantMultiplier(spawn->GetMerchantID()); - //if(!multiplier || (multiplier && client->GetPlayer()->GetFactions()->GetFactionValue(multiplier->faction_id) >= multiplier->faction_min)){ - client->SendBuyMerchantList(sell); - if(!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY)) - client->SendSellMerchantList(sell); - if(!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK)) - client->SendBuyBackList(sell); - - if(client->GetVersion() > 561) { - PacketStruct* packet = configReader.getStruct("WS_UpdateMerchant", client->GetVersion()); - if (packet) { - packet->setDataByName("spawn_id", 0xFFFFFFFF); - packet->setDataByName("type", 16); - EQ2Packet* outapp = packet->serialize(); - if (outapp) - client->QueuePacket(outapp); - safe_delete(packet); - } - } - } - if (spawn->GetMerchantType() & MERCHANT_TYPE_REPAIR) - client->SendRepairList(); - } - // client->SimpleMessage(CHANNEL_COLOR_RED, "Your faction is too low to use this merchant."); + if(spawn) + client->SendMerchantWindow(spawn, sell); } @@ -12946,4 +13040,301 @@ void Commands::Command_RaidSay(Client* client, Seperator* sep) { */ void Commands::Command_ReloadZoneInfo(Client* client, Seperator* sep) { world.ClearZoneInfoCache(); +} + +// somewhere in your command‐handler: +void Commands::Command_SetLocationEntry(Client* client, Seperator* sep) { + if(client->GetCurrentZone()->GetInstanceType() == PERSONAL_HOUSE_INSTANCE) { + client->Message(CHANNEL_COLOR_YELLOW, "Use in a non house zone."); + return; + } + + if (!sep->IsSet(1) || (sep->IsNumber(0))) { + client->Message(CHANNEL_COLOR_YELLOW, "Usage: /sle "); + return; + } + + Spawn* target = client->GetPlayer()->GetTarget(); + + if(!target) { + client->Message(CHANNEL_COLOR_RED, "Target missing for set location entry, /sle "); + return; + } + + // GetSpawnEntryID for spawn_location_entry to set the spawnpercentage + int32 spawnEntryID = target->GetSpawnEntryID(); + int32 spawnPlacementID = target->GetSpawnLocationPlacementID(); + int32 spawnLocationID = target->GetSpawnLocationID(); + int32 dbID = target->GetDatabaseID(); + if (spawnPlacementID == 0 || spawnLocationID == 0 || spawnEntryID == 0 || dbID == 0) { + client->Message(CHANNEL_COLOR_RED, "Error: no valid spawn entry selected."); + return; + } + + // 2) Whitelist the allowed columns + static const std::unordered_set allowed = { + "x", + "y", + "z", + "x_offset", + "y_offset", + "z_offset", + "heading", + "pitch", + "roll", + "respawn", + "respawn_offset_low", + "respawn_offset_high", + "duplicated_spawn", + "expire_timer", + "expire_offset", + "grid_id", + "processed", + "instance_id", + "lvl_override", + "hp_override", + "mp_override", + "str_override", + "sta_override", + "wis_override", + "int_override", + "agi_override", + "heat_override", + "cold_override", + "magic_override", + "mental_override", + "divine_override", + "disease_override", + "poison_override", + "difficulty_override", + "spawnpercentage", + "condition" + }; + + + const std::string& field = std::string(sep->arg[0]); + if (!allowed.count(field)) { + client->Message(CHANNEL_COLOR_RED, "Error: column '%s' is not modifiable.", field.c_str()); + return; + } + + const std::string& val = std::string(sep->arg[1]); + + Query query; + if(field == "spawnpercentage" || field == "condition") { + query.AddQueryAsync(0, + &database, + Q_UPDATE, + // we embed the whitelisted field name directly in the format + "UPDATE spawn_location_entry " + "SET %s=%s " + "WHERE id=%u and spawn_location_id=%u and spawn_id=%u ", + field.c_str(), + val.c_str(), + spawnEntryID, + spawnLocationID, + dbID + ); + } + else { + query.AddQueryAsync(0, + &database, + Q_UPDATE, + // we embed the whitelisted field name directly in the format + "UPDATE spawn_location_placement " + "SET %s=%s " + "WHERE id=%u and spawn_location_id=%u ", + field.c_str(), + val.c_str(), + spawnPlacementID, + spawnLocationID + ); + } + + client->Message(CHANNEL_COLOR_YELLOW, "Modified %s to %s for row entry id %u, spawn placement id %u, related to location id %u and spawn database id %u.", + field.c_str(), val.c_str(), spawnEntryID, spawnPlacementID, spawnLocationID, dbID); +} + + + +void Commands::Command_StoreListItem(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + + if(sep && sep->arg[0]) { + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else if(sep->IsNumber(0)) { + int64 unique_id = atoll(sep->arg[0]); + if(client->GetPlayer()->item_list.CanStoreSellItem(unique_id, true)) { + Item* item = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(unique_id, true); + if(item) { + bool isInv = !client->GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT); + int64 cost = broker.GetSalePrice(client->GetPlayer()->GetCharacterID(), item->details.unique_id); + client->AddItemSale(item->details.unique_id, item->details.item_id, cost, item->details.inv_slot_id, item->details.slot_id, item->details.count, isInv, true, item->creator); + } + else + client->Message(CHANNEL_COLOR_RED, "Broker issue, cannot find item %u.", unique_id); + + client->SetItemSaleStatus(unique_id, true); + client->GetPlayer()->item_list.SetVaultItemLockUniqueID(client, unique_id, true, false); + client->SetSellerStatus(); + } + } + else { + client->Message(CHANNEL_COLOR_RED, "Invalid arguments for /store_list_item unique_id."); + + } + } +} +void Commands::Command_StoreSetPrice(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + + if(sep && sep->arg[0]) { + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + int64 unique_id = atoll(sep->arg[0]); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else if(info->sell_from_inventory && broker.IsItemFromInventory(client->GetPlayer()->GetCharacterID(), unique_id)) { + client->Message(CHANNEL_COLOR_RED, "You cannot change the price while selling."); + } + else if(info->sell_from_inventory && !broker.IsItemFromInventory(client->GetPlayer()->GetCharacterID(), unique_id) && broker.IsItemForSale(client->GetPlayer()->GetCharacterID(), unique_id)) { + client->Message(CHANNEL_COLOR_RED, "You cannot change the price while selling."); + } + else if(sep->IsNumber(1) && sep->IsNumber(2) && sep->IsNumber(3) && sep->IsNumber(4)) { + int32 plat = atoul(sep->arg[1]); + int32 gold = atoul(sep->arg[2]); + int32 silver = atoul(sep->arg[3]); + int32 copper = atoul(sep->arg[4]); + int64 price = plat * 1000000 + gold * 10000 + silver * 100 + copper; + + LogWrite(PLAYER__INFO, 5, "Broker", + "--StoreSetPrice: %u (%u), cost=%u", + client->GetPlayer()->GetCharacterID(), unique_id, price + ); + client->SetItemSaleCost(unique_id, plat, gold, silver, copper); + } + else { + client->Message(CHANNEL_COLOR_RED, "Invalid arguments for /store_set_price unique_id platinum gold silver copper."); + } + } +} +void Commands::Command_StoreSetPriceLocal(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + + if(sep && sep->arg[0]) { + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else if(info->sell_from_inventory) { + client->Message(CHANNEL_COLOR_RED, "You cannot change the price while selling."); + } + else if(sep->IsNumber(0) && sep->IsNumber(1) && sep->IsNumber(2) && sep->IsNumber(3) && sep->IsNumber(4)) { + int64 unique_id = atoll(sep->arg[0]); + int32 plat = atoul(sep->arg[1]); + int32 gold = atoul(sep->arg[2]); + int32 silver = atoul(sep->arg[3]); + int32 copper = atoul(sep->arg[4]); + int64 price = plat * 1000000 + gold * 10000 + silver * 100 + copper; + LogWrite(PLAYER__INFO, 5, "Broker", + "--StoreSetLocalPrice: %u (%u), cost=%u", + client->GetPlayer()->GetCharacterID(), unique_id, price + ); + client->SetItemSaleCost(unique_id, plat, gold, silver, copper); + } + else { + client->Message(CHANNEL_COLOR_RED, "Invalid arguments for /store_set_price_local unique_id platinum gold silver copper."); + } + } + +} +void Commands::Command_StoreStartSelling(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, true); + client->OpenShopWindow(nullptr); + broker.LockActiveItemsForClient(client); +} + +void Commands::Command_StoreStopSelling(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, false); + client->OpenShopWindow(nullptr); + broker.LockActiveItemsForClient(client); +} + +void Commands::Command_StoreUnlistItem(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + if(sep && sep->arg[0]) { + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else if(sep->IsNumber(0)) { + int64 unique_id = atoll(sep->arg[0]); + client->SetItemSaleStatus(unique_id, false); + client->SetSellerStatus(); + client->GetPlayer()->item_list.SetVaultItemLockUniqueID(client, unique_id, false, false); + } + else { + client->Message(CHANNEL_COLOR_RED, "Invalid arguments for /store_unlist_item unique_id."); + + } + } +} + +void Commands::Command_CloseStoreKeepSelling(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + client->SetShopWindowStatus(false); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else { + bool itemsSelling = broker.IsSellingItems(client->GetPlayer()->GetCharacterID()); + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), itemsSelling, true); + } + broker.LockActiveItemsForClient(client); +} + +void Commands::Command_CancelStore(Client* client, Seperator* sep) { + if(!client->GetShopWindowStatus()) { + client->Message(CHANNEL_COLOR_RED, "Shop not available."); + return; + } + auto info = broker.GetSellerInfo(client->GetPlayer()->GetCharacterID()); + client->SetShopWindowStatus(false); + if(!info) { + client->Message(CHANNEL_COLOR_RED, "Player %u is not in the broker database.", client->GetPlayer()->GetCharacterID()); + } + else { + bool itemsSelling = broker.IsSellingItems(client->GetPlayer()->GetCharacterID(), true); + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), itemsSelling, false); + } + broker.LockActiveItemsForClient(client); } \ No newline at end of file diff --git a/source/WorldServer/Commands/Commands.h b/source/WorldServer/Commands/Commands.h index 3630616..05bcec0 100644 --- a/source/WorldServer/Commands/Commands.h +++ b/source/WorldServer/Commands/Commands.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -467,7 +467,16 @@ public: void Command_RaidSay(Client* client, Seperator* sep); void Command_ReloadZoneInfo(Client* client, Seperator* sep); - + void Command_SetLocationEntry(Client* client, Seperator* sep); + void Command_StoreListItem(Client* client, Seperator* sep); + void Command_StoreSetPrice(Client* client, Seperator* sep); + void Command_StoreSetPriceLocal(Client* client, Seperator* sep); + void Command_StoreStartSelling(Client* client, Seperator* sep); + void Command_StoreStopSelling(Client* client, Seperator* sep); + void Command_StoreUnlistItem(Client* client, Seperator* sep); + void Command_CloseStoreKeepSelling(Client* client, Seperator* sep); + void Command_CancelStore(Client* client, Seperator* sep); + // AA Commands void Get_AA_Xml(Client* client, Seperator* sep); void Add_AA(Client* client, Seperator* sep); @@ -984,7 +993,16 @@ private: #define COMMAND_MOOD 800 #define COMMAND_RELOAD_PLAYERSCRIPTS 801 - +#define COMMAND_SLE 802 +#define COMMAND_STORE_LIST_ITEM 803 +#define COMMAND_STORE_SET_PRICE 804 +#define COMMAND_STORE_SET_PRICE_LOCAL 805 +#define COMMAND_STORE_START_SELLING 806 +#define COMMAND_STORE_STOP_SELLING 807 +#define COMMAND_STORE_UNLIST_ITEM 808 +#define COMMAND_CLOSE_STORE_KEEP_SELLING 809 +#define COMMAND_CANCEL_STORE 810 + #define COMMAND_MODIFY 1000 // INSERT INTO `commands`(`id`,`type`,`command`,`subcommand`,`handler`,`required_status`) VALUES ( NULL,'1','modify','','1000','200'); #define COMMAND_MODIFY_CHARACTER 1001 #define COMMAND_MODIFY_FACTION 1002 diff --git a/source/WorldServer/Commands/CommandsDB.cpp b/source/WorldServer/Commands/CommandsDB.cpp index a5c8d01..f5ca257 100644 --- a/source/WorldServer/Commands/CommandsDB.cpp +++ b/source/WorldServer/Commands/CommandsDB.cpp @@ -1,6 +1,6 @@ -/* +/* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifdef WIN32 #include #include @@ -118,6 +119,9 @@ bool WorldDatabase::RemoveSpawnTemplate(int32 template_id) int32 WorldDatabase::CreateSpawnFromTemplateByID(Client* client, int32 template_id) { + if(client && client->GetCurrentZone() && client->GetCurrentZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE) { + return 0; + } Query query, query2, query3, query4, query5, query6; MYSQL_ROW row; int32 spawn_location_id = 0; diff --git a/source/WorldServer/Entity.cpp b/source/WorldServer/Entity.cpp index 536d946..1d247db 100644 --- a/source/WorldServer/Entity.cpp +++ b/source/WorldServer/Entity.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "Entity.h" #include #include "Items/Items.h" @@ -149,8 +150,8 @@ void Entity::DeleteSpellEffects(bool removeClient) GetInfoStruct()->spell_effects[i].spell = nullptr; } } - MMaintainedSpells.releasewritelock(__FUNCTION__, __LINE__); MSpellEffects.releasewritelock(__FUNCTION__, __LINE__); + MMaintainedSpells.releasewritelock(__FUNCTION__, __LINE__); map::iterator deletedPtrItrs; for(deletedPtrItrs = deletedPtrs.begin(); deletedPtrItrs != deletedPtrs.end(); deletedPtrItrs++) { @@ -1091,6 +1092,11 @@ void Entity::DoRegenUpdate(){ void Entity::AddMaintainedSpell(LuaSpell* luaspell){ if (!luaspell) return; + + if(luaspell->spell->GetSpellData()->not_maintained || luaspell->spell->GetSpellData()->duration1 == 0) { + LogWrite(NPC__SPELLS, 5, "NPC", "AddMaintainedSpell Spell ID: %u, Concentration: %u disallowed, not_maintained true (%u) or duration is 0 (%u).", luaspell->spell->GetSpellData()->id, luaspell->spell->GetSpellData()->req_concentration, luaspell->spell->GetSpellData()->not_maintained, luaspell->spell->GetSpellData()->duration1); + return; + } Spell* spell = luaspell->spell; MaintainedEffects* effect = GetFreeMaintainedSpellSlot(); @@ -1290,7 +1296,7 @@ SpellEffects* Entity::GetSpellEffectBySpellType(int8 spell_type) { return ret; } -SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer, sint32 type_group_spell_id, Entity* caster) { +SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer, sint32 type_group_spell_id, Entity* caster, bool notCaster) { SpellEffects* ret = 0; InfoStruct* info = GetInfoStruct(); MSpellEffects.readlock(__FUNCTION__, __LINE__); @@ -1301,7 +1307,7 @@ SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer (linked_timer > 0 && info->spell_effects[i].spell->spell->GetSpellData()->linked_timer == linked_timer) || (type_group_spell_id > 0 && info->spell_effects[i].spell->spell->GetSpellData()->type_group_spell_id == type_group_spell_id)) { - if (type_group_spell_id >= -1 && (!caster || info->spell_effects[i].caster == caster)){ + if (type_group_spell_id >= -1 && (!caster || (!notCaster && info->spell_effects[i].caster == caster) || (notCaster && info->spell_effects[i].caster != caster))){ ret = &info->spell_effects[i]; break; } @@ -1312,12 +1318,14 @@ SpellEffects* Entity::GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer return ret; } -LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWithOtherPlayers) { +LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWithOtherPlayers, bool checkNotCaster) { if(!spell->spell->GetSpellData()->linked_timer && !spell->spell->GetSpellData()->type_group_spell_id) return nullptr; LuaSpell* ret = nullptr; InfoStruct* info = GetInfoStruct(); - MSpellEffects.readlock(__FUNCTION__, __LINE__); + std::vector targets = spell->GetTargets(); + + MMaintainedSpells.readlock(__FUNCTION__, __LINE__); //this for loop primarily handles self checks and 'friendly' checks for(int i = 0; i < NUM_MAINTAINED_EFFECTS; i++) { if(info->maintained_effects[i].spell_id != 0xFFFFFFFF) @@ -1327,21 +1335,34 @@ LuaSpell* Entity::HasLinkedTimerID(LuaSpell* spell, Spawn* target, bool stackWit (spell->spell->GetSpellData()->type_group_spell_id > 0 && spell->spell->GetSpellData()->type_group_spell_id == info->maintained_effects[i].spell->spell->GetSpellData()->type_group_spell_id)) && ((spell->spell->GetSpellData()->friendly_spell) || (!spell->spell->GetSpellData()->friendly_spell && spell->spell->GetSpellData()->type_group_spell_id >= -1 && spell->caster == info->maintained_effects[i].spell->caster) ) && - (target == nullptr || info->maintained_effects[i].spell->initial_target == target->GetID())) { + (target == nullptr || info->maintained_effects[i].spell->HasTarget(target->GetID()) || info->maintained_effects[i].spell->HasAnyTarget(targets))) { ret = info->maintained_effects[i].spell; break; } } } - MSpellEffects.releasereadlock(__FUNCTION__, __LINE__); - - if(!ret && !stackWithOtherPlayers && target && target->IsEntity()) - { - SpellEffects* effect = ((Entity*)target)->GetSpellEffectWithLinkedTimer(spell->spell->GetSpellID(), spell->spell->GetSpellData()->linked_timer, spell->spell->GetSpellData()->type_group_spell_id, nullptr); - if(effect) - ret = effect->spell; + MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__); + + if(checkNotCaster && ret && ret->caster != spell->caster) + return ret; + else if(checkNotCaster && ret) + ret = nullptr; + + for (int32 id : spell->GetTargets()) { + Spawn* tmpTarget = spell->zone->GetSpawnByID(id); + if(!ret && !stackWithOtherPlayers && tmpTarget && tmpTarget->IsEntity()) + { + SpellEffects* effect = ((Entity*)tmpTarget)->GetSpellEffectWithLinkedTimer(spell->spell->GetSpellID(), spell->spell->GetSpellData()->linked_timer, spell->spell->GetSpellData()->type_group_spell_id, checkNotCaster ? spell->caster : nullptr, checkNotCaster); + if(effect) { + ret = effect->spell; + break; + } + } } + if(checkNotCaster && ret && ret->caster == spell->caster) + ret = nullptr; + return ret; } diff --git a/source/WorldServer/Entity.h b/source/WorldServer/Entity.h index 9d1a434..40348a0 100644 --- a/source/WorldServer/Entity.h +++ b/source/WorldServer/Entity.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -1496,8 +1496,8 @@ public: SpellEffects* GetFreeSpellEffectSlot(); SpellEffects* GetSpellEffect(int32 id, Entity* caster = 0, bool on_char_load = false); SpellEffects* GetSpellEffectBySpellType(int8 spell_type); - SpellEffects* GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer = 0, sint32 type_group_spell_id = 0, Entity* caster = 0); - LuaSpell* HasLinkedTimerID(LuaSpell* spell, Spawn* target = nullptr, bool stackWithOtherPlayers = true); + SpellEffects* GetSpellEffectWithLinkedTimer(int32 id, int32 linked_timer = 0, sint32 type_group_spell_id = 0, Entity* caster = 0, bool notCaster = false); + LuaSpell* HasLinkedTimerID(LuaSpell* spell, Spawn* target = nullptr, bool stackWithOtherPlayers = true, bool checkNotCaster = false); //flags int32 GetFlags() { return info_struct.get_flags(); } diff --git a/source/WorldServer/Items/Items.cpp b/source/WorldServer/Items/Items.cpp index 54ad29d..6b6f5bc 100644 --- a/source/WorldServer/Items/Items.cpp +++ b/source/WorldServer/Items/Items.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "Items.h" #include "../Spells.h" #include "../Quests.h" @@ -32,6 +33,8 @@ #include #include #include "../Rules/Rules.h" +#include "../WorldDatabase.h" +#include "../Broker/BrokerManager.h" extern World world; extern MasterSpellList master_spell_list; @@ -41,6 +44,9 @@ extern ConfigReader configReader; extern LuaInterface* lua_interface; extern RuleManager rule_manager; extern Classes classes; +extern MasterItemList master_item_list; +extern WorldDatabase database; +extern BrokerManager broker; MasterItemList::MasterItemList(){ AddMappedItemStat(ITEM_STAT_ADORNING, std::string("adorning")); @@ -151,10 +157,519 @@ map>::iterator MasterItemList::FindBrokerItemMap return enditr; } +bool MasterItemList::ShouldAddItemBrokerType(Item* item, int64 itype) { + bool should_add = false; + switch(itype){ + case ITEM_BROKER_TYPE_ADORNMENT:{ + if(item->IsAdornment()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_AMMO:{ + if(item->IsAmmo()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_ATTUNEABLE:{ + if(item->CheckFlag(ATTUNEABLE)) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_BAG:{ + if(item->IsBag()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_BAUBLE:{ + if(item->IsBauble()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_BOOK:{ + if(item->IsBook()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_CHAINARMOR:{ + if(item->IsChainArmor()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_CLOAK:{ + if(item->IsCloak()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_CLOTHARMOR:{ + if(item->IsClothArmor()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_COLLECTABLE:{ + if(item->IsCollectable()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_CRUSHWEAPON:{ + if(item->IsCrushWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_DRINK:{ + if(item->IsFoodDrink()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_FOOD:{ + if(item->IsFoodFood()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_HOUSEITEM:{ + if(item->IsHouseItem() || item->IsHouseContainer()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_JEWELRY:{ + if(item->IsJewelry()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_LEATHERARMOR:{ + if(item->IsLeatherArmor()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_LORE:{ + if(item->CheckFlag(LORE)) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_MISC:{ + if(item->IsMisc()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_PIERCEWEAPON:{ + if(item->IsPierceWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_PLATEARMOR:{ + if(item->IsPlateArmor()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_POISON:{ + if(item->IsPoison()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_POTION:{ + if(item->IsPotion()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_RECIPEBOOK:{ + if(item->IsRecipeBook()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_SALESDISPLAY:{ + if(item->IsSalesDisplay()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_SHIELD:{ + if(item->IsShield()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_SLASHWEAPON:{ + if(item->IsSlashWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_SPELLSCROLL:{ + if(item->IsSpellScroll()) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_TINKERED:{ + if(item->tinkered == 1) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_TRADESKILL:{ + if(item->crafted == 1) + should_add = true; + break; + } + case ITEM_BROKER_TYPE_2H_CRUSH:{ + should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_STAFF; + break; + } + case ITEM_BROKER_TYPE_2H_PIERCE:{ + should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_GREATSPEAR; + break; + } + case ITEM_BROKER_TYPE_2H_SLASH:{ + should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_GREATSWORD; + break; + } + } + return should_add; +} +bool MasterItemList::ShouldAddItemBrokerSlot(Item* item, int64 ltype) { + bool should_add = false; + + switch(ltype){ + case ITEM_BROKER_SLOT_AMMO:{ + should_add = item->HasSlot(EQ2_AMMO_SLOT); + break; + } + case ITEM_BROKER_SLOT_CHARM:{ + should_add = item->HasSlot(EQ2_CHARM_SLOT_1, EQ2_CHARM_SLOT_2); + break; + } + case ITEM_BROKER_SLOT_CHEST:{ + should_add = item->HasSlot(EQ2_CHEST_SLOT); + break; + } + case ITEM_BROKER_SLOT_CLOAK:{ + should_add = item->HasSlot(EQ2_CLOAK_SLOT); + break; + } + case ITEM_BROKER_SLOT_DRINK:{ + should_add = item->HasSlot(EQ2_DRINK_SLOT); + break; + } + case ITEM_BROKER_SLOT_EARS:{ + should_add = item->HasSlot(EQ2_EARS_SLOT_1, EQ2_EARS_SLOT_2); + break; + } + case ITEM_BROKER_SLOT_FEET:{ + should_add = item->HasSlot(EQ2_FEET_SLOT); + break; + } + case ITEM_BROKER_SLOT_FOOD:{ + should_add = item->HasSlot(EQ2_FOOD_SLOT); + break; + } + case ITEM_BROKER_SLOT_FOREARMS:{ + should_add = item->HasSlot(EQ2_FOREARMS_SLOT); + break; + } + case ITEM_BROKER_SLOT_HANDS:{ + should_add = item->HasSlot(EQ2_HANDS_SLOT); + break; + } + case ITEM_BROKER_SLOT_HEAD:{ + should_add = item->HasSlot(EQ2_HEAD_SLOT); + break; + } + case ITEM_BROKER_SLOT_LEGS:{ + should_add = item->HasSlot(EQ2_LEGS_SLOT); + break; + } + case ITEM_BROKER_SLOT_NECK:{ + should_add = item->HasSlot(EQ2_NECK_SLOT); + break; + } + case ITEM_BROKER_SLOT_PRIMARY:{ + should_add = item->HasSlot(EQ2_PRIMARY_SLOT); + break; + } + case ITEM_BROKER_SLOT_PRIMARY_2H:{ + should_add = item->HasSlot(EQ2_PRIMARY_SLOT) && item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND; + break; + } + case ITEM_BROKER_SLOT_RANGE_WEAPON:{ + should_add = item->HasSlot(EQ2_RANGE_SLOT); + break; + } + case ITEM_BROKER_SLOT_RING:{ + should_add = item->HasSlot(EQ2_LRING_SLOT, EQ2_RRING_SLOT); + break; + } + case ITEM_BROKER_SLOT_SECONDARY:{ + should_add = item->HasSlot(EQ2_SECONDARY_SLOT); + break; + } + case ITEM_BROKER_SLOT_SHOULDERS:{ + should_add = item->HasSlot(EQ2_SHOULDERS_SLOT); + break; + } + case ITEM_BROKER_SLOT_WAIST:{ + should_add = item->HasSlot(EQ2_WAIST_SLOT); + break; + } + case ITEM_BROKER_SLOT_WRIST:{ + should_add = item->HasSlot(EQ2_LWRIST_SLOT, EQ2_RWRIST_SLOT); + break; + } + } + + return should_add; +} + +bool MasterItemList::ShouldAddItemBrokerStat(Item* item, int64 btype) { + bool should_add = false; + bool stat_found = false; + switch(btype){ + case ITEM_BROKER_STAT_TYPE_NONE:{ + if (item->item_stats.size() == 0) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_DEF:{ + stat_found = item->HasStat(ITEM_STAT_DEFENSE, GetItemStatNameByID(ITEM_STAT_DEFENSE)); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_STR:{ + stat_found = item->HasStat(ITEM_STAT_STR); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_STA:{ + stat_found = item->HasStat(ITEM_STAT_STA); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_AGI:{ + stat_found = item->HasStat(ITEM_STAT_AGI); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_WIS:{ + stat_found = item->HasStat(ITEM_STAT_WIS); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_INT:{ + stat_found = item->HasStat(ITEM_STAT_INT); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_HEALTH:{ + stat_found = item->HasStat(ITEM_STAT_HEALTH); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_POWER:{ + stat_found = item->HasStat(ITEM_STAT_POWER); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_HEAT:{ + stat_found = item->HasStat(ITEM_STAT_VS_HEAT); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_COLD:{ + stat_found = item->HasStat(ITEM_STAT_VS_COLD); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_MAGIC:{ + stat_found = item->HasStat(ITEM_STAT_VS_MAGIC); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_MENTAL:{ + stat_found = item->HasStat(ITEM_STAT_VS_MENTAL); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_DIVINE:{ + stat_found = item->HasStat(ITEM_STAT_VS_DIVINE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_POISON:{ + stat_found = item->HasStat(ITEM_STAT_VS_POISON); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_DISEASE:{ + stat_found = item->HasStat(ITEM_STAT_VS_DISEASE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_CRUSH:{ + stat_found = item->HasStat(ITEM_STAT_DMG_CRUSH); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_SLASH:{ + stat_found = item->HasStat(ITEM_STAT_DMG_SLASH); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_PIERCE:{ + stat_found = item->HasStat(ITEM_STAT_DMG_PIERCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_CRITICAL: { + stat_found = item->HasStat(ITEM_STAT_CRITICALMITIGATION); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_DBL_ATTACK:{ + stat_found = item->HasStat(ITEM_STAT_MULTIATTACKCHANCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_ABILITY_MOD:{ + stat_found = item->HasStat(ITEM_STAT_ABILITY_MODIFIER); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_POTENCY:{ + stat_found = item->HasStat(ITEM_STAT_POTENCY); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_AEAUTOATTACK:{ + stat_found = item->HasStat(ITEM_STAT_AEAUTOATTACKCHANCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_ATTACKSPEED:{ + stat_found = item->HasStat(ITEM_STAT_ATTACKSPEED); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_BLOCKCHANCE:{ + stat_found = item->HasStat(ITEM_STAT_EXTRASHIELDBLOCKCHANCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_CASTINGSPEED:{ + stat_found = item->HasStat(ITEM_STAT_ABILITYCASTINGSPEED); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_CRITBONUS:{ + stat_found = item->HasStat(ITEM_STAT_CRITBONUS); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_CRITCHANCE:{ + stat_found = item->HasStat(ITEM_STAT_MELEECRITCHANCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_DPS:{ + stat_found = item->HasStat(ITEM_STAT_DPS); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_FLURRYCHANCE:{ + stat_found = item->HasStat(ITEM_STAT_FLURRY); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_HATEGAIN:{ + stat_found = item->HasStat(ITEM_STAT_HATEGAINMOD); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_MITIGATION:{ + stat_found = item->HasStat(ITEM_STAT_ARMORMITIGATIONINCREASE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_MULTI_ATTACK:{ + stat_found = item->HasStat(ITEM_STAT_MULTIATTACKCHANCE); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_RECOVERY:{ + stat_found = item->HasStat(ITEM_STAT_ABILITYRECOVERYSPEED); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_REUSE_SPEED:{ + stat_found = item->HasStat(ITEM_STAT_ABILITYREUSESPEED); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_SPELL_WPNDMG:{ + stat_found = item->HasStat(ITEM_STAT_SPELLWEAPONDAMAGEBONUS); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_STRIKETHROUGH:{ + stat_found = item->HasStat(ITEM_STAT_STRIKETHROUGH); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_TOUGHNESS:{ + stat_found = item->HasStat(ITEM_STAT_PVPTOUGHNESS); + if (stat_found) + should_add = true; + break; + } + case ITEM_BROKER_STAT_TYPE_WEAPONDMG:{ + stat_found = item->HasStat(ITEM_STAT_WEAPONDAMAGEBONUS); + if (stat_found) + should_add = true; + break; + } + default: { + LogWrite(ITEM__DEBUG, 0, "Item", "Unknown item broker stat type %u", btype); + LogWrite(ITEM__DEBUG, 0, "Item", "If you have a client before the new expansion this may be the reason. Please be patient while we update items to support the new client.", btype); + break; + } + } + + return should_add; +} + vector* MasterItemList::GetItems(string name, int64 itype, int64 ltype, int64 btype, int64 minprice, int64 maxprice, int8 minskill, int8 maxskill, string seller, string adornment, int8 mintier, int8 maxtier, int16 minlevel, int16 maxlevel, sint8 itemclass){ vector* ret = new vector; map::iterator iter; - Item* item = 0; + Item* item = 0; const char* chkname = 0; //const char* chkseller = 0; //const char* chkadornment = 0; @@ -170,511 +685,19 @@ vector* MasterItemList::GetItems(string name, int64 itype, int64 ltype, i item = iter->second; if(item){ if(itype != ITEM_BROKER_TYPE_ANY && itype != ITEM_BROKER_TYPE_ANY64BIT){ - should_add = false; - switch(itype){ - case ITEM_BROKER_TYPE_ADORNMENT:{ - if(item->IsAdornment()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_AMMO:{ - if(item->IsAmmo()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_ATTUNEABLE:{ - if(item->CheckFlag(ATTUNEABLE)) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_BAG:{ - if(item->IsBag()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_BAUBLE:{ - if(item->IsBauble()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_BOOK:{ - if(item->IsBook()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_CHAINARMOR:{ - if(item->IsChainArmor()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_CLOAK:{ - if(item->IsCloak()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_CLOTHARMOR:{ - if(item->IsClothArmor()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_COLLECTABLE:{ - if(item->IsCollectable()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_CRUSHWEAPON:{ - if(item->IsCrushWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_DRINK:{ - if(item->IsFoodDrink()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_FOOD:{ - if(item->IsFoodFood()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_HOUSEITEM:{ - if(item->IsHouseItem() || item->IsHouseContainer()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_JEWELRY:{ - if(item->IsJewelry()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_LEATHERARMOR:{ - if(item->IsLeatherArmor()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_LORE:{ - if(item->CheckFlag(LORE)) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_MISC:{ - if(item->IsMisc()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_PIERCEWEAPON:{ - if(item->IsPierceWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_PLATEARMOR:{ - if(item->IsPlateArmor()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_POISON:{ - if(item->IsPoison()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_POTION:{ - if(item->IsPotion()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_RECIPEBOOK:{ - if(item->IsRecipeBook()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_SALESDISPLAY:{ - if(item->IsSalesDisplay()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_SHIELD:{ - if(item->IsShield()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_SLASHWEAPON:{ - if(item->IsSlashWeapon() && (item->weapon_info->wield_type == ITEM_WIELD_TYPE_DUAL || item->weapon_info->wield_type == ITEM_WIELD_TYPE_SINGLE)) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_SPELLSCROLL:{ - if(item->IsSpellScroll()) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_TINKERED:{ - if(item->tinkered == 1) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_TRADESKILL:{ - if(item->crafted == 1) - should_add = true; - break; - } - case ITEM_BROKER_TYPE_2H_CRUSH:{ - should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_STAFF; - break; - } - case ITEM_BROKER_TYPE_2H_PIERCE:{ - should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_GREATSPEAR; - break; - } - case ITEM_BROKER_TYPE_2H_SLASH:{ - should_add = item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND && item->generic_info.skill_req1 == SKILL_ID_GREATSWORD; - break; - } - } + should_add = ShouldAddItemBrokerType(item, itype); if(!should_add) continue; } if(ltype != ITEM_BROKER_SLOT_ANY){ - should_add = false; - switch(ltype){ - case ITEM_BROKER_SLOT_AMMO:{ - should_add = item->HasSlot(EQ2_AMMO_SLOT); - break; - } - case ITEM_BROKER_SLOT_CHARM:{ - should_add = item->HasSlot(EQ2_CHARM_SLOT_1, EQ2_CHARM_SLOT_2); - break; - } - case ITEM_BROKER_SLOT_CHEST:{ - should_add = item->HasSlot(EQ2_CHEST_SLOT); - break; - } - case ITEM_BROKER_SLOT_CLOAK:{ - should_add = item->HasSlot(EQ2_CLOAK_SLOT); - break; - } - case ITEM_BROKER_SLOT_DRINK:{ - should_add = item->HasSlot(EQ2_DRINK_SLOT); - break; - } - case ITEM_BROKER_SLOT_EARS:{ - should_add = item->HasSlot(EQ2_EARS_SLOT_1, EQ2_EARS_SLOT_2); - break; - } - case ITEM_BROKER_SLOT_FEET:{ - should_add = item->HasSlot(EQ2_FEET_SLOT); - break; - } - case ITEM_BROKER_SLOT_FOOD:{ - should_add = item->HasSlot(EQ2_FOOD_SLOT); - break; - } - case ITEM_BROKER_SLOT_FOREARMS:{ - should_add = item->HasSlot(EQ2_FOREARMS_SLOT); - break; - } - case ITEM_BROKER_SLOT_HANDS:{ - should_add = item->HasSlot(EQ2_HANDS_SLOT); - break; - } - case ITEM_BROKER_SLOT_HEAD:{ - should_add = item->HasSlot(EQ2_HEAD_SLOT); - break; - } - case ITEM_BROKER_SLOT_LEGS:{ - should_add = item->HasSlot(EQ2_LEGS_SLOT); - break; - } - case ITEM_BROKER_SLOT_NECK:{ - should_add = item->HasSlot(EQ2_NECK_SLOT); - break; - } - case ITEM_BROKER_SLOT_PRIMARY:{ - should_add = item->HasSlot(EQ2_PRIMARY_SLOT); - break; - } - case ITEM_BROKER_SLOT_PRIMARY_2H:{ - should_add = item->HasSlot(EQ2_PRIMARY_SLOT) && item->IsWeapon() && item->weapon_info->wield_type == ITEM_WIELD_TYPE_TWO_HAND; - break; - } - case ITEM_BROKER_SLOT_RANGE_WEAPON:{ - should_add = item->HasSlot(EQ2_RANGE_SLOT); - break; - } - case ITEM_BROKER_SLOT_RING:{ - should_add = item->HasSlot(EQ2_LRING_SLOT, EQ2_RRING_SLOT); - break; - } - case ITEM_BROKER_SLOT_SECONDARY:{ - should_add = item->HasSlot(EQ2_SECONDARY_SLOT); - break; - } - case ITEM_BROKER_SLOT_SHOULDERS:{ - should_add = item->HasSlot(EQ2_SHOULDERS_SLOT); - break; - } - case ITEM_BROKER_SLOT_WAIST:{ - should_add = item->HasSlot(EQ2_WAIST_SLOT); - break; - } - case ITEM_BROKER_SLOT_WRIST:{ - should_add = item->HasSlot(EQ2_LWRIST_SLOT, EQ2_RWRIST_SLOT); - break; - } - } + should_add = ShouldAddItemBrokerSlot(item, ltype); if(!should_add) continue; } if(btype != 0xFFFFFFFF){ vector::iterator itr; - bool stat_found = false; - should_add = false; - switch(btype){ - case ITEM_BROKER_STAT_TYPE_NONE:{ - if (item->item_stats.size() == 0) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_DEF:{ - stat_found = item->HasStat(ITEM_STAT_DEFENSE, GetItemStatNameByID(ITEM_STAT_DEFENSE)); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_STR:{ - stat_found = item->HasStat(ITEM_STAT_STR); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_STA:{ - stat_found = item->HasStat(ITEM_STAT_STA); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_AGI:{ - stat_found = item->HasStat(ITEM_STAT_AGI); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_WIS:{ - stat_found = item->HasStat(ITEM_STAT_WIS); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_INT:{ - stat_found = item->HasStat(ITEM_STAT_INT); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_HEALTH:{ - stat_found = item->HasStat(ITEM_STAT_HEALTH); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_POWER:{ - stat_found = item->HasStat(ITEM_STAT_POWER); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_HEAT:{ - stat_found = item->HasStat(ITEM_STAT_VS_HEAT); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_COLD:{ - stat_found = item->HasStat(ITEM_STAT_VS_COLD); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_MAGIC:{ - stat_found = item->HasStat(ITEM_STAT_VS_MAGIC); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_MENTAL:{ - stat_found = item->HasStat(ITEM_STAT_VS_MENTAL); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_DIVINE:{ - stat_found = item->HasStat(ITEM_STAT_VS_DIVINE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_POISON:{ - stat_found = item->HasStat(ITEM_STAT_VS_POISON); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_DISEASE:{ - stat_found = item->HasStat(ITEM_STAT_VS_DISEASE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_CRUSH:{ - stat_found = item->HasStat(ITEM_STAT_DMG_CRUSH); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_SLASH:{ - stat_found = item->HasStat(ITEM_STAT_DMG_SLASH); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_PIERCE:{ - stat_found = item->HasStat(ITEM_STAT_DMG_PIERCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_CRITICAL: { - stat_found = item->HasStat(ITEM_STAT_CRITICALMITIGATION); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_DBL_ATTACK:{ - stat_found = item->HasStat(ITEM_STAT_MULTIATTACKCHANCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_ABILITY_MOD:{ - stat_found = item->HasStat(ITEM_STAT_ABILITY_MODIFIER); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_POTENCY:{ - stat_found = item->HasStat(ITEM_STAT_POTENCY); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_AEAUTOATTACK:{ - stat_found = item->HasStat(ITEM_STAT_AEAUTOATTACKCHANCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_ATTACKSPEED:{ - stat_found = item->HasStat(ITEM_STAT_ATTACKSPEED); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_BLOCKCHANCE:{ - stat_found = item->HasStat(ITEM_STAT_EXTRASHIELDBLOCKCHANCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_CASTINGSPEED:{ - stat_found = item->HasStat(ITEM_STAT_ABILITYCASTINGSPEED); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_CRITBONUS:{ - stat_found = item->HasStat(ITEM_STAT_CRITBONUS); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_CRITCHANCE:{ - stat_found = item->HasStat(ITEM_STAT_MELEECRITCHANCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_DPS:{ - stat_found = item->HasStat(ITEM_STAT_DPS); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_FLURRYCHANCE:{ - stat_found = item->HasStat(ITEM_STAT_FLURRY); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_HATEGAIN:{ - stat_found = item->HasStat(ITEM_STAT_HATEGAINMOD); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_MITIGATION:{ - stat_found = item->HasStat(ITEM_STAT_ARMORMITIGATIONINCREASE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_MULTI_ATTACK:{ - stat_found = item->HasStat(ITEM_STAT_MULTIATTACKCHANCE); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_RECOVERY:{ - stat_found = item->HasStat(ITEM_STAT_ABILITYRECOVERYSPEED); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_REUSE_SPEED:{ - stat_found = item->HasStat(ITEM_STAT_ABILITYREUSESPEED); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_SPELL_WPNDMG:{ - stat_found = item->HasStat(ITEM_STAT_SPELLWEAPONDAMAGEBONUS); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_STRIKETHROUGH:{ - stat_found = item->HasStat(ITEM_STAT_STRIKETHROUGH); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_TOUGHNESS:{ - stat_found = item->HasStat(ITEM_STAT_PVPTOUGHNESS); - if (stat_found) - should_add = true; - break; - } - case ITEM_BROKER_STAT_TYPE_WEAPONDMG:{ - stat_found = item->HasStat(ITEM_STAT_WEAPONDAMAGEBONUS); - if (stat_found) - should_add = true; - break; - } - default: { - LogWrite(ITEM__DEBUG, 0, "Item", "Unknown item broker stat type %u", btype); - LogWrite(ITEM__DEBUG, 0, "Item", "If you have a client before the new expansion this may be the reason. Please be patient while we update items to support the new client.", btype); - break; - } - } + should_add = ShouldAddItemBrokerStat(item, btype); if (!should_add) continue; } @@ -775,18 +798,16 @@ vector* MasterItemList::GetItems(map criteria, Client* cl btype = itr->second[btype]; } } - return GetItems(name, itype, ltype, btype, minprice, maxprice, minskill, maxskill, seller, adornment, mintier, maxtier, minlevel, maxlevel, itemclass); + if(client_to_map && client_to_map->IsGMStoreSearch()) { + return GetItems(name, itype, ltype, btype, minprice, maxprice, minskill, maxskill, seller, adornment, mintier, maxtier, minlevel, maxlevel, itemclass); + } + else { + return broker.GetItems(name, itype, ltype, btype, minprice, maxprice, minskill, maxskill, seller, adornment, mintier, maxtier, minlevel, maxlevel, itemclass); + } } -void MasterItemList::ResetUniqueID(int32 new_id){ - next_unique_id = new_id; -} - -int32 MasterItemList::NextUniqueID(){ - next_unique_id++; - if(next_unique_id >= 0xFFFFFFF0) - next_unique_id = 1; - return next_unique_id; +int64 MasterItemList::NextUniqueID(){ + return database.LoadNextUniqueItemID(); } bool MasterItemList::IsBag(int32 item_id){ @@ -888,7 +909,11 @@ void MasterItemList::AddItem(Item* item){ } Item::Item(){ + seller_char_id = 0; + seller_house_id = 0; + is_search_store_item = false; item_script = ""; + broker_price = 0; sell_price = 0; sell_status = 0; max_sell_value = 0; @@ -920,7 +945,11 @@ Item::Item(){ } Item::Item(Item* in_item){ + seller_char_id = 0; + seller_house_id = 0; + is_search_store_item = false; needs_deletion = false; + broker_price = 0; sell_price = in_item->sell_price; sell_status = in_item->sell_status; max_sell_value = in_item->max_sell_value; @@ -938,6 +967,38 @@ Item::Item(Item* in_item){ grouped_char_ids.insert(in_item->grouped_char_ids.begin(), in_item->grouped_char_ids.end()); effect_type = in_item->effect_type; book_language = in_item->book_language; + details.lock_flags = 0; + details.item_locked = false; +} + +Item::Item(Item* in_item, int64 unique_id, std::string in_creator, std::string in_seller_name, int32 in_seller_char_id, int64 in_broker_price, int16 count, int64 in_seller_house_id){ + is_search_store_item = true; + broker_price = in_broker_price; + needs_deletion = false; + sell_price = in_item->sell_price; + sell_status = in_item->sell_status; + max_sell_value = in_item->max_sell_value; + save_needed = false; + SetItem(in_item); + details.unique_id = unique_id; + if (IsBag()) + details.bag_id = details.unique_id; + generic_info.condition = 100; + spell_id = in_item->spell_id; + spell_tier = in_item->spell_tier; + no_buy_back = in_item->no_buy_back; + no_sale = in_item->no_sale; + created = in_item->created; + grouped_char_ids.insert(in_item->grouped_char_ids.begin(), in_item->grouped_char_ids.end()); + effect_type = in_item->effect_type; + book_language = in_item->book_language; + creator = in_creator; + seller_name = in_seller_name; + seller_char_id = in_seller_char_id; + details.count = count; + seller_house_id = in_seller_house_id; + details.lock_flags = 0; + details.item_locked = false; } Item::~Item(){ @@ -1029,7 +1090,7 @@ void Item::SetItem(Item* old_item){ memcpy(bauble_info, old_item->bauble_info, sizeof(Bauble_Info)); break; } - case ITEM_TYPE_SKILL:{ + case ITEM_TYPE_SKILL:{ skill_info = new Skill_Info; memcpy(skill_info, old_item->skill_info, sizeof(Skill_Info)); break; @@ -1074,6 +1135,12 @@ void Item::SetItem(Item* old_item){ case ITEM_TYPE_HOUSE_CONTAINER:{ houseitem_info = new HouseItem_Info; memset(houseitem_info, 0, sizeof(HouseItem_Info)); + bag_info = new Bag_Info; + memset(bag_info, 0, sizeof(Bag_Info)); + + if(old_item->bag_info) + memcpy(bag_info, old_item->bag_info, sizeof(Bag_Info)); + if(old_item->houseitem_info) { memcpy(houseitem_info, old_item->houseitem_info, sizeof(HouseItem_Info)); } @@ -1346,7 +1413,7 @@ bool Item::IsRanged(){ } bool Item::IsBag(){ - return generic_info.item_type == ITEM_TYPE_BAG; + return generic_info.item_type == ITEM_TYPE_BAG || generic_info.item_type == ITEM_TYPE_HOUSE_CONTAINER; } bool Item::IsFood(){ @@ -1547,7 +1614,7 @@ void Item::SetItemType(int8 in_type){ ranged_info = new Ranged_Info; memset(ranged_info, 0, sizeof(Ranged_Info)); } - else if(IsBag() && !bag_info){ + else if(IsBag() && !IsHouseContainer() && !bag_info){ bag_info = new Bag_Info; memset(bag_info, 0, sizeof(Bag_Info)); } @@ -1578,11 +1645,14 @@ void Item::SetItemType(int8 in_type){ book_info->author.size = 0; book_info->title.size = 0; } - else if(IsHouseItem() && !houseitem_info){ + else if(IsHouseItem() && !IsHouseContainer() && !houseitem_info){ houseitem_info = new HouseItem_Info; memset(houseitem_info, 0, sizeof(HouseItem_Info)); } else if(IsHouseContainer() && !housecontainer_info){ + bag_info = new Bag_Info; + memset(bag_info, 0, sizeof(Bag_Info)); + if(!houseitem_info) { houseitem_info = new HouseItem_Info; memset(houseitem_info, 0, sizeof(HouseItem_Info)); @@ -2515,7 +2585,58 @@ void Item::serialize(PacketStruct* packet, bool show_name, Player* player, int16 packet->setDataByName("broker_commission", housecontainer_info->broker_commission); packet->setDataByName("fence_commission", housecontainer_info->fence_commission); } - } + if(bag_info){ + int8 max_slots = player->GetMaxBagSlots(client->GetVersion()); + if (bag_info->num_slots > max_slots) + bag_info->num_slots = max_slots; + + int16 free_slots = bag_info->num_slots; + if (player) { + Item* bag = player->GetPlayerItemList()->GetItemFromUniqueID(details.unique_id, true); + if (bag && bag->IsBag()) { + vector* bag_items = player->GetPlayerItemList()->GetItemsInBag(bag); + if (bag_items->size() > bag->bag_info->num_slots) { + free_slots = 0; + packet->setArrayLengthByName("num_names", bag->bag_info->num_slots); + } + else { + free_slots = bag->bag_info->num_slots - bag_items->size(); + packet->setArrayLengthByName("num_names", bag_items->size()); + } + vector::iterator itr; + int16 i = 0; + Item* tmp_bag_item = 0; + for (itr = bag_items->begin(); itr != bag_items->end(); itr++) { + tmp_bag_item = *itr; + if (tmp_bag_item && tmp_bag_item->details.slot_id < bag->bag_info->num_slots) { + packet->setArrayDataByName("item_name", tmp_bag_item->name.c_str(), i); + i++; + } + } + safe_delete(bag_items); + } + } + packet->setDataByName("num_slots", bag_info->num_slots); + packet->setDataByName("num_empty", free_slots); + packet->setDataByName("weight_reduction", bag_info->weight_reduction); + packet->setDataByName("item_score", 2); + //packet->setDataByName("unknown5", 0x1e50a86f); + //packet->setDataByName("unknown6", 0x2c17f61d); + //1 armorer + //2 weaponsmith + //4 tailor + //16 jeweler + //32 sage + //64 alchemist + //120 all scholars + //250 all craftsman + //int8 blah[] = {0x00,0x00,0x01,0x01,0xb6,0x01,0x01}; + //int8 blah[] = {0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; + int8 blah[] = { 0xd8,0x66,0x9b,0x6d,0xb6,0xfb,0x7f }; + for (int8 i = 0; i < sizeof(blah); i++) + packet->setSubstructDataByName("footer", "footer_unknown_0", blah[i], 0, i); + } + } } } @@ -3040,8 +3161,8 @@ Item* PlayerItemList::GetBankBag(int8 inventory_slot, bool lock){ Item* bag = 0; if(lock) MPlayerItems.readlock(__FUNCTION__, __LINE__); - if(items.count(-3) > 0 && items[-3][BASE_EQUIPMENT].count(inventory_slot) > 0 && items[-3][BASE_EQUIPMENT][inventory_slot]->IsBag()) - bag = items[-3][BASE_EQUIPMENT][inventory_slot]; + if(items.count(InventorySlotType::BANK) > 0 && items[InventorySlotType::BANK][BASE_EQUIPMENT].count(inventory_slot) > 0 && items[InventorySlotType::BANK][BASE_EQUIPMENT][inventory_slot]->IsBag()) + bag = items[InventorySlotType::BANK][BASE_EQUIPMENT][inventory_slot]; if(lock) MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); return bag; @@ -3129,10 +3250,10 @@ bool PlayerItemList::HasFreeSlot(){ bool PlayerItemList::GetFirstFreeBankSlot(sint32* bag_id, sint16* slot) { bool ret = false; MPlayerItems.readlock(__FUNCTION__, __LINE__); - if (items.count(-3) > 0) { + if (items.count(InventorySlotType::BANK) > 0) { for (int8 i = 0; i < NUM_BANK_SLOTS; i++) { - if (items[-3][BASE_EQUIPMENT].count(i) == 0) { - *bag_id = -3; + if (items[InventorySlotType::BANK][BASE_EQUIPMENT].count(i) == 0) { + *bag_id = InventorySlotType::BANK; *slot = i; ret = true; break; @@ -3140,7 +3261,7 @@ bool PlayerItemList::GetFirstFreeBankSlot(sint32* bag_id, sint16* slot) { } } else { - *bag_id = -3; + *bag_id = InventorySlotType::BANK; *slot = 0; ret = true; } @@ -3299,9 +3420,13 @@ void PlayerItemList::Stack(Item* orig_item, Item* item){ orig_item->save_needed = true; } -bool PlayerItemList::AssignItemToFreeSlot(Item* item){ +bool PlayerItemList::AssignItemToFreeSlot(Item* item, bool inventory_only){ if(item){ - Item* orig_item = CanStack(item); + Item* orig_item = CanStack(item, !inventory_only); + + if(inventory_only && !IsItemInSlotType(orig_item, InventorySlotType::BASE_INVENTORY)){ + orig_item = nullptr; + } if(orig_item){ Stack(orig_item, item); return true; @@ -3347,8 +3472,9 @@ bool PlayerItemList::AssignItemToFreeSlot(Item* item){ } -void PlayerItemList::RemoveItem(Item* item, bool delete_item){ - MPlayerItems.writelock(__FUNCTION__, __LINE__); +void PlayerItemList::RemoveItem(Item* item, bool delete_item, bool lock){ + if(lock) + MPlayerItems.writelock(__FUNCTION__, __LINE__); if(items.count(item->details.inv_slot_id) > 0 && items[item->details.inv_slot_id][item->details.appearance_type].count(item->details.slot_id) > 0){ items[item->details.inv_slot_id][item->details.appearance_type].erase(item->details.slot_id); indexed_items[item->details.index] = 0; @@ -3375,7 +3501,8 @@ void PlayerItemList::RemoveItem(Item* item, bool delete_item){ lua_interface->SetLuaUserDataStale(item); safe_delete(item); } - MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); + if(lock) + MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); } void PlayerItemList::DestroyItem(int16 index){ @@ -3446,7 +3573,9 @@ int32 PlayerItemList::GetWeight(){ for(int16 i = 0; i < indexed_items.size(); i++){ Item* item = indexed_items[i]; if (item) { - if(item->details.inv_slot_id != -3 && item->details.inv_slot_id != -4) + if(!IsItemInSlotType(item, InventorySlotType::BANK, false) && + !IsItemInSlotType(item, InventorySlotType::SHARED_BANK, false) && + !IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT, false)) ret += item->generic_info.weight; } } @@ -3454,15 +3583,27 @@ int32 PlayerItemList::GetWeight(){ return ret; } +bool PlayerItemList::IsItemInSlotType(Item* item, InventorySlotType type, bool lockItems) { + if(!item) + return false; + + bool matchType = (item->details.inv_slot_id == type); + if(item->details.inv_slot_id > 0) { + Item* bagItem = GetItemFromUniqueID(item->details.inv_slot_id, true, lockItems); + if(bagItem && bagItem->details.inv_slot_id == type) + matchType = true; + } + return matchType; +} bool PlayerItemList::MoveItem(sint32 to_bag_id, int16 from_index, sint8 to, int8 appearance_type, int8 charges){ MPlayerItems.writelock(__FUNCTION__, __LINE__); Item* item_from = indexed_items[from_index]; Item* item_to = 0; - if(item_from){ + if(item_from && !item_from->IsItemLocked()){ if(to_bag_id > 0){ //bag item Item* bag = GetItemFromUniqueID(to_bag_id, true, false); - if(bag && bag->details.num_slots > to && (!item_from || !item_from->IsBag())) + if(bag && !bag->IsItemLocked() && bag->details.num_slots > to && (!item_from || !item_from->IsBag())) item_to = items[to_bag_id][BASE_EQUIPMENT][to]; else{ MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); @@ -3476,6 +3617,14 @@ bool PlayerItemList::MoveItem(sint32 to_bag_id, int16 from_index, sint8 to, int8 return false; } } + + LogWrite(PLAYER__ERROR, 0, "MoveItem", + "--Item: %u is locked %u", item_to ? item_to->details.unique_id : 0, item_to ? item_to->IsItemLocked() : 0 + ); + if(item_to && item_to->IsItemLocked()) { + MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); + return false; + } if(charges > 0) { if (item_to && item_from->details.item_id == item_to->details.item_id){ if(item_to->details.count > 0 && item_to->details.count < item_to->stack_count){ @@ -3554,14 +3703,23 @@ bool PlayerItemList::MoveItem(sint32 to_bag_id, int16 from_index, sint8 to, int8 return true; } } + + bool canMove = true; + if(item_to && item_to->IsItemLocked()) + canMove = false; + MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); - if (item_to) + LogWrite(PLAYER__ERROR, 0, "MoveItem", + "--Item#2: %u is locked %u", item_to ? item_to->details.unique_id : 0, item_to ? item_to->IsItemLocked() : 0 + ); + if (item_to && canMove) MoveItem(item_to, item_from->details.inv_slot_id, item_from->details.slot_id, BASE_EQUIPMENT, true); - MoveItem(item_from, to_bag_id, to, BASE_EQUIPMENT, item_to ? false:true); + if(canMove) + MoveItem(item_from, to_bag_id, to, BASE_EQUIPMENT, item_to ? false:true); - return true; + return canMove; } MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); return false; @@ -3751,7 +3909,7 @@ void PlayerItemList::AddItemToPacket(PacketStruct* packet, Player* player, Item* //TODO: Add check to allow scribe menu_data += ITEM_MENU_TYPE_SCRIBE; } - if (item->generic_info.item_type == ITEM_TYPE_HOUSE || item->generic_info.item_type == ITEM_TYPE_HOUSE_CONTAINER){ + if (item->generic_info.item_type == ITEM_TYPE_HOUSE || (item->generic_info.item_type == ITEM_TYPE_HOUSE_CONTAINER && item->details.inv_slot_id == InventorySlotType::HOUSE_VAULT)){ // containers must be in base house slot for placement menu_data += ITEM_MENU_TYPE_TEST1; menu_data += ITEM_MENU_TYPE_HOUSE; } @@ -3800,7 +3958,7 @@ void PlayerItemList::AddItemToPacket(PacketStruct* packet, Player* player, Item* menu_data += ORIG_ITEM_MENU_TYPE_FOOD; } } - if(item->details.item_locked) { + if(item->IsItemLocked()) { menu_data += ITEM_MENU_TYPE_BROKEN; // broken is also used to lock item during crafting } // Added the if (overflow) so mouseover examines work properly @@ -3862,7 +4020,7 @@ bool PlayerItemList::AddOverflowItem(Item* item) { MPlayerItems.writelock(__FUNCTION__, __LINE__); if (item && item->details.item_id > 0 && overflowItems.size() < 255) { item->details.slot_id = 6; - item->details.inv_slot_id = -2; + item->details.inv_slot_id = InventorySlotType::OVERFLOW; overflowItems.push_back(item); ret = true; } @@ -3897,11 +4055,18 @@ vector* PlayerItemList::GetOverflowItemList() { } bool PlayerItemList::HasItem(int32 id, bool include_bank){ + if(include_bank) { + Item* item = GetItemFromID(id, 1, true, true); + if(item) + return true; + else + return false; + } map> >::iterator itr; map::iterator slot_itr; MPlayerItems.readlock(__FUNCTION__, __LINE__); for(itr = items.begin(); itr != items.end(); itr++){ - if(include_bank || (!include_bank && itr->first >= 0)){ + if(itr->first >= 0){ for(slot_itr=itr->second[BASE_EQUIPMENT].begin();slot_itr!=itr->second[BASE_EQUIPMENT].end(); slot_itr++){ if(slot_itr->second && slot_itr->second->details.item_id == id){ MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); @@ -4082,10 +4247,245 @@ Item* PlayerItemList::GetItemFromUniqueID(int32 id, bool include_bank, bool lock return 0; } +void PlayerItemList::SetVaultItemLockUniqueID(Client* client, int64 id, bool state, bool lock) { + if (lock) { + MPlayerItems.readlock(__FUNCTION__, __LINE__); + } + sint32 inv_slot_id = 0; + Item* item = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(id, false); + if(item) { + bool bag_remains_locked = true; + if(state && !item->TryLockItem(LockReason::LockReason_Shop)) { + // this shouldn't happen, but if we have a conflict it might + client->Message(CHANNEL_COLOR_RED, "Failed to lock item %u for vault.", id); + return; + } + else if(!state && !item->TryUnlockItem(LockReason::LockReason_Shop)) { + // still in use for another reason, we don't need to report to the user it will spam them + } + if(item->details.inv_slot_id) { + Item* bagItem = client->GetPlayer()->item_list.GetVaultItemFromUniqueID(item->details.inv_slot_id, false); + if(bagItem) { + inv_slot_id = item->details.inv_slot_id; + if(!state) { + bag_remains_locked = false; + if (auto bagIt = items.find(item->details.inv_slot_id); + bagIt != items.end()) + { + const auto& bagSlots = bagIt->second.at(BASE_EQUIPMENT); + for (auto& [bagSlot, bagItem] : bagSlots) { + if (bagItem && bagItem->IsItemLocked()) { + bag_remains_locked = true; + break; + } + } + } + } + if(!bag_remains_locked) { + bagItem->TryUnlockItem(LockReason::LockReason_Shop); + } + else { + bagItem->TryLockItem(LockReason::LockReason_Shop); + } + } + } + } + if (lock) { + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + if(inv_slot_id) { + client->GetPlayer()->UpdateInventory(item->details.inv_slot_id); + } + else { + EQ2Packet* outapp = client->GetPlayer()->SendInventoryUpdate(client->GetVersion()); + client->QueuePacket(outapp); + } +} + +bool PlayerItemList::CanStoreSellItem(int64 unique_id, bool lock) { + if (lock) { + MPlayerItems.readlock(__FUNCTION__, __LINE__); + } + Item* item = GetVaultItemFromUniqueID(unique_id, false); + if(!item || (item->CheckFlag(NO_TRADE) && (item->CheckFlag2(HEIRLOOM) == 0))) { + if(lock) + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + if(item->CheckFlag(ATTUNED)) { + if(lock) + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + + if(item->IsBag() && items.count(item->details.bag_id) > 0){ + map::iterator itr; + for(itr = items[item->details.bag_id][BASE_EQUIPMENT].begin(); itr != items[item->details.bag_id][BASE_EQUIPMENT].end(); itr++){ + if(itr->second->CheckFlag(NO_TRADE) && itr->second->CheckFlag2(HEIRLOOM) == 0){ + if(lock) + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + if(item->CheckFlag(ATTUNED)) { + if(lock) + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return false; + } + } + } + if(lock) { + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + return true; +} + +void PlayerItemList::SetVaultItemUniqueIDCount(Client* client, int64 unique_id, int16 count, bool lock) { + if (lock) { + MPlayerItems.readlock(__FUNCTION__, __LINE__); + } + + sint32 inv_slot_id = 0; + bool countUpdated = false; + Item* item = GetVaultItemFromUniqueID(unique_id, false); + if(item) { + item->details.count = count; + item->save_needed = true; + inv_slot_id = item->details.inv_slot_id; + countUpdated = true; + } + if (lock) { + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + + if(client && client->GetPlayer() && countUpdated) { + if(inv_slot_id) { + client->GetPlayer()->UpdateInventory(inv_slot_id); + } + else { + EQ2Packet* outapp = client->GetPlayer()->SendInventoryUpdate(client->GetVersion()); + client->QueuePacket(outapp); + } + } +} + + +void PlayerItemList::RemoveVaultItemFromUniqueID(Client* client, int64 unique_id, bool lock) { + if (lock) { + MPlayerItems.writelock(__FUNCTION__, __LINE__); + } + + sint32 inv_slot_id = 0; + bool foundItem = false; + Item* item = GetVaultItemFromUniqueID(unique_id, false); + if(item) { + inv_slot_id = item->details.inv_slot_id; + RemoveItem(item, true, false); + foundItem = true; + } + if (lock) { + MPlayerItems.releasewritelock(__FUNCTION__, __LINE__); + } + + if(client && client->GetPlayer() && foundItem) { + if(inv_slot_id) { + client->GetPlayer()->UpdateInventory(inv_slot_id); + } + else { + EQ2Packet* outapp = client->GetPlayer()->SendInventoryUpdate(client->GetVersion()); + client->QueuePacket(outapp); + } + } +} + +Item* PlayerItemList::GetVaultItemFromUniqueID(int64 id, bool lock) { + if (lock) { + MPlayerItems.readlock(__FUNCTION__, __LINE__); + } + + // 1) Check the house vault + if (auto vaultIt = items.find(InventorySlotType::HOUSE_VAULT); + vaultIt != items.end()) + { + for (auto& [containerIdx, slotMap] : vaultIt->second) { + for (auto& [slotID, itemPtr] : slotMap) { + if(itemPtr) { + LogWrite(PLAYER__ERROR, 0, "Vault", + "--GetVaultItem: %u (%s - %u) needs to match %u", slotID, itemPtr->name.c_str(), itemPtr->details.unique_id, id + ); + } + else { + LogWrite(PLAYER__ERROR, 0, "Vault", + "--GetVaultItem: %u (??) needs to match %u", slotID, id + ); + } + if (itemPtr && itemPtr->details.unique_id == id) { + if (lock) MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return itemPtr; + } + + // If it's a bag, search its contents + if (!itemPtr->IsBag()) + continue; + + if (auto bagIt = items.find(itemPtr->details.bag_id); + bagIt != items.end()) + { + const auto& bagSlots = bagIt->second.at(BASE_EQUIPMENT); + for (auto& [bagSlot, bagItem] : bagSlots) { + if (bagItem && bagItem->details.unique_id == id) { + if (lock) MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return bagItem; + } + } + } + } + } + } + + // 2) Check base inventory slots and any bags inside them + const auto& baseEquip = items + .at(InventorySlotType::BASE_INVENTORY) + .at(BASE_EQUIPMENT); + + for (int8 slotIdx = 0; slotIdx < NUM_INV_SLOTS; ++slotIdx) { + auto it = baseEquip.find(slotIdx); + if (it == baseEquip.end() || it->second == nullptr) + continue; + + Item* curr = it->second; + // Direct match in base slot + if (curr->details.unique_id == id) { + if (lock) MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return curr; + } + + // If it's a bag, search its contents + if (!curr->IsBag()) + continue; + + if (auto bagIt = items.find(curr->details.bag_id); + bagIt != items.end()) + { + const auto& bagSlots = bagIt->second.at(BASE_EQUIPMENT); + for (auto& [bagSlot, bagItem] : bagSlots) { + if (bagItem && bagItem->details.unique_id == id) { + if (lock) MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + return bagItem; + } + } + } + } + + if (lock) { + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); + } + return nullptr; +} + bool PlayerItemList::HasFreeBankSlot() { bool ret = false; MPlayerItems.readlock(__FUNCTION__, __LINE__); - if (items[-3][BASE_EQUIPMENT].size() < 12) //12 slots in the bank + if (items[InventorySlotType::BANK][BASE_EQUIPMENT].size() < 12) //12 slots in the bank ret = true; MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); return ret; @@ -4095,7 +4495,7 @@ int8 PlayerItemList::FindFreeBankSlot() { int8 ret = 0; MPlayerItems.readlock(__FUNCTION__, __LINE__); for (int8 i = 0; i < 12; i++) { //12 slots in the bank - if (items[-3][BASE_EQUIPMENT].count(i) == 0) { + if (items[InventorySlotType::BANK][BASE_EQUIPMENT].count(i) == 0) { ret = i; break; } @@ -4104,6 +4504,154 @@ int8 PlayerItemList::FindFreeBankSlot() { return ret; } +void PlayerItemList::PopulateHouseStoragePacket(Client* client, PacketStruct* packet, Item* item, int16 itemIdx, int8 storage_flags) { + int64 cost = broker.GetSalePrice(client->GetPlayer()->GetCharacterID(), item->details.unique_id); + bool sale = broker.IsItemForSale(client->GetPlayer()->GetCharacterID(), item->details.unique_id); + bool isInv = !IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT, false); + client->AddItemSale(item->details.unique_id, item->details.item_id, cost, item->details.inv_slot_id, item->details.slot_id, item->details.count, isInv, sale, item->creator); + + if(broker.IsItemForSale(client->GetPlayer()->GetCharacterID(), item->details.unique_id)) + storage_flags += HouseStoreItemFlags::HOUSE_STORE_FOR_SALE; + + LogWrite(PLAYER__ERROR, 5, "Broker", + "--Sell broker item: %u (%s - %u), cost=%u", + client->GetPlayer()->GetCharacterID(), item->name.c_str(), item->details.unique_id, cost + ); + + packet->setArrayDataByName("your_item_name", item->name.c_str(), itemIdx); + packet->setArrayDataByName("unique_id", item->details.item_id, itemIdx); + packet->setArrayDataByName("unique_id2", item->details.unique_id, itemIdx); + packet->setArrayDataByName("cost", cost, itemIdx); + packet->setArrayDataByName("your_item_quantity", item->details.count, itemIdx); + packet->setArrayDataByName("your_item_icon", item->GetIcon(packet->GetVersion()), itemIdx); + packet->setArrayDataByName("storage_flags", storage_flags, itemIdx); +} + +void PlayerItemList::GetVaultItems(Client* client, int32 spawn_id, int8 maxSlots, bool isSelling) { + int8 ret = 0; + int8 numItems = 0; + MPlayerItems.readlock(__FUNCTION__, __LINE__); + for (int8 i = 0; i < maxSlots; i++) { + if (items[InventorySlotType::HOUSE_VAULT][BASE_EQUIPMENT].count(i) != 0) { + Item* item = items[InventorySlotType::HOUSE_VAULT][BASE_EQUIPMENT][i]; + if(item) { + if(!item->IsBag()) { + numItems++; + } + else { + bool bagHasItem = false; + if(items.count(item->details.bag_id) > 0){ + map::iterator itr; + for(itr = items[item->details.bag_id][BASE_EQUIPMENT].begin(); itr != items[item->details.bag_id][BASE_EQUIPMENT].end(); itr++){ + if(itr->second) { + numItems++; + bagHasItem = true; + } + } + } + if(!bagHasItem) { + numItems++; + } + } + } + } + } + for (int8 i = 0; i < NUM_INV_SLOTS; i++) { + if (items[InventorySlotType::BASE_INVENTORY][BASE_EQUIPMENT].count(i) != 0) { + Item* item = items[InventorySlotType::BASE_INVENTORY][BASE_EQUIPMENT][i]; + if(item) { + if(!item->IsBag()) { + numItems++; + } + else { + bool bagHasItem = false; + if(items.count(item->details.bag_id) > 0){ + map::iterator itr; + for(itr = items[item->details.bag_id][BASE_EQUIPMENT].begin(); itr != items[item->details.bag_id][BASE_EQUIPMENT].end(); itr++){ + if(itr->second) { + numItems++; + bagHasItem = true; + } + } + } + if(!bagHasItem) { + numItems++; + } + } + } + } + } + + PacketStruct* packet = configReader.getStruct("WS_HouseStorage", client->GetVersion()); + if (packet) { + packet->setDataByName("spawn_id", spawn_id); + packet->setDataByName("type", isSelling ? 6 : 4); + packet->setArrayLengthByName("your_item_count", numItems); + int16 itemIdx = 0; + for (int8 i = 0; i < maxSlots; i++) { + if (items[InventorySlotType::HOUSE_VAULT][BASE_EQUIPMENT].count(i) != 0) { + Item* item = items[InventorySlotType::HOUSE_VAULT][BASE_EQUIPMENT][i]; + if(item) { + if(!item->IsBag()) { + PopulateHouseStoragePacket(client, packet, item, itemIdx, HouseStoreItemFlags::HOUSE_STORE_VAULT_TAB); + itemIdx++; + } + else { + bool bagHasItem = false; + if(items.count(item->details.bag_id) > 0){ + map::iterator itr; + for(itr = items[item->details.bag_id][BASE_EQUIPMENT].begin(); itr != items[item->details.bag_id][BASE_EQUIPMENT].end(); itr++){ + if(itr->second) { + PopulateHouseStoragePacket(client, packet, itr->second, itemIdx, HouseStoreItemFlags::HOUSE_STORE_VAULT_TAB); + bagHasItem = true; + itemIdx++; + } + } + } + if(!bagHasItem) { + PopulateHouseStoragePacket(client, packet, item, itemIdx, HouseStoreItemFlags::HOUSE_STORE_VAULT_TAB); + itemIdx++; + } + } + } + } + } + + for (int8 i = 0; i < NUM_INV_SLOTS; i++) { + if (items[InventorySlotType::BASE_INVENTORY][BASE_EQUIPMENT].count(i) != 0) { + Item* item = items[InventorySlotType::BASE_INVENTORY][BASE_EQUIPMENT][i]; + if(item) { + if(!item->IsBag()) { + PopulateHouseStoragePacket(client, packet, item, itemIdx, 0); + itemIdx++; + } + else { + bool bagHasItem = false; + if(items.count(item->details.bag_id) > 0){ + map::iterator itr; + for(itr = items[item->details.bag_id][BASE_EQUIPMENT].begin(); itr != items[item->details.bag_id][BASE_EQUIPMENT].end(); itr++){ + if(itr->second) { + PopulateHouseStoragePacket(client, packet, itr->second, itemIdx, 0); + bagHasItem = true; + itemIdx++; + } + } + } + if(!bagHasItem) { + PopulateHouseStoragePacket(client, packet, item, itemIdx, 0); + itemIdx++; + } + } + } + } + } + EQ2Packet* outapp = packet->serialize(); + client->QueuePacket(outapp); + safe_delete(packet); + } + MPlayerItems.releasereadlock(__FUNCTION__, __LINE__); +} + void PlayerItemList::ResetPackets() { MPlayerItems.writelock(__FUNCTION__, __LINE__); safe_delete_array(orig_packet); @@ -4237,7 +4785,7 @@ bool EquipmentItemList::AddItem(int8 slot, Item* item){ SetItem(slot, item, true); if (item->details.unique_id == 0) { - GetItem(slot)->details.unique_id = MasterItemList::NextUniqueID(); + GetItem(slot)->details.unique_id = master_item_list.NextUniqueID(); if (item->IsBag()) item->details.bag_id = item->details.unique_id; } @@ -4639,6 +5187,57 @@ int16 Item::GetIcon(int16 version) { return details.icon; } +bool Item::TryLockItem(LockReason reason) { + std::unique_lock lock(item_lock_mtx_); + // current flags + auto cur = static_cast(details.lock_flags); + + // 0) If this reason is already applied, succeed immediately + if ((cur & reason) == reason) { + return true; + } + + // 1) No lock held? allow any first‐lock + if (cur == LockReason::LockReason_None) { + details.lock_flags = static_cast(reason); + details.item_locked = true; + return true; + } + + // 2) Only House‐lock held, and we're adding Shop‐lock? allow + if ((cur == LockReason::LockReason_House && reason == LockReason::LockReason_Shop) || + (cur == LockReason::LockReason_Shop && reason == LockReason::LockReason_House)) { + details.lock_flags = static_cast(cur | reason); + // item_locked already true + return true; + } + + // 3) Anything else: reject + return false; +} + +bool Item::TryUnlockItem(LockReason reason) { + std::unique_lock lock(item_lock_mtx_); + LockReason cur = static_cast(details.lock_flags); + if ((cur & reason) == reason) { + details.lock_flags = int32(cur & ~reason); + if (details.lock_flags == 0) + details.item_locked = false; + return true; + } + return false; +} + +bool Item::IsItemLocked() { + std::shared_lock lock(item_lock_mtx_); + return details.lock_flags != 0; +} + +bool Item::IsItemLockedFor(LockReason reason) { + std::shared_lock lock(item_lock_mtx_); + return (static_cast(details.lock_flags) & reason) == reason; +} + int32 MasterItemList::GetItemStatIDByName(std::string name) { boost::to_lower(name); diff --git a/source/WorldServer/Items/Items.h b/source/WorldServer/Items/Items.h index 25c021c..6c200fd 100644 --- a/source/WorldServer/Items/Items.h +++ b/source/WorldServer/Items/Items.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,11 +17,13 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __EQ2_ITEMS__ #define __EQ2_ITEMS__ #include #include #include +#include #include "../../common/types.h" #include "../../common/DataBuffer.h" #include "../Commands/Commands.h" @@ -647,6 +649,47 @@ enum ItemEffectType { EFFECT_CURE_TYPE_MAGIC=6, EFFECT_CURE_TYPE_ALL=7 }; + +enum InventorySlotType { + HOUSE_VAULT=-5, + SHARED_BANK=-4, + BANK=-3, + OVERFLOW=-2, + UNKNOWN_INV_SLOT_TYPE=-1, + BASE_INVENTORY=0 +}; + +enum class LockReason : int32 { + LockReason_None = 0, + LockReason_House = 1u << 0, + LockReason_Crafting = 1u << 1, + LockReason_Shop = 1u << 2, +}; + +inline LockReason operator|(LockReason a, LockReason b) { + return static_cast( + static_cast(a) | static_cast(b) + ); +} +inline LockReason operator&(LockReason a, LockReason b) { + return static_cast( + static_cast(a) & static_cast(b) + ); +} +inline LockReason operator~(LockReason a) { + return static_cast(~static_cast(a)); +} + +enum HouseStoreItemFlags { + HOUSE_STORE_ITEM_TEXT_RED=1, + HOUSE_STORE_UNKNOWN_BIT2=2, + HOUSE_STORE_UNKNOWN_BIT4=4, + HOUSE_STORE_FOR_SALE=8, + HOUSE_STORE_UNKNOWN_BIT16=16, + HOUSE_STORE_VAULT_TAB=32 + // rest are also unknown +}; + #pragma pack(1) struct ItemStatsValues{ sint16 str; @@ -710,10 +753,11 @@ struct ItemCore{ int16 count; int8 tier; int8 num_slots; - int32 unique_id; + int64 unique_id; int8 num_free_slots; int16 recommended_level; bool item_locked; + int32 lock_flags; bool new_item; int16 new_index; }; @@ -934,6 +978,8 @@ public: #pragma pack() Item(); Item(Item* in_item); + Item(Item* in_item, int64 unique_id, std::string in_creator, std::string in_seller_name, int32 in_seller_char_id, int64 in_broker_price, int16 count, int64 in_seller_house_id); + ~Item(); string lowername; string name; @@ -942,10 +988,15 @@ public: int32 sell_price; int32 sell_status; int32 max_sell_value; + int64 broker_price; + bool is_search_store_item; bool save_needed; int8 weapon_type; string adornment; string creator; + string seller_name; + int32 seller_char_id; + int64 seller_house_id; int32 adorn0; int32 adorn1; int32 adorn2; @@ -986,6 +1037,7 @@ public: bool crafted; bool tinkered; int8 book_language; + mutable std::shared_mutex item_lock_mtx_; void AddEffect(string effect, int8 percentage, int8 subbulletflag); void AddBookPage(int8 page, string page_text,int8 valign, int8 halign); @@ -1067,6 +1119,10 @@ public: void AddSlot(int8 slot_id); void SetSlots(int32 slots); int16 GetIcon(int16 version); + bool TryLockItem(LockReason reason); + bool TryUnlockItem(LockReason reason); + bool IsItemLocked(); + bool IsItemLockedFor(LockReason reason); }; class MasterItemList{ public: @@ -1079,14 +1135,16 @@ public: Item* GetAllItemsByClassification(const char* name); ItemStatsValues* CalculateItemBonuses(int32 item_id, Entity* entity = 0); ItemStatsValues* CalculateItemBonuses(Item* desc, Entity* entity = 0, ItemStatsValues* values = 0); + + bool ShouldAddItemBrokerType(Item* item, int64 itype); + bool ShouldAddItemBrokerSlot(Item* item, int64 ltype); + bool ShouldAddItemBrokerStat(Item* item, int64 btype); vector* GetItems(string name, int64 itype, int64 ltype, int64 btype, int64 minprice, int64 maxprice, int8 minskill, int8 maxskill, string seller, string adornment, int8 mintier, int8 maxtier, int16 minlevel, int16 maxlevel, sint8 itemclass); vector* GetItems(map criteria, Client* client_to_map); void AddItem(Item* item); bool IsBag(int32 item_id); void RemoveAll(); - static int32 NextUniqueID(); - static void ResetUniqueID(int32 new_id); - static int32 next_unique_id; + static int64 NextUniqueID(); int32 GetItemStatIDByName(std::string name); std::string GetItemStatNameByID(int32 id); void AddMappedItemStat(int32 id, std::string lower_case_name); @@ -1120,10 +1178,18 @@ public: void MoveItem(Item* item, sint32 inv_slot, int16 slot, int8 appearance_type, bool erase_old); // erase old was true bool MoveItem(sint32 to_bag_id, int16 from_index, sint8 to, int8 appearance_type, int8 charges); void EraseItem(Item* item); + Item* GetItemFromUniqueID(int32 item_id, bool include_bank = false, bool lock = true); + void SetVaultItemLockUniqueID(Client* client, int64 id, bool state, bool lock); + bool CanStoreSellItem(int64 unique_id, bool lock); + bool IsItemInSlotType(Item* item, InventorySlotType type, bool lockItems=true); + + void SetVaultItemUniqueIDCount(Client* client, int64 unique_id, int16 count, bool lock = true); + void RemoveVaultItemFromUniqueID(Client* client, int64 item_id, bool lock = true); + Item* GetVaultItemFromUniqueID(int64 item_id, bool lock = true); Item* GetItemFromID(int32 item_id, int8 count = 0, bool include_bank = false, bool lock = true); sint32 GetAllStackCountItemFromID(int32 item_id, int8 count = 0, bool include_bank = false, bool lock = true); - bool AssignItemToFreeSlot(Item* item); + bool AssignItemToFreeSlot(Item* item, bool inventory_only = true); int16 GetNumberOfFreeSlots(); int16 GetNumberOfItems(); int32 GetWeight(); @@ -1132,7 +1198,7 @@ public: void DestroyItem(int16 index); Item* CanStack(Item* item, bool include_bank = false); vector GetAllItemsFromID(int32 item, bool include_bank = false, bool lock = false); - void RemoveItem(Item* item, bool delete_item = false); + void RemoveItem(Item* item, bool delete_item = false, bool lock = true); bool AddItem(Item* item); Item* GetItem(sint32 bag_slot, int16 slot, int8 appearance_type = 0); @@ -1143,7 +1209,10 @@ public: map* GetAllItems(); bool HasFreeBankSlot(); int8 FindFreeBankSlot(); - + + void GetVaultItems(Client* client, int32 spawn_id, int8 maxSlots, bool isSelling = false); + void PopulateHouseStoragePacket(Client* client, PacketStruct* packet, Item* item, int16 itemIdx, int8 storage_flags); + ///Get the first free slot and store them in the provided variables ///Will contain the bag id of the first free spot ///Will contain the slot id of the first free slot diff --git a/source/WorldServer/Items/ItemsDB.cpp b/source/WorldServer/Items/ItemsDB.cpp index 8fdf45b..76c8b99 100644 --- a/source/WorldServer/Items/ItemsDB.cpp +++ b/source/WorldServer/Items/ItemsDB.cpp @@ -1,6 +1,6 @@ -/* +/* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifdef WIN32 #include #include @@ -172,6 +173,7 @@ void WorldDatabase::LoadDataFromRow(DatabaseResult* result, Item* item) item->generic_info.part_of_quest_id = result->GetInt32Str("part_of_quest_id"); item->details.recommended_level = result->GetInt16Str("recommended_level"); item->details.item_locked = false; + item->details.lock_flags = 0; item->generic_info.adventure_default_level = result->GetInt16Str("adventure_default_level"); item->generic_info.max_charges = result->GetInt16Str("max_charges"); item->generic_info.display_charges = result->GetInt8Str("display_charges"); @@ -541,7 +543,7 @@ int32 WorldDatabase::LoadHouseContainers(int32 item_id){ if (item) { LogWrite(ITEM__DEBUG, 5, "Items", "\tHouse Container for item_id %u", id); - LogWrite(ITEM__DEBUG, 5, "Items", "\tType: %i, '%i', '%u', '%i', '%i'", ITEM_TYPE_RECIPE, result.GetInt8Str("num_slots"), result.GetInt64Str("allowed_types"), result.GetInt8Str("broker_commission"), result.GetInt8Str("fence_commission")); + LogWrite(ITEM__DEBUG, 5, "Items", "\tType: %i, '%i', '%u', '%i', '%i'", ITEM_TYPE_HOUSE_CONTAINER, result.GetInt8Str("num_slots"), result.GetInt64Str("allowed_types"), result.GetInt8Str("broker_commission"), result.GetInt8Str("fence_commission")); item->SetItemType(ITEM_TYPE_HOUSE_CONTAINER); item->housecontainer_info->num_slots = result.GetInt8Str("num_slots"); @@ -549,6 +551,11 @@ int32 WorldDatabase::LoadHouseContainers(int32 item_id){ item->housecontainer_info->broker_commission = result.GetInt8Str("broker_commission"); item->housecontainer_info->fence_commission = result.GetInt8Str("fence_commission"); + item->details.num_slots = item->housecontainer_info->num_slots; + item->details.num_free_slots = item->housecontainer_info->num_slots; + item->bag_info->num_slots = item->housecontainer_info->num_slots; + item->bag_info->weight_reduction = 0; + total++; } else @@ -1097,18 +1104,19 @@ void WorldDatabase::LoadItemList(int32 item_id) LogWrite(ITEM__INFO, 0, "Items", "Loaded %u Total Item%s (took %u seconds)", total, ( total == 1 ) ? "" : "s", Timer::GetUnixTimeStamp() - t_now); } -int32 WorldDatabase::LoadNextUniqueItemID() +int64 WorldDatabase::LoadNextUniqueItemID() { Query query; MYSQL_ROW row; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT max(id) FROM character_items"); + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT NEXT VALUE FOR seq_character_items AS next_id"); if(result && (row = mysql_fetch_row(result))) { if(row[0]) { - LogWrite(ITEM__DEBUG, 0, "Items", "%s: max(id): %u", __FUNCTION__, atoul(row[0])); - return strtoul(row[0], NULL, 0); + int64 max_ = strtoull(row[0], NULL, 0); + LogWrite(ITEM__DEBUG, 0, "Items", "%s: max(id): %u", __FUNCTION__, max_); + return max_; } else return 0; @@ -1119,6 +1127,31 @@ int32 WorldDatabase::LoadNextUniqueItemID() return 0; } +void WorldDatabase::ResetNextUniqueItemID() +{ + Query query; + Query query2; + MYSQL_ROW row; + MYSQL_ROW row2; + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT next_not_cached_value FROM seq_character_items"); + MYSQL_RES* result2 = query2.RunQuery2(Q_SELECT, "SELECT COALESCE(MAX(id),0) + 1 FROM character_items"); + if(result && (row = mysql_fetch_row(result)) && result2 && (row2 = mysql_fetch_row(result2))) + { + if(row[0] && row2[0]) + { + int64 max_cur = strtoull(row[0], NULL, 0); + int64 max_expected = strtoull(row2[0], NULL, 0); + string update_item = string("ALTER SEQUENCE seq_character_items RESTART WITH %llu"); + if(max_cur < max_expected) + query.AddQueryAsync(0, this, Q_UPDATE, update_item.c_str(), max_expected); + + LogWrite(ITEM__DEBUG, 0, "Items", "%s: max(current): %u max(expected): %u", __FUNCTION__, max_cur, max_expected); + } + } + else if(!result) + LogWrite(ITEM__ERROR, 0, "Items", "%s: Unable to reset next unique item ID.", __FUNCTION__); +} + void WorldDatabase::SaveItems(Client* client) { LogWrite(ITEM__DEBUG, 3, "Items", "Save Items for Player %i", client->GetCharacterID()); @@ -1382,7 +1415,7 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe int8 remainder = item->details.count % 255; item->details.count = remainder; - if (item->details.inv_slot_id == -2) + if (item->details.inv_slot_id == InventorySlotType::OVERFLOW) player->item_list.AddOverflowItem(item); else { if(!player->item_list.AddItem(item)) @@ -1398,7 +1431,7 @@ void WorldDatabase::LoadCharacterItemList(int32 account_id, int32 char_id, Playe } } else { - if (item->details.inv_slot_id == -2) + if (item->details.inv_slot_id == InventorySlotType::OVERFLOW) player->item_list.AddOverflowItem(item); else player->item_list.AddItem(item); diff --git a/source/WorldServer/Items/Items_CoE.h b/source/WorldServer/Items/Items_CoE.h index 5786b7f..457c0f1 100644 --- a/source/WorldServer/Items/Items_CoE.h +++ b/source/WorldServer/Items/Items_CoE.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __EQ2_ITEMS__ #define __EQ2_ITEMS__ #include @@ -730,9 +731,7 @@ public: void AddItem(Item* item); bool IsBag(int32 item_id); void RemoveAll(); - static int32 NextUniqueID(); - static void ResetUniqueID(int32 new_id); - static int32 next_unique_id; + static int64 NextUniqueID(); }; class PlayerItemList { public: diff --git a/source/WorldServer/Items/Items_DoV.h b/source/WorldServer/Items/Items_DoV.h index 9bfc87e..7ce9a88 100644 --- a/source/WorldServer/Items/Items_DoV.h +++ b/source/WorldServer/Items/Items_DoV.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __EQ2_ITEMS__ #define __EQ2_ITEMS__ #include @@ -824,9 +825,7 @@ public: void AddItem(Item* item); bool IsBag(int32 item_id); void RemoveAll(); - static int32 NextUniqueID(); - static void ResetUniqueID(int32 new_id); - static int32 next_unique_id; + static int64 NextUniqueID(); }; class PlayerItemList { public: diff --git a/source/WorldServer/LuaFunctions.cpp b/source/WorldServer/LuaFunctions.cpp index c576e08..2b72136 100644 --- a/source/WorldServer/LuaFunctions.cpp +++ b/source/WorldServer/LuaFunctions.cpp @@ -1,22 +1,23 @@ -/* - EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) +/* + 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. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ + #include "LuaFunctions.h" #include "Spawn.h" #include "WorldDatabase.h" @@ -37,6 +38,7 @@ #include "ClientPacketFunctions.h" #include "Transmute.h" #include "Titles.h" +#include "./Broker/BrokerManager.h" #include #include #include @@ -60,6 +62,7 @@ extern MasterRaceTypeList race_types_list; extern MasterLanguagesList master_languages_list; extern MasterTitlesList master_titles_list; extern RuleManager rule_manager; +extern BrokerManager broker; vector ParseString(string strVal, char delim) { stringstream ss(strVal); @@ -9646,7 +9649,6 @@ int EQ2Emu_lua_CureByType(lua_State* state) { } else { ZoneServer* zone = spell->zone; - vector targets = spell->targets; vector targets_to_cure; for (int32 id : spell->GetTargets()) { target = zone->GetSpawnByID(id); @@ -9797,6 +9799,7 @@ int EQ2Emu_lua_StartHeroicOpportunity(lua_State* state) { Spawn* caster = lua_interface->GetSpawn(state); int8 class_id = lua_interface->GetInt8Value(state, 2); + Spawn* targetOverride = lua_interface->GetSpawn(state, 3); lua_interface->ResetFunctionStack(state); if (!caster) { @@ -9810,10 +9813,13 @@ int EQ2Emu_lua_StartHeroicOpportunity(lua_State* state) { } Spawn* target = caster->GetTarget(); - if (!target) { + if (!target && !targetOverride) { lua_interface->LogError("%s: LUA StartHeroicOpportunity command error: target is not valid", lua_interface->GetScriptName(state)); return 0; } + + if(!target) + target = targetOverride; Client* client = ((Player*)caster)->GetClient(); if (!client) { @@ -14656,3 +14662,103 @@ int EQ2Emu_lua_GetExpRequiredByLevel(lua_State* state) { lua_interface->SetInt32Value(state, exp_required); return 1; } + +int EQ2Emu_lua_ShowShopWindow(lua_State* state) { + + Spawn* player = lua_interface->GetSpawn(state); + Spawn* from_spawn = lua_interface->GetSpawn(state, 2); + lua_interface->ResetFunctionStack(state); + if (player) { + Client* client = 0; + if (player->IsPlayer()) + client = ((Player*)player)->GetClient(); + if (client) { + client->OpenShopWindow(from_spawn, true); + } + } + return 0; +} + +int EQ2Emu_lua_SetSpawnHouseScript(lua_State* state) { + + Spawn* target = lua_interface->GetSpawn(state); + string lua_script = lua_interface->GetStringValue(state, 2); + lua_interface->ResetFunctionStack(state); + if (target && target->GetDatabaseID() && !target->IsPlayer() && !target->IsBot()) { + database.UpdateHouseSpawnScript(target->GetDatabaseID(), lua_script); + bool scriptActive = false; + if (lua_interface && lua_interface->GetSpawnScript(lua_script.c_str()) != 0) { + scriptActive = true; + target->SetSpawnScript(lua_script); + } + if (scriptActive) { + target->GetZone()->CallSpawnScript(target, SPAWN_SCRIPT_SPAWN); + } + } + return 0; +} + +int EQ2Emu_lua_SendBook(lua_State* state) { + + Spawn* target = lua_interface->GetSpawn(state); + int32 item_id = lua_interface->GetInt32Value(state, 2); + lua_interface->ResetFunctionStack(state); + if (target && target->IsPlayer() && ((Player*)target)->GetClient()) { + Item* item = master_item_list.GetItem(item_id); + if (item) { + ((Player*)target)->GetClient()->SendShowBook(target, item->name, item->book_language, item->book_pages); + } + } + return 0; +} + +int EQ2Emu_lua_GetPickupItemID(lua_State* state) { + Spawn* spawn = lua_interface->GetSpawn(state); + lua_interface->ResetFunctionStack(state); + if (spawn) { + lua_interface->SetInt32Value(state, spawn->GetPickupItemID()); + return 1; + } + return 0; +} + +int EQ2Emu_lua_SetHouseCharacterID(lua_State* state) { + Spawn* spawn = lua_interface->GetSpawn(state); + int32 character_id = lua_interface->GetInt32Value(state, 2); + lua_interface->ResetFunctionStack(state); + if (spawn) { + if(!character_id) { + PlayerHouse* ph = world.GetPlayerHouseByInstanceID(spawn->GetZone()->GetInstanceID()); + if(ph) + spawn->SetHouseCharacterID(ph->character_id); + } + else { + spawn->SetHouseCharacterID(character_id); + } + } + return 0; +} + +int EQ2Emu_lua_GetHouseCharacterID(lua_State* state) { + Spawn* spawn = lua_interface->GetSpawn(state); + lua_interface->ResetFunctionStack(state); + if (spawn) { + lua_interface->SetInt32Value(state, spawn->GetHouseCharacterID()); + } + else { + lua_interface->SetInt32Value(state, 0); + } + return 1; +} + +int EQ2Emu_lua_ShowHouseShopMerchant(lua_State* state) { + Spawn* spawn = lua_interface->GetSpawn(state); + Spawn* player = lua_interface->GetSpawn(state, 2); + lua_interface->ResetFunctionStack(state); + if (spawn && player && player->IsPlayer()) { + if(player->GetClient()) + player->GetClient()->SendMerchantWindow(spawn, false); + } + + return 0; +} \ No newline at end of file diff --git a/source/WorldServer/LuaFunctions.h b/source/WorldServer/LuaFunctions.h index 6554693..4a025da 100644 --- a/source/WorldServer/LuaFunctions.h +++ b/source/WorldServer/LuaFunctions.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef LUA_FUNCTIONS_H #define LUA_FUNCTIONS_H @@ -684,4 +685,12 @@ int EQ2Emu_lua_GetZonePlayerFirstLevel(lua_State* state); int EQ2Emu_lua_GetSpellRequiredLevel(lua_State* state); int EQ2Emu_lua_GetExpRequiredByLevel(lua_State* state); + +int EQ2Emu_lua_ShowShopWindow(lua_State* state); +int EQ2Emu_lua_SetSpawnHouseScript(lua_State* state); +int EQ2Emu_lua_SendBook(lua_State* state); +int EQ2Emu_lua_GetPickupItemID(lua_State* state); +int EQ2Emu_lua_SetHouseCharacterID(lua_State* state); +int EQ2Emu_lua_GetHouseCharacterID(lua_State* state); +int EQ2Emu_lua_ShowHouseShopMerchant(lua_State* state); #endif \ No newline at end of file diff --git a/source/WorldServer/LuaInterface.cpp b/source/WorldServer/LuaInterface.cpp index faf1c14..17a37e8 100644 --- a/source/WorldServer/LuaInterface.cpp +++ b/source/WorldServer/LuaInterface.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -1824,6 +1824,14 @@ void LuaInterface::RegisterFunctions(lua_State* state) { lua_register(state,"GetSpellRequiredLevel", EQ2Emu_lua_GetSpellRequiredLevel); lua_register(state,"GetExpRequiredByLevel", EQ2Emu_lua_GetExpRequiredByLevel); + + lua_register(state,"ShowShopWindow", EQ2Emu_lua_ShowShopWindow); + lua_register(state,"SetSpawnHouseScript", EQ2Emu_lua_SetSpawnHouseScript); + lua_register(state,"SendBook", EQ2Emu_lua_SendBook); + lua_register(state,"GetPickupItemID", EQ2Emu_lua_GetPickupItemID); + lua_register(state,"SetHouseCharacterID", EQ2Emu_lua_SetHouseCharacterID); + lua_register(state,"GetHouseCharacterID", EQ2Emu_lua_GetHouseCharacterID); + lua_register(state,"ShowHouseShopMerchant", EQ2Emu_lua_ShowHouseShopMerchant); } void LuaInterface::LogError(const char* error, ...) { diff --git a/source/WorldServer/LuaInterface.h b/source/WorldServer/LuaInterface.h index 51d2892..032e20a 100644 --- a/source/WorldServer/LuaInterface.h +++ b/source/WorldServer/LuaInterface.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef LUA_INTERFACE_H #define LUA_INTERFACE_H @@ -161,7 +162,16 @@ struct LuaSpell{ std::shared_lock lock(targets_mutex); return std::find(targets.begin(), targets.end(), id) != targets.end(); } - + + bool HasAnyTarget(const std::vector& ids) const { + std::shared_lock lock(targets_mutex); + return std::any_of( + ids.begin(), ids.end(), + [this](int32 id){ + return std::find(targets.begin(), targets.end(), id) != targets.end(); + } + ); + } std::vector GetRemovedTargets() const { std::shared_lock lock(targets_mutex); diff --git a/source/WorldServer/NPC.cpp b/source/WorldServer/NPC.cpp index e13d0ea..41123cf 100644 --- a/source/WorldServer/NPC.cpp +++ b/source/WorldServer/NPC.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "NPC.h" #include "WorldDatabase.h" #include @@ -108,6 +109,8 @@ NPC::NPC(NPC* old_npc){ SetLootDropType(old_npc->GetLootDropType()); has_spells = old_npc->HasSpells(); SetScaredByStrongPlayers(old_npc->IsScaredByStrongPlayers()); + if(old_npc->GetSpawnScriptSetDB() && old_npc->GetSpawnScript()) + SetSpawnScript(std::string(old_npc->GetSpawnScript())); } } diff --git a/source/WorldServer/NPC_AI.cpp b/source/WorldServer/NPC_AI.cpp index 861ab6b..1e41f48 100644 --- a/source/WorldServer/NPC_AI.cpp +++ b/source/WorldServer/NPC_AI.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -271,13 +271,13 @@ void Brain::AddHate(Entity* entity, sint32 hate) { } } -void Brain::ClearHate() { +void Brain::ClearHate(bool lockSpawnList) { // Lock the hate list, we are altering the list so use a write lock MHateList.writelock(__FUNCTION__, __LINE__); map::iterator itr; for (itr = m_hatelist.begin(); itr != m_hatelist.end(); itr++) { - Spawn* spawn = m_body->GetZone()->GetSpawnByID(itr->first); + Spawn* spawn = m_body->GetZone()->GetSpawnByID(itr->first, lockSpawnList); if (spawn && spawn->IsEntity()) { ((Entity*)spawn)->MHatedBy.lock(); diff --git a/source/WorldServer/NPC_AI.h b/source/WorldServer/NPC_AI.h index cbb8166..4532cdd 100644 --- a/source/WorldServer/NPC_AI.h +++ b/source/WorldServer/NPC_AI.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __NPC_AI_H__ #define __NPC_AI_H__ #include "NPC.h" @@ -59,7 +60,7 @@ public: /// The amount of hate to add virtual void AddHate(Entity* entity, sint32 hate); /// Completely clears the hate list for this npc - void ClearHate(); + void ClearHate(bool lockSpawnList = true); /// Removes the given entity from this NPC's hate list /// Entity to remove from this NPC's hate list void ClearHate(Entity* entity); diff --git a/source/WorldServer/Object.cpp b/source/WorldServer/Object.cpp index 971e789..c53bcad 100644 --- a/source/WorldServer/Object.cpp +++ b/source/WorldServer/Object.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "World.h" #include "Object.h" #include "Spells.h" @@ -95,5 +96,7 @@ Object* Object::Copy(){ new_spawn->SetOmittedByDBFlag(IsOmittedByDBFlag()); new_spawn->SetLootTier(GetLootTier()); new_spawn->SetLootDropType(GetLootDropType()); + if(GetSpawnScriptSetDB() && GetSpawnScript()) + new_spawn->SetSpawnScript(std::string(GetSpawnScript())); return new_spawn; } diff --git a/source/WorldServer/Player.cpp b/source/WorldServer/Player.cpp index 322e3c4..2476f17 100644 --- a/source/WorldServer/Player.cpp +++ b/source/WorldServer/Player.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -137,6 +137,7 @@ Player::Player(){ active_drink_unique_id = 0; raidsheet_changed = false; hassent_raid = false; + house_vault_slots = 0; } Player::~Player(){ SetSaveSpellEffects(true); @@ -953,7 +954,7 @@ EQ2Packet* PlayerInfo::serialize(int16 version, int16 modifyPos, int32 modifyVal packet->setDataByName("rain2", info_struct->get_wind()); //-102.24); packet->setDataByName("status_points", info_struct->get_status_points()); packet->setDataByName("guild_status", 888888); - + packet->setDataByName("vault_slots", player->GetHouseVaultSlots()); if (house_zone_id > 0){ string house_name = database.GetZoneName(house_zone_id); if(house_name.length() > 0) @@ -1484,7 +1485,7 @@ vector Player::UnequipItem(int16 index, sint32 bag_id, int8 slot, in SetCharSheetChanged(true); SetEquipment(0, equip_slot_id ? equip_slot_id : old_slot); } - else if (item_list.AssignItemToFreeSlot(item)) { + else if (item_list.AssignItemToFreeSlot(item, true)) { if(appearance_type) database.DeleteItem(GetCharacterID(), item, "APPEARANCE"); else @@ -1645,8 +1646,8 @@ vector Player::UnequipItem(int16 index, sint32 bag_id, int8 slot, in } } else { - if ((bag_id == 0 && slot < NUM_INV_SLOTS) || (bag_id == -3 && slot < NUM_BANK_SLOTS) || (bag_id == -4 && slot < NUM_SHARED_BANK_SLOTS)) { - if (bag_id == -4 && item->CheckFlag(NO_TRADE)) { + if ((bag_id == 0 && slot < NUM_INV_SLOTS) || (bag_id == InventorySlotType::BANK && slot < NUM_BANK_SLOTS) || (bag_id == InventorySlotType::SHARED_BANK && slot < NUM_SHARED_BANK_SLOTS) || (bag_id == InventorySlotType::HOUSE_VAULT && slot < GetHouseVaultSlots())) { + if ((bag_id == InventorySlotType::SHARED_BANK || bag_id == InventorySlotType::HOUSE_VAULT) && !item_list.SharedBankAddAllowed(item)) { PacketStruct* packet = configReader.getStruct("WS_DisplayText", version); if (packet) { packet->setDataByName("color", CHANNEL_COLOR_YELLOW); @@ -2018,7 +2019,7 @@ vector Player::EquipItem(int16 index, int16 version, int8 appearance const char* zone_script = world.GetZoneScript(GetZone()->GetZoneID()); if (zone_script && lua_interface) lua_interface->RunZoneScript(zone_script, "item_equipped", GetZone(), this, item->details.item_id, item->name.c_str(), 0, item->details.unique_id); - int32 bag_id = item->details.inv_slot_id; + sint32 bag_id = item->details.inv_slot_id; if (item->generic_info.condition == 0) { Client* client = GetClient(); if (client) { @@ -2108,7 +2109,7 @@ bool Player::AddItem(Item* item, AddItemType type) { safe_delete(item); return false; } - else if (item_list.AssignItemToFreeSlot(item)) { + else if (item_list.AssignItemToFreeSlot(item, true)) { item->save_needed = true; CalculateApplyWeight(); return true; @@ -2160,7 +2161,7 @@ void Player::UpdateInventory(int32 bag_id) { EQ2Packet* Player::MoveInventoryItem(sint32 to_bag_id, int16 from_index, int8 new_slot, int8 charges, int8 appearance_type, bool* item_deleted, int16 version) { Item* item = item_list.GetItemFromIndex(from_index); - bool isOverflow = ((item != nullptr) && (item->details.inv_slot_id == -2)); + bool isOverflow = ((item != nullptr) && (item->details.inv_slot_id == InventorySlotType::OVERFLOW)); int8 result = item_list.MoveItem(to_bag_id, from_index, new_slot, appearance_type, charges); if (result == 1) { if(isOverflow && item->details.inv_slot_id != -2) { @@ -3434,6 +3435,11 @@ void Player::AddMaintainedSpell(LuaSpell* luaspell){ if(!luaspell) return; + if(luaspell->spell->GetSpellData()->not_maintained || luaspell->spell->GetSpellData()->duration1 == 0) { + LogWrite(PLAYER__INFO, 0, "NPC", "AddMaintainedSpell Spell ID: %u, Concentration: %u disallowed, not_maintained true (%u) or duration is 0 (%u).", luaspell->spell->GetSpellData()->id, luaspell->spell->GetSpellData()->req_concentration, luaspell->spell->GetSpellData()->not_maintained, luaspell->spell->GetSpellData()->duration1); + return; + } + Spell* spell = luaspell->spell; MaintainedEffects* effect = GetFreeMaintainedSpellSlot(); int32 target_type = 0; @@ -7458,8 +7464,8 @@ void Player::SaveSpellEffects() savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_custom_spell_display where charid=%u", GetCharacterID()); savedEffects.AddQueryAsync(GetCharacterID(), &database, Q_DELETE, "delete from character_custom_spell_data where charid=%u", GetCharacterID()); InfoStruct* info = GetInfoStruct(); - MSpellEffects.readlock(__FUNCTION__, __LINE__); MMaintainedSpells.readlock(__FUNCTION__, __LINE__); + MSpellEffects.readlock(__FUNCTION__, __LINE__); for(int i = 0; i < 45; i++) { if(info->spell_effects[i].spell_id != 0xFFFFFFFF) { @@ -7492,7 +7498,8 @@ void Player::SaveSpellEffects() SaveCustomSpellDataIndex(info->spell_effects[i].spell); SaveCustomSpellEffectsDisplay(info->spell_effects[i].spell); } - if (i < NUM_MAINTAINED_EFFECTS && info->maintained_effects[i].spell_id != 0xFFFFFFFF){ + if (i < NUM_MAINTAINED_EFFECTS && info->maintained_effects[i].spell && info->maintained_effects[i].spell_id != 0xFFFFFFFF){ + LogWrite(PLAYER__INFO, 0, "Player", "Saving slot %u maintained effect %u", i, info->maintained_effects[i].spell_id); Spawn* spawn = GetZone()->GetSpawnByID(info->maintained_effects[i].spell->initial_target); int32 target_char_id = 0; @@ -7582,8 +7589,8 @@ void Player::SaveSpellEffects() } } } - MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__); MSpellEffects.releasereadlock(__FUNCTION__, __LINE__); + MMaintainedSpells.releasereadlock(__FUNCTION__, __LINE__); } void Player::MentorTarget() diff --git a/source/WorldServer/Player.h b/source/WorldServer/Player.h index b0324f0..305fa58 100644 --- a/source/WorldServer/Player.h +++ b/source/WorldServer/Player.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -225,7 +225,7 @@ struct QuickBarItem{ int16 icon_type; int32 id; int8 tier; - int32 unique_id; + int64 unique_id; EQ2_16BitString text; }; @@ -1103,8 +1103,11 @@ public: void SetActiveFoodUniqueID(int32 unique_id, bool update_db = true); void SetActiveDrinkUniqueID(int32 unique_id, bool update_db = true); - int32 GetActiveFoodUniqueID() { return active_food_unique_id; } - int32 GetActiveDrinkUniqueID() { return active_drink_unique_id; } + int64 GetActiveFoodUniqueID() { return active_food_unique_id; } + int64 GetActiveDrinkUniqueID() { return active_drink_unique_id; } + + void SetHouseVaultSlots(int8 allowed_slots) { house_vault_slots = allowed_slots; } + int8 GetHouseVaultSlots() { return house_vault_slots; } Mutex MPlayerQuests; float pos_packet_speed; @@ -1264,8 +1267,10 @@ private: Quest* GetAnyQuest(int32 quest_id); Quest* GetCompletedQuest(int32 quest_id); - std::atomic active_food_unique_id; - std::atomic active_drink_unique_id; + std::atomic active_food_unique_id; + std::atomic active_drink_unique_id; + + int8 house_vault_slots; }; #pragma pack() #endif diff --git a/source/WorldServer/PlayerGroups.cpp b/source/WorldServer/PlayerGroups.cpp index c1251d0..f34efd1 100644 --- a/source/WorldServer/PlayerGroups.cpp +++ b/source/WorldServer/PlayerGroups.cpp @@ -1,21 +1,21 @@ -/* -EQ2Emulator: Everquest II Server Emulator -Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) +/* + 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. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ #include "PlayerGroups.h" @@ -34,6 +34,7 @@ extern ZoneList zone_list; extern RuleManager rule_manager; extern PeerManager peer_manager; extern WorldDatabase database; +extern LuaInterface* lua_interface; /******************************************************** PlayerGroup ********************************************************/ PlayerGroup::PlayerGroup(int32 id) { @@ -1094,6 +1095,7 @@ void PlayerGroupManager::UpdateGroupBuffs() { caster = *vitr; caster->GetMaintainedMutex()->readlock(__FUNCTION__, __LINE__); // go through the player's maintained spells + bool skipSpell = false; me = caster->GetMaintainedSpells(); for (i = 0; i < NUM_MAINTAINED_EFFECTS; i++) { if (me[i].spell_id == 0xFFFFFFFF) @@ -1113,9 +1115,7 @@ void PlayerGroupManager::UpdateGroupBuffs() { if (spell && spell->GetSpellData()->group_spell && spell->GetSpellData()->friendly_spell && (spell->GetSpellData()->target_type == SPELL_TARGET_GROUP_AE || spell->GetSpellData()->target_type == SPELL_TARGET_RAID_AE)) { - luaspell->ClearCharTargets(); - for (target_itr = group->GetMembers()->begin(); target_itr != group->GetMembers()->end(); target_itr++) { group_member = (*target_itr)->member; @@ -1127,8 +1127,39 @@ void PlayerGroupManager::UpdateGroupBuffs() { client = (*target_itr)->client; + LuaSpell* conflictSpell = caster->HasLinkedTimerID(luaspell, group_member, false, true); + if(conflictSpell && group_member && group_member->IsEntity()) + { + if(conflictSpell->spell->GetSpellData()->min_class_skill_req > 0 && spell->GetSpellData()->min_class_skill_req > 0) + { + if(conflictSpell->spell->GetSpellData()->min_class_skill_req <= spell->GetSpellData()->min_class_skill_req) + { + if(spell->GetSpellData()->duration_until_cancel && !luaspell->num_triggers) + { + for (int32 id : conflictSpell->GetTargets()) { + Spawn* tmpTarget = caster->GetZone()->GetSpawnByID(id); + if(tmpTarget && tmpTarget->IsEntity()) + { + ((Entity*)tmpTarget)->RemoveEffectsFromLuaSpell(conflictSpell); + caster->GetZone()->RemoveTargetFromSpell(conflictSpell, tmpTarget, false); + caster->GetZone()->GetSpellProcess()->CheckRemoveTargetFromSpell(conflictSpell); + lua_interface->RemoveSpawnFromSpell(conflictSpell, tmpTarget); + } + } + } + } + else + { + // this is a spell that is no good, have to abort! + caster->GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + caster->GetZone()->GetSpellProcess()->SpellCannotStack(caster->GetZone(), client, caster, luaspell, conflictSpell); + skipSpell = true; + break; + } + } + } has_effect = false; - + if (group_member->GetSpellEffect(spell->GetSpellID(), caster)) { has_effect = true; } @@ -1226,13 +1257,18 @@ void PlayerGroupManager::UpdateGroupBuffs() { client->QueuePacket(packet); } } - - luaspell->SwapTargets(new_target_list); - SpellProcess::AddSelfAndPet(luaspell, caster); - new_target_list.clear(); + if(!skipSpell) { + luaspell->SwapTargets(new_target_list); + SpellProcess::AddSelfAndPet(luaspell, caster); + new_target_list.clear(); + } + else + break; } } - caster->GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); + + if(!skipSpell) + caster->GetMaintainedMutex()->releasereadlock(__FUNCTION__, __LINE__); } } } diff --git a/source/WorldServer/Recipes/Recipe.cpp b/source/WorldServer/Recipes/Recipe.cpp index b005209..6608506 100644 --- a/source/WorldServer/Recipes/Recipe.cpp +++ b/source/WorldServer/Recipes/Recipe.cpp @@ -1,6 +1,6 @@ -/* +/* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include #include "../../common/debug.h" #include "../../common/Log.h" @@ -734,7 +735,12 @@ bool Recipe::PlayerHasComponentByItemID(Client* client, vector* player_co cur_qty = track_req_qty; track_req_qty -= cur_qty; - itemss[i]->details.item_locked = true; + if(!itemss[i]->TryLockItem(LockReason::LockReason_Crafting)) { + for (int8 s = 0; s < i; s++) { + itemss[i]->TryUnlockItem(LockReason::LockReason_Crafting); + } + return false; + } player_component_pair_qty->push_back({itemss[i]->details.unique_id, cur_qty}); player_components->push_back(itemss[i]); if(have_qty >= required_qty) diff --git a/source/WorldServer/Sign.cpp b/source/WorldServer/Sign.cpp index 66b7ec1..d9c3170 100644 --- a/source/WorldServer/Sign.cpp +++ b/source/WorldServer/Sign.cpp @@ -1,22 +1,23 @@ /* -EQ2Emulator: Everquest II Server Emulator -Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + 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. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ + #include "Sign.h" #include "../common/ConfigReader.h" @@ -152,6 +153,8 @@ Sign* Sign::Copy(){ new_spawn->SetLootTier(GetLootTier()); new_spawn->SetLootDropType(GetLootDropType()); new_spawn->SetLanguage(GetLanguage()); + if(GetSpawnScriptSetDB() && GetSpawnScript()) + new_spawn->SetSpawnScript(std::string(GetSpawnScript())); return new_spawn; } diff --git a/source/WorldServer/Spawn.cpp b/source/WorldServer/Spawn.cpp index b269d52..6c21372 100644 --- a/source/WorldServer/Spawn.cpp +++ b/source/WorldServer/Spawn.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -120,6 +120,7 @@ Spawn::Spawn(){ has_spawn_proximities = false; pickup_item_id = 0; pickup_unique_item_id = 0; + house_character_id = 0; disable_sounds = false; has_quests_required = false; has_history_required = false; @@ -153,6 +154,7 @@ Spawn::Spawn(){ respawn_offset_high = 0; duplicated_spawn = true; ResetKnockedBack(); + spawn_script_setdb = false; } Spawn::~Spawn(){ @@ -1925,8 +1927,9 @@ const char* Spawn::GetSpawnScript(){ return 0; } -void Spawn::SetSpawnScript(string name){ +void Spawn::SetSpawnScript(string name, bool db_set){ spawn_script = name; + spawn_script_setdb = db_set; } void Spawn::SetPrimaryCommand(const char* name, const char* command, float distance){ diff --git a/source/WorldServer/Spawn.h b/source/WorldServer/Spawn.h index e3e6a6a..542151b 100644 --- a/source/WorldServer/Spawn.h +++ b/source/WorldServer/Spawn.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __EQ2_SPAWN__ #define __EQ2_SPAWN__ @@ -1089,8 +1090,9 @@ public: bool HasProvidedQuests(){ return (quest_ids.size() > 0); } - void SetSpawnScript(string name); + void SetSpawnScript(string name, bool db_set = false); const char* GetSpawnScript(); + bool GetSpawnScriptSetDB() { return spawn_script_setdb; } vector* GetSpawnGroup(); bool HasSpawnGroup(); @@ -1321,9 +1323,15 @@ public: { pickup_unique_item_id = uniqueid; } + + void SetHouseCharacterID(int32 charid) + { + house_character_id = charid; + } int32 GetPickupItemID() { return pickup_item_id; } int32 GetPickupUniqueItemID() { return pickup_unique_item_id; } + int32 GetHouseCharacterID() { return house_character_id; } bool IsSoundsDisabled() { return disable_sounds; } void SetSoundsDisabled(bool val) { disable_sounds = val; } @@ -1468,6 +1476,7 @@ protected: int32 transporter_id; int32 pickup_item_id; int32 pickup_unique_item_id; + int32 house_character_id; map* > required_quests; map required_history; @@ -1506,6 +1515,7 @@ private: int tmp_action_state; int32 running_to; string spawn_script; + bool spawn_script_setdb; static int32 next_id; ZoneServer* zone; int32 spawn_location_id; diff --git a/source/WorldServer/SpellProcess.cpp b/source/WorldServer/SpellProcess.cpp index 7c90a91..230beb7 100644 --- a/source/WorldServer/SpellProcess.cpp +++ b/source/WorldServer/SpellProcess.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "SpellProcess.h" #include "../common/Log.h" #include "Tradeskills/Tradeskills.h" @@ -433,10 +434,15 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi bool ret = false; Spawn* target = 0; bool target_valid = false; + if(spell) { + LogWrite(SPELL__INFO, 0, "Spell", "SpellProcess::DeleteCasterSpell Spell: %s, Reason: %s, RemoveTarget: %s.", + spell->spell->GetName(), reason.c_str(), remove_target ? "Yes" : "All"); if(remove_target) { for (int32 id : spell->GetTargets()) { if(remove_target->GetID() == id) { + LogWrite(SPELL__INFO, 0, "Spell", "SpellProcess::DeleteCasterSpell RemoveTarget Spell: %s, Reason: %s, Target: %s.", + spell->spell->GetName(), reason.c_str(), remove_target->GetName()); if(remove_target->IsEntity()){ spell->RemoveTarget(remove_target->GetID()); lua_interface->RemoveSpawnFromSpell(spell, remove_target); @@ -485,13 +491,19 @@ bool SpellProcess::DeleteCasterSpell(LuaSpell* spell, string reason, bool removi CheckRemoveTargetFromSpell(spell, removing_all_spells, removing_all_spells); ZoneServer* zone = spell->zone; if(zone) { + LogWrite(SPELL__DEBUG, 0, "Spell", "SpellProcess::DeleteCasterSpell RemoveTargets Spell: %s.", + spell->spell->GetName()); for (int32 id : spell->GetTargets()) { target = zone->GetSpawnByID(id); if(target && target->IsEntity()){ + LogWrite(SPELL__INFO, 0, "Spell", "SpellProcess::DeleteCasterSpell RemoveTargets Spell: %s, Reason: %s, CurrentTarget: %s (%u).", + spell->spell->GetName(), reason.c_str(), target->GetName(), id); spell->RemoveTarget(target->GetID()); lua_interface->RemoveSpawnFromSpell(spell, target); } else{ + LogWrite(SPELL__INFO, 0, "Spell", "SpellProcess::DeleteCasterSpell RemoveTarget Spell: %s, Reason: %s, CurrentTarget: ??Unknown??.", + spell->spell->GetName(), reason.c_str()); spell->RemoveTarget(spell->caster->GetID()); lua_interface->RemoveSpawnFromSpell(spell, spell->caster); } @@ -637,6 +649,7 @@ bool SpellProcess::CastInstant(Spell* spell, Entity* caster, Entity* target, boo lua_spell->initial_target = target->GetID(); lua_spell->initial_target_char_id = (target && target->IsPlayer()) ? ((Player*)target)->GetCharacterID() : 0; GetSpellTargets(lua_spell); + PrintTargets(lua_spell, "instant"); if (!lua_spell->spell->IsCopiedSpell()) { @@ -1019,6 +1032,23 @@ Spell* SpellProcess::GetSpell(Entity* caster){ return spell; } +void SpellProcess::PrintTargets(LuaSpell* spell, std::string stage) { + std::vector targets = spell->GetTargets(); + if(spell->zone) { + for (int32_t id : targets) { + Spawn* tmp = spell->zone->GetSpawnByID(id); + if(tmp) { + LogWrite(SPELL__INFO, 0, "Spell", "SpellProcess::PrintTarget(%s) Spell: %s, Spawn: %s (%u) (DBID: %u).", + stage.c_str(), spell->spell->GetName(), tmp->GetName(), tmp->GetID(), tmp->GetDatabaseID()); + } + else { + LogWrite(SPELL__ERROR, 0, "Spell", "SpellProcess::PrintTarget(%s) Spell: %s SpawnID: %u - ??Unknown??.", + stage.c_str(), spell->spell->GetName(), id); + } + } + } +} + void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster, Spawn* target, bool lock, bool harvest_spell, LuaSpell* customSpell, int16 custom_cast_time, bool in_heroic_opp) { if((customSpell != 0 || spell != 0) && caster) @@ -1173,6 +1203,8 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster, return; } + PrintTargets(lua_spell, "cast"); + LuaSpell* conflictSpell = caster->HasLinkedTimerID(lua_spell, target, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT)); if(conflictSpell) { @@ -1185,18 +1217,17 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster, if(spell->GetSpellData()->friendly_spell) { ZoneServer* zone = caster->GetZone(); - Spawn* tmpTarget = zone->GetSpawnByID(conflictSpell->initial_target); - if(tmpTarget && tmpTarget->IsEntity()) - { - ((Entity*)tmpTarget)->RemoveEffectsFromLuaSpell(conflictSpell); - zone->RemoveTargetFromSpell(conflictSpell, tmpTarget, false); - CheckRemoveTargetFromSpell(conflictSpell); - lua_interface->RemoveSpawnFromSpell(conflictSpell, tmpTarget); - if(client && IsReady(conflictSpell->spell, client->GetPlayer())) - UnlockSpell(client, conflictSpell->spell); + + for (int32 id : conflictSpell->GetTargets()) { + Spawn* tmpTarget = zone->GetSpawnByID(id); + if(tmpTarget && tmpTarget->IsEntity()) + { + ((Entity*)tmpTarget)->RemoveEffectsFromLuaSpell(conflictSpell); + zone->RemoveTargetFromSpell(conflictSpell, tmpTarget, false); + CheckRemoveTargetFromSpell(conflictSpell); + lua_interface->RemoveSpawnFromSpell(conflictSpell, tmpTarget); + } } - DeleteSpell(lua_spell); - return; } else if(lua_spell->spell->GetSpellData()->spell_type == SPELL_TYPE_DEBUFF) { @@ -1205,11 +1236,11 @@ void SpellProcess::ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster, } } } - else - { - SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell); - return; - } + else + { + SpellCannotStack(zone, client, lua_spell->caster, lua_spell, conflictSpell); + return; + } } } @@ -1717,8 +1748,8 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her } } - if(client && client->GetCurrentZone() && - !spell->spell->GetSpellData()->friendly_spell) + PrintTargets(spell, "castprocess"); + if(client && client->GetCurrentZone()) { ZoneServer* zone = client->GetCurrentZone(); Spawn* tmpTarget = zone->GetSpawnByID(spell->initial_target); @@ -1726,8 +1757,30 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her LuaSpell* conflictSpell = spell->caster->HasLinkedTimerID(spell, tmpTarget, (spell_type == SPELL_TYPE_DD || spell_type == SPELL_TYPE_DOT)); if(conflictSpell && tmpTarget && tmpTarget->IsEntity()) { - ((Entity*)tmpTarget)->RemoveSpellEffect(conflictSpell); - zone->RemoveTargetFromSpell(conflictSpell, tmpTarget); + if(conflictSpell->spell->GetSpellData()->min_class_skill_req > 0 && spell->spell->GetSpellData()->min_class_skill_req > 0) + { + if(conflictSpell->spell->GetSpellData()->min_class_skill_req <= spell->spell->GetSpellData()->min_class_skill_req) + { + if(spell->spell->GetSpellData()->duration_until_cancel && !spell->num_triggers) + { + for (int32 id : conflictSpell->GetTargets()) { + Spawn* tmpTarget = zone->GetSpawnByID(id); + if(tmpTarget && tmpTarget->IsEntity()) + { + ((Entity*)tmpTarget)->RemoveEffectsFromLuaSpell(conflictSpell); + zone->RemoveTargetFromSpell(conflictSpell, tmpTarget, false); + CheckRemoveTargetFromSpell(conflictSpell); + lua_interface->RemoveSpawnFromSpell(conflictSpell, tmpTarget); + } + } + } + } + else + { + SpellCannotStack(zone, client, spell->caster, spell, conflictSpell); + return false; + } + } } } @@ -1813,19 +1866,37 @@ bool SpellProcess::CastProcessedSpell(LuaSpell* spell, bool passive, bool in_her } continue; } - if (firstTarget && !spell->spell->GetSpellData()->not_maintained) { - spell->caster->AddMaintainedSpell(spell); - } - firstTarget = false; SpellEffects* effect = ((Entity*)target)->GetSpellEffect(spell->spell->GetSpellID()); - if (effect && effect->tier > spell->spell->GetSpellTier()) { + + if(!effect) + effect = ((Entity*)target)->GetSpellEffectWithLinkedTimer(spell->spell->GetSpellID(), spell->spell->GetSpellData()->linked_timer, spell->spell->GetSpellData()->type_group_spell_id, spell->caster, true); + + if (effect && effect->spell != spell && effect->tier > spell->spell->GetSpellTier()) { if(client) { spell->caster->GetZone()->SendSpellFailedPacket(client, SPELL_ERROR_TAKE_EFFECT_MOREPOWERFUL); client->SimpleMessage(CHANNEL_COLOR_YELLOW, "The spell did not take hold as the target already has a better spell of this type."); + + if (!passive) + SendFinishedCast(spell, client); + + if(spell->zone) + spell->zone->GetSpellProcess()->RemoveSpellScriptTimerBySpell(spell); + DeleteActiveSpell(spell, true); + return false; } } else{ + if(effect && effect->spell != spell) { + if(effect->spell->zone) + effect->spell->zone->GetSpellProcess()->RemoveSpellScriptTimerBySpell(effect->spell); + + DeleteActiveSpell(effect->spell, true); + } + if (firstTarget) { + spell->caster->AddMaintainedSpell(spell); + } + firstTarget = false; ((Entity*)target)->AddSpellEffect(spell); if(spell->spell->GetSpellData()->det_type > 0) ((Entity*)target)->AddDetrimentalSpell(spell); @@ -2251,6 +2322,8 @@ void SpellProcess::GetSpellTargets(LuaSpell* luaspell) } else // default self cast for group/raid AE { + if(caster->IsPlayer()) + GetPlayerGroupTargets((Player*)caster, caster, luaspell, true); target = caster; luaspell->initial_target = caster->GetID(); luaspell->initial_target_char_id = (caster && caster->IsPlayer()) ? ((Player*)caster)->GetCharacterID() : 0; @@ -2818,8 +2891,9 @@ void SpellProcess::CheckRemoveTargetFromSpell(LuaSpell* spell, bool allow_delete for (int32 id : spell->GetTargets()) { if (remove_spawn->GetID() == id) { found_target = true; + spell->RemoveTarget(id); - LogWrite(SPELL__DEBUG, 0, "Spell", "%s CheckRemoveTargetFromSpell %s (%u).", spell->spell->GetName(), remove_spawn->GetName(), remove_spawn->GetID()); + LogWrite(SPELL__DEBUG, 0, "Spell", "SpellProcess::CheckRemoveTargetFromSpell Spell: %s, Spawn: %s (%u).", spell->spell->GetName(), remove_spawn->GetName(), remove_spawn->GetID()); if(remove_spawn && std::find(spawnsToRemove.begin(), spawnsToRemove.end(), remove_spawn) == spawnsToRemove.end()) spawnsToRemove.push_back(remove_spawn); break; @@ -3035,6 +3109,16 @@ void SpellProcess::AddSelfAndPetToCharTargets(LuaSpell* spell, Spawn* caster, bo void SpellProcess::DeleteActiveSpell(LuaSpell* spell, bool skipRemoveCurrent) { lua_interface->SetLuaUserDataStale(spell); + if(spell->zone) { + for (int32 id : spell->GetTargets()) { + Spawn* target = spell->zone->GetSpawnByID(id); + if(target) + lua_interface->RemoveSpawnFromSpell(spell, target); + } + } + if (spell->caster) { + ((Entity*)spell->caster)->RemoveMaintainedSpell(spell); + } if(!skipRemoveCurrent) { lua_interface->RemoveCurrentSpell(spell->state, spell, true); lua_interface->DeletePendingSpell(spell); @@ -3052,7 +3136,21 @@ bool SpellProcess::AddLuaSpellTarget(LuaSpell* lua_spell, int32 id, bool lock_sp lua_spell->AddTarget(id); ret = true; } - + + if(lua_spell->zone) { + Spawn* target = lua_spell->zone->GetSpawnByID(id); + if(target && target->IsPlayer()){ + lua_spell->AddCharIDTarget(((Player*)target)->GetCharacterID(), 0); + if (((Entity*)target)->HasPet()){ + Entity* pet = ((Entity*)target)->GetPet(); + if (pet) + lua_spell->AddCharIDTarget(((Player*)target)->GetCharacterID(), PET_TYPE_COMBAT); + pet = ((Entity*)target)->GetCharmedPet(); + if (pet) + lua_spell->AddCharIDTarget(((Player*)target)->GetCharacterID(), PET_TYPE_CHARMED); + } + } + } return ret; } diff --git a/source/WorldServer/SpellProcess.h b/source/WorldServer/SpellProcess.h index da38ea0..2afb327 100644 --- a/source/WorldServer/SpellProcess.h +++ b/source/WorldServer/SpellProcess.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef __EQ2_SPELL_PROCESS__ #define __EQ2_SPELL_PROCESS__ #include @@ -182,7 +183,8 @@ public: /// ??? not currently used /// Is this a harvest spell? void ProcessSpell(ZoneServer* zone, Spell* spell, Entity* caster, Spawn* target = 0, bool lock = true, bool harvest_spell = false, LuaSpell* customSpell = 0, int16 custom_cast_time = 0, bool in_heroic_opp = false); - + void PrintTargets(LuaSpell* spell, std::string stage); + /// Cast an EntityCommand (right click menu) /// The current ZoneServer /// the EntityCommand to cast diff --git a/source/WorldServer/Tradeskills/Tradeskills.cpp b/source/WorldServer/Tradeskills/Tradeskills.cpp index c875bdf..5b9636c 100644 --- a/source/WorldServer/Tradeskills/Tradeskills.cpp +++ b/source/WorldServer/Tradeskills/Tradeskills.cpp @@ -1,6 +1,6 @@ -/* +/* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include #include @@ -253,6 +254,7 @@ void TradeskillMgr::BeginCrafting(Client* client, vector> comp // TODO: use the vecotr to lock inventory slots vector>::iterator itr; + vector>::iterator itr2; bool missingItem = false; int32 itemid = 0; vector tmpItems; @@ -266,7 +268,16 @@ void TradeskillMgr::BeginCrafting(Client* client, vector> comp break; } - item->details.item_locked = true; + if(!item->TryLockItem(LockReason::LockReason_Crafting)) { + for (itr = components.begin(); itr != components.end(); itr++) { + itemid = itr->first; + Item* item = client->GetPlayer()->item_list.GetItemFromUniqueID(itemid); + if(item) + item->TryUnlockItem(LockReason::LockReason_Crafting); + } + missingItem = true; + break; + } tmpItems.push_back(item); } @@ -280,7 +291,8 @@ void TradeskillMgr::BeginCrafting(Client* client, vector> comp vector::iterator itemitr; for (itemitr = tmpItems.begin(); itemitr != tmpItems.end(); itemitr++) { Item* tmpItem = *itemitr; - tmpItem->details.item_locked = false; + if(tmpItem) + tmpItem->TryUnlockItem(LockReason::LockReason_Crafting); } ClientPacketFunctions::StopCrafting(client); return; @@ -358,13 +370,13 @@ void TradeskillMgr::StopCrafting(Client* client, bool lock) { item = client->GetPlayer()->item_list.GetItemFromUniqueID(itmid); if (item && item->details.count <= qty) { - item->details.item_locked = false; + item->TryUnlockItem(LockReason::LockReason_Crafting); client->GetPlayer()->item_list.RemoveItem(item); updateInvReq = true; } else if(item) { item->details.count -= qty; - item->details.item_locked = false; + item->TryUnlockItem(LockReason::LockReason_Crafting); item->save_needed = true; updateInvReq = true; } diff --git a/source/WorldServer/Web/PeerManager.cpp b/source/WorldServer/Web/PeerManager.cpp index 28a5c1e..713094b 100644 --- a/source/WorldServer/Web/PeerManager.cpp +++ b/source/WorldServer/Web/PeerManager.cpp @@ -1,21 +1,21 @@ -/* -EQ2Emu: Everquest II Server Emulator -Copyright (C) 2007-2025 EQ2Emu Development Team (https://www.eq2emu.com) +/* + 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 EQ2Emu. + This file is part of EQ2Emulator. -EQ2Emu 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 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. -EQ2Emu 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. + 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 EQ2Emu. If not, see . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ #include "PeerManager.h" @@ -855,4 +855,84 @@ void PeerManager::sendPeersActiveQuery(int32 character_id, bool remove) { peer_https_pool.sendPostRequestToPeerAsync(peer->id, peer->webAddr, std::to_string(peer->webPort), "/activequery", jsonPayload); } +} + +void PeerManager::sendPeersAddSeller(int32 character_id, int32 house_id, std::string name, bool saleEnabled, bool invEnabled) { + boost::property_tree::ptree root; + root.put("character_id", character_id); + root.put("house_id", house_id); + root.put("name", name); + root.put("sale_enabled", saleEnabled); + root.put("inventory_enabled", invEnabled); + + std::ostringstream jsonStream; + boost::property_tree::write_json(jsonStream, root); + std::string jsonPayload = jsonStream.str(); + LogWrite(PEERING__DEBUG, 0, "Peering", "%s: Add Seller, CharID: %u", __FUNCTION__, character_id); + + for (const auto& [id, peer] : peers) { + if (peer->healthCheck.status != HealthStatus::OK) + continue; + peer_https_pool.sendPostRequestToPeerAsync(peer->id, peer->webAddr, std::to_string(peer->webPort), "/addseller", jsonPayload); + } +} + +void PeerManager::sendPeersRemoveSeller(int32 character_id) { + boost::property_tree::ptree root; + root.put("character_id", character_id); + + std::ostringstream jsonStream; + boost::property_tree::write_json(jsonStream, root); + std::string jsonPayload = jsonStream.str(); + LogWrite(PEERING__DEBUG, 0, "Peering", "%s: Remove Seller, CharID: %u", __FUNCTION__, character_id); + + for (const auto& [id, peer] : peers) { + if (peer->healthCheck.status != HealthStatus::OK) + continue; + peer_https_pool.sendPostRequestToPeerAsync(peer->id, peer->webAddr, std::to_string(peer->webPort), "/removeseller", jsonPayload); + } +} + +void PeerManager::sendPeersAddItemSale(int32 character_id, int32 house_id, int32 itemID, int64 uniqueID, int64 price, sint32 invSlotID, int16 slotID, int16 count, bool inInventory, bool forSale, std::string itemCreator) { + boost::property_tree::ptree root; + root.put("character_id", character_id); + root.put("house_id", house_id); + root.put("item_creator", itemCreator); + root.put("item_id", itemID); + root.put("unique_id", uniqueID); + root.put("price", price); + root.put("inv_slot_id", invSlotID); + root.put("slot_id", slotID); + root.put("count", count); + root.put("in_inventory", inInventory); + root.put("for_sale", forSale); + + std::ostringstream jsonStream; + boost::property_tree::write_json(jsonStream, root); + std::string jsonPayload = jsonStream.str(); + LogWrite(PEERING__DEBUG, 0, "Peering", "%s: Add Item Sale, CharID: %u, UniqueID: %u, ItemID: %u", __FUNCTION__, character_id, uniqueID, itemID); + + for (const auto& [id, peer] : peers) { + if (peer->healthCheck.status != HealthStatus::OK) + continue; + peer_https_pool.sendPostRequestToPeerAsync(peer->id, peer->webAddr, std::to_string(peer->webPort), "/additemsale", jsonPayload); + } +} + + +void PeerManager::sendPeersRemoveItemSale(int32 character_id, int64 uniqueID) { + boost::property_tree::ptree root; + root.put("character_id", character_id); + root.put("unique_id", uniqueID); + + std::ostringstream jsonStream; + boost::property_tree::write_json(jsonStream, root); + std::string jsonPayload = jsonStream.str(); + LogWrite(PEERING__DEBUG, 0, "Peering", "%s: Remove Item Sale, CharID: %u, UniqueID: %u", __FUNCTION__, character_id, uniqueID); + + for (const auto& [id, peer] : peers) { + if (peer->healthCheck.status != HealthStatus::OK) + continue; + peer_https_pool.sendPostRequestToPeerAsync(peer->id, peer->webAddr, std::to_string(peer->webPort), "/removeitemsale", jsonPayload); + } } \ No newline at end of file diff --git a/source/WorldServer/Web/PeerManager.h b/source/WorldServer/Web/PeerManager.h index 90488a7..0bb27ff 100644 --- a/source/WorldServer/Web/PeerManager.h +++ b/source/WorldServer/Web/PeerManager.h @@ -1,21 +1,21 @@ -/* -EQ2Emu: Everquest II Server Emulator -Copyright (C) 2007-2025 EQ2Emu Development Team (https://www.eq2emu.com) +/* + 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 EQ2Emu. + This file is part of EQ2Emulator. -EQ2Emu 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 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. -EQ2Emu 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. + 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 EQ2Emu. If not, see . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ #ifndef PEERMANAGER_H @@ -225,6 +225,12 @@ public: void sendZonePlayerList(std::vector* queries, std::vector* peer_list, bool isGM); bool GetClientGuildDetails(int32 matchCharID, GuildMember* member_details); + + + void sendPeersAddSeller(int32 character_id, int32 house_id, std::string name, bool saleEnabled, bool invEnabled); + void sendPeersRemoveSeller(int32 character_id); + void sendPeersAddItemSale(int32 character_id, int32 house_id, int32 itemID, int64 uniqueID, int64 price, sint32 invSlotID, int16 slotID, int16 count, bool inInventory, bool forSale, std::string itemCreator); + void sendPeersRemoveItemSale(int32 character_id, int64 uniqueID); }; diff --git a/source/WorldServer/Web/WorldWeb.cpp b/source/WorldServer/Web/WorldWeb.cpp index c39e8fb..bc38e52 100644 --- a/source/WorldServer/Web/WorldWeb.cpp +++ b/source/WorldServer/Web/WorldWeb.cpp @@ -1,21 +1,21 @@ -/* -EQ2Emu: Everquest II Server Emulator -Copyright (C) 2007-2025 EQ2Emu Development Team (https://www.eq2emu.com) +/* + 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 EQ2Emu. + This file is part of EQ2Emulator. -EQ2Emu 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 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. -EQ2Emu 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. + 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 EQ2Emu. If not, see . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ #include "HTTPSClientPool.h" @@ -24,6 +24,7 @@ along with EQ2Emu. If not, see . #include "../LoginServer.h" #include "../LuaInterface.h" #include "../Guilds/Guild.h" +#include "../Broker/BrokerManager.h" #include #include @@ -44,6 +45,7 @@ extern MasterSpellList master_spell_list; extern MasterFactionList master_faction_list; extern ClientList client_list; extern GuildList guild_list; +extern BrokerManager broker; PeerManager peer_manager; HTTPSClientPool peer_https_pool; @@ -1409,4 +1411,175 @@ void World::Web_worldhandle_activequery(const http::request& std::string json = oss.str(); res.body() = json; res.prepare_payload(); +} + +void World::Web_worldhandle_addseller(const http::request& req, http::response& res) { + res.set(http::field::content_type, "application/json; charset=utf-8"); + boost::property_tree::ptree pt, json_tree; + + std::istringstream json_stream(req.body()); + boost::property_tree::read_json(json_stream, json_tree); + std::string playerName(""); + int32 houseID = 0, charID = 0; + bool saleEnabled = false, invEnabled = false, success = false; + if (auto character_id = json_tree.get_optional("character_id")) { + charID = character_id.get(); + } + if (auto name = json_tree.get_optional("name")) { + playerName = name.get(); + } + if (auto house_id = json_tree.get_optional("house_id")) { + houseID = house_id.get(); + } + if (auto sale_enabled = json_tree.get_optional("sale_enabled")) { + saleEnabled = sale_enabled.get(); + } + if (auto inventory_enabled = json_tree.get_optional("inventory_enabled")) { + invEnabled = inventory_enabled.get(); + } + + if(charID) { + broker.AddSeller(charID, playerName, houseID, saleEnabled, invEnabled); + success = true; + } + + pt.put("success", success); + pt.put("character_id", charID); + std::ostringstream oss; + boost::property_tree::write_json(oss, pt); + std::string json = oss.str(); + res.body() = json; + res.prepare_payload(); +} + +void World::Web_worldhandle_removeseller(const http::request& req, http::response& res) { + res.set(http::field::content_type, "application/json; charset=utf-8"); + boost::property_tree::ptree pt, json_tree; + + std::istringstream json_stream(req.body()); + boost::property_tree::read_json(json_stream, json_tree); + int32 charID = 0; + bool success = false; + if (auto character_id = json_tree.get_optional("character_id")) { + charID = character_id.get(); + } + + if(charID) { + broker.RemoveSeller(charID, true); + success = true; + } + + pt.put("success", success); + pt.put("character_id", charID); + std::ostringstream oss; + boost::property_tree::write_json(oss, pt); + std::string json = oss.str(); + res.body() = json; + res.prepare_payload(); +} + +void World::Web_worldhandle_additemsale(const http::request& req, http::response& res) { + res.set(http::field::content_type, "application/json; charset=utf-8"); + boost::property_tree::ptree pt, json_tree; + + std::istringstream json_stream(req.body()); + boost::property_tree::read_json(json_stream, json_tree); + int32 houseID = 0, charID = 0; + bool success = false; + int32 itemID = 0; + int64 uniqueID = 0, inPrice = 0; + sint32 invSlotID = 0; + int16 slotID = 0, inCount = 0; + bool inInventory = false; + bool forSale = false; + std::string itemCreator(""); + if (auto character_id = json_tree.get_optional("character_id")) { + charID = character_id.get(); + } + if (auto house_id = json_tree.get_optional("house_id")) { + houseID = house_id.get(); + } + if (auto item_id = json_tree.get_optional("item_id")) { + itemID = item_id.get(); + } + if (auto unique_id = json_tree.get_optional("unique_id")) { + uniqueID = unique_id.get(); + } + if (auto price = json_tree.get_optional("price")) { + inPrice = price.get(); + } + if (auto inv_slot_id = json_tree.get_optional("inv_slot_id")) { + invSlotID = inv_slot_id.get(); + } + if (auto slot_id = json_tree.get_optional("slot_id")) { + slotID = slot_id.get(); + } + if (auto count = json_tree.get_optional("count")) { + inCount = count.get(); + } + if (auto in_inventory = json_tree.get_optional("in_inventory")) { + inInventory = in_inventory.get(); + } + if (auto for_sale = json_tree.get_optional("for_sale")) { + forSale = for_sale.get(); + } + if (auto item_creator = json_tree.get_optional("item_creator")) { + itemCreator = item_creator.get(); + } + + + if(charID && uniqueID && itemID) { + SaleItem it{}; + it.unique_id = uniqueID; + it.character_id = charID; + it.house_id = houseID; + it.item_id = itemID; + it.cost_copper = inPrice; + it.for_sale = forSale; + it.inv_slot_id = invSlotID; + it.slot_id = slotID; + it.count = inCount; + it.from_inventory = inInventory; + it.creator = itemCreator; + broker.AddItem(it, true); + success = true; + } + + pt.put("success", success); + pt.put("character_id", charID); + std::ostringstream oss; + boost::property_tree::write_json(oss, pt); + std::string json = oss.str(); + res.body() = json; + res.prepare_payload(); +} + +void World::Web_worldhandle_removeitemsale(const http::request& req, http::response& res) { + res.set(http::field::content_type, "application/json; charset=utf-8"); + boost::property_tree::ptree pt, json_tree; + + std::istringstream json_stream(req.body()); + boost::property_tree::read_json(json_stream, json_tree); + int32 charID = 0; + int64 uniqueID = 0; + bool success = false; + if (auto character_id = json_tree.get_optional("character_id")) { + charID = character_id.get(); + } + if (auto unique_id = json_tree.get_optional("unique_id")) { + uniqueID = unique_id.get(); + } + + if(charID && uniqueID) { + broker.OnPeerRemoveItem(charID, uniqueID); + success = true; + } + + pt.put("success", success); + pt.put("character_id", charID); + std::ostringstream oss; + boost::property_tree::write_json(oss, pt); + std::string json = oss.str(); + res.body() = json; + res.prepare_payload(); } \ No newline at end of file diff --git a/source/WorldServer/Widget.cpp b/source/WorldServer/Widget.cpp index 37444ca..774c175 100644 --- a/source/WorldServer/Widget.cpp +++ b/source/WorldServer/Widget.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -167,6 +167,8 @@ Widget* Widget::Copy(){ new_spawn->SetOpenZ(GetOpenZ()); new_spawn->SetMultiFloorLift(multi_floor_lift); new_spawn->SetSoundsDisabled(IsSoundsDisabled()); + if(GetSpawnScriptSetDB() && GetSpawnScript()) + new_spawn->SetSpawnScript(std::string(GetSpawnScript())); return new_spawn; } @@ -417,6 +419,7 @@ void Widget::HandleUse(Client* client, string command, int8 overrideWidgetType){ ClientPacketFunctions::SendHouseVisitWindow(client, world.GetAllPlayerHousesByHouseID(hz->id)); ClientPacketFunctions::SendBaseHouseWindow(client, hz, ph, id); + client->GetPlayer()->SetTarget(this); } } } @@ -445,6 +448,7 @@ void Widget::HandleUse(Client* client, string command, int8 overrideWidgetType){ ClientPacketFunctions::SendBaseHouseWindow(client, hz, ph, id); client->GetCurrentZone()->SendHouseItems(client); + client->GetPlayer()->SetTarget(this); } else { if (hz) diff --git a/source/WorldServer/World.cpp b/source/WorldServer/World.cpp index e2283f1..51ad5e4 100644 --- a/source/WorldServer/World.cpp +++ b/source/WorldServer/World.cpp @@ -283,6 +283,11 @@ void World::init(std::string web_ipaddr, int16 web_port, std::string cert_file, world_webserver->register_route("/peerstatus", World::Web_worldhandle_peerstatus); world_webserver->register_route("/activequery", World::Web_worldhandle_activequery); + + world_webserver->register_route("/addseller", World::Web_worldhandle_addseller); + world_webserver->register_route("/removeseller", World::Web_worldhandle_removeseller); + world_webserver->register_route("/additemsale", World::Web_worldhandle_additemsale); + world_webserver->register_route("/removeitemsale", World::Web_worldhandle_removeitemsale); world_webserver->run(); LogWrite(INIT__INFO, 0, "Init", "World Web Server is listening on %s:%u..", web_ipaddr.c_str(), web_port); web_success = true; @@ -2641,6 +2646,7 @@ void World::AddPlayerHouse(int32 char_id, int32 house_id, int64 unique_id, int32 ph->escrow_status = escrow_status; ph->upkeep_due = upkeep_due; ph->player_name = player_name; + ph->character_id = char_id; ReloadHouseData(ph); m_playerHouses[house_id][char_id] = ph; } @@ -3709,4 +3715,5 @@ void ZoneInfoMemory::LoadFromDatabaseRow(MYSQL_ROW row) { canGate = atoul(row[24]); cityZone = atoul(row[25]); canEvac = atoul(row[26]); + zoneLuaScript = (row[27] != nullptr) ? row[27] : ""; } \ No newline at end of file diff --git a/source/WorldServer/World.h b/source/WorldServer/World.h index f3bbca3..8c6e2c3 100644 --- a/source/WorldServer/World.h +++ b/source/WorldServer/World.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -229,6 +229,7 @@ struct PlayerHouse { int64 escrow_coins; int32 escrow_status; string player_name; + int32 character_id; list deposits; map depositsMap; list history; @@ -425,6 +426,7 @@ public: std::string zoneDescription; std::string zoneMotd; std::string zoneSkyFile; + std::string zoneLuaScript; float underworld; float safeX, safeY, safeZ, safeHeading; int16 minimumLevel, maximumLevel, minimumVersion; @@ -738,6 +740,10 @@ public: static void Web_worldhandle_setguildeventfilter(const http::request& req, http::response& res); static void Web_worldhandle_peerstatus(const http::request& req, http::response& res); static void Web_worldhandle_activequery(const http::request& req, http::response& res); + static void Web_worldhandle_addseller(const http::request& req, http::response& res); + static void Web_worldhandle_removeseller(const http::request& req, http::response& res); + static void Web_worldhandle_additemsale(const http::request& req, http::response& res); + static void Web_worldhandle_removeitemsale(const http::request& req, http::response& res); static void Web_populate_status(boost::property_tree::ptree& pt); diff --git a/source/WorldServer/WorldDatabase.cpp b/source/WorldServer/WorldDatabase.cpp index f2eefcf..627419c 100644 --- a/source/WorldServer/WorldDatabase.cpp +++ b/source/WorldServer/WorldDatabase.cpp @@ -1,21 +1,21 @@ /* -EQ2Emulator: Everquest II Server Emulator -Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + 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. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ #include @@ -53,6 +53,7 @@ along with EQ2Emulator. If not, see . #include "SpellProcess.h" #include "races.h" #include "Web/PeerManager.h" +#include "Broker/BrokerManager.h" extern Classes classes; extern Commands commands; @@ -75,6 +76,7 @@ extern RuleManager rule_manager; extern MasterLanguagesList master_languages_list; extern ChestTrapList chest_trap_list; extern PeerManager peer_manager; +BrokerManager broker; //devn00b: Fix for linux builds since we dont use stricmp we use strcasecmp #if defined(__GNUC__) @@ -953,23 +955,29 @@ int32 WorldDatabase::LoadAppearances(ZoneServer* zone, Client* client){ return count; } -void WorldDatabase::LoadNPCs(ZoneServer* zone){ +void WorldDatabase::LoadNPCs(ZoneServer* zone, bool isInstanceType){ Query query; MYSQL_ROW row; NPC* npc = 0; int32 id = 0; int32 total = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT,"SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, npc.water_type, npc.flying_type, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str\n" - "FROM spawn s\n" - "INNER JOIN spawn_npcs npc\n" + + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + + MYSQL_RES* result = query.RunQuery2(Q_SELECT,"SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, npc.water_type, npc.flying_type, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_npcs%s npc\n" "ON s.id = npc.spawn_id\n" - "INNER JOIN spawn_location_entry le\n" + "INNER JOIN spawn_location_entry%s le\n" "ON npc.spawn_id = le.spawn_id\n" - "INNER JOIN spawn_location_placement lp\n" + "INNER JOIN spawn_location_placement%s lp\n" "ON le.spawn_location_id = lp.spawn_location_id\n" "WHERE lp.zone_id = %u and (lp.instance_id = 0 or lp.instance_id = %u)\n" "GROUP BY s.id", - zone->GetZoneID(), zone->GetInstanceID()); + houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), zone->GetZoneID(), zone->GetInstanceID()); while(result && (row = mysql_fetch_row(result))){ /*npc->SetAppearanceID(atoi(row[12])); AppearanceData* appearance = world.GetNPCAppearance(npc->GetAppearanceID()); @@ -1128,6 +1136,11 @@ void WorldDatabase::LoadNPCs(ZoneServer* zone){ npc->GetInfoStruct()->set_action_state(action_state_str); } + if(row[87]){ + std::string lua_script = std::string(row[87]); + npc->SetSpawnScript(lua_script, true); + } + zone->AddNPC(id, npc); total++; LogWrite(NPC__DEBUG, 5, "NPC", "---Loading NPC: '%s' (%u)", npc->appearance.name, id); @@ -1243,23 +1256,27 @@ void WorldDatabase::LoadSpiritShards(ZoneServer* zone){ LogWrite(NPC__INFO, 0, "NPC", "--Loaded %i Spirit Shard(s).", total); } -void WorldDatabase::LoadSigns(ZoneServer* zone){ +void WorldDatabase::LoadSigns(ZoneServer* zone, bool isInstanceType){ Query query; MYSQL_ROW row; Sign* sign = 0; int32 id = 0; int32 total = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT ss.spawn_id, s.name, s.model_type, s.size, s.show_command_icon, ss.widget_id, ss.widget_x, ss.widget_y, ss.widget_z, s.command_primary, s.command_secondary, s.collision_radius, ss.icon, ss.type, ss.title, ss.description, ss.sign_distance, ss.zone_id, ss.zone_x, ss.zone_y, ss.zone_z, ss.zone_heading, ss.include_heading, ss.include_location, s.transport_id, s.size_offset, s.display_hand_icon, s.visual_state, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, ss.language\n" - "FROM spawn s\n" - "INNER JOIN spawn_signs ss\n" + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT ss.spawn_id, s.name, s.model_type, s.size, s.show_command_icon, ss.widget_id, ss.widget_x, ss.widget_y, ss.widget_z, s.command_primary, s.command_secondary, s.collision_radius, ss.icon, ss.type, ss.title, ss.description, ss.sign_distance, ss.zone_id, ss.zone_x, ss.zone_y, ss.zone_z, ss.zone_heading, ss.include_heading, ss.include_location, s.transport_id, s.size_offset, s.display_hand_icon, s.visual_state, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, ss.language, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_signs%s ss\n" "ON s.id = ss.spawn_id\n" - "INNER JOIN spawn_location_entry le\n" + "INNER JOIN spawn_location_entry%s le\n" "ON ss.spawn_id = le.spawn_id\n" - "INNER JOIN spawn_location_placement lp\n" + "INNER JOIN spawn_location_placement%s lp\n" "ON le.spawn_location_id = lp.spawn_location_id\n" "WHERE lp.zone_id = %u and (lp.instance_id = 0 or lp.instance_id = %u)\n" "GROUP BY s.id", - zone->GetZoneID(), zone->GetInstanceID()); + houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), zone->GetZoneID(), zone->GetInstanceID()); while(result && (row = mysql_fetch_row(result))){ int32 signXpackFlag = atoul(row[28]); @@ -1330,6 +1347,11 @@ void WorldDatabase::LoadSigns(ZoneServer* zone){ sign->SetLanguage(atoul(row[36])); + if(row[37]){ + std::string lua_script = std::string(row[37]); + sign->SetSpawnScript(lua_script, true); + } + zone->AddSign(id, sign); total++; @@ -1339,23 +1361,28 @@ void WorldDatabase::LoadSigns(ZoneServer* zone){ LogWrite(SIGN__DEBUG, 0, "Sign", "--Loaded %i Sign(s)", total); } -void WorldDatabase::LoadWidgets(ZoneServer* zone){ +void WorldDatabase::LoadWidgets(ZoneServer* zone, bool isInstanceType){ Query query; MYSQL_ROW row; Widget* widget = 0; int32 id = 0; int32 total = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT sw.spawn_id, s.name, s.model_type, s.size, s.show_command_icon, sw.widget_id, sw.widget_x, sw.widget_y, sw.widget_z, s.command_primary, s.command_secondary, s.collision_radius, sw.include_heading, sw.include_location, sw.icon, sw.type, sw.open_heading, sw.open_y, sw.action_spawn_id, sw.open_sound_file, sw.close_sound_file, sw.open_duration, sw.closed_heading, sw.linked_spawn_id, sw.close_y, s.transport_id, s.size_offset, sw.house_id, sw.open_x, sw.open_z, sw.close_x, sw.close_z, s.display_hand_icon, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type\n" - "FROM spawn s\n" - "INNER JOIN spawn_widgets sw\n" + + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT sw.spawn_id, s.name, s.model_type, s.size, s.show_command_icon, sw.widget_id, sw.widget_x, sw.widget_y, sw.widget_z, s.command_primary, s.command_secondary, s.collision_radius, sw.include_heading, sw.include_location, sw.icon, sw.type, sw.open_heading, sw.open_y, sw.action_spawn_id, sw.open_sound_file, sw.close_sound_file, sw.open_duration, sw.closed_heading, sw.linked_spawn_id, sw.close_y, s.transport_id, s.size_offset, sw.house_id, sw.open_x, sw.open_z, sw.close_x, sw.close_z, s.display_hand_icon, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_widgets%s sw\n" "ON s.id = sw.spawn_id\n" - "INNER JOIN spawn_location_entry le\n" + "INNER JOIN spawn_location_entry%s le\n" "ON sw.spawn_id = le.spawn_id\n" - "INNER JOIN spawn_location_placement lp\n" + "INNER JOIN spawn_location_placement%s lp\n" "ON le.spawn_location_id = lp.spawn_location_id\n" "WHERE lp.zone_id = %u and (lp.instance_id = 0 or lp.instance_id = %u)\n" "GROUP BY s.id", - zone->GetZoneID(), zone->GetInstanceID()); + houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), zone->GetZoneID(), zone->GetInstanceID()); while(result && (row = mysql_fetch_row(result))){ int32 widgetXpackFlag = atoul(row[33]); int32 widgetHolidayFlag = atoul(row[34]); @@ -1437,7 +1464,12 @@ void WorldDatabase::LoadWidgets(ZoneServer* zone){ widget->SetLootTier(atoul(row[39])); widget->SetLootDropType(atoul(row[40])); - + + if(row[41]){ + std::string lua_script = std::string(row[41]); + widget->SetSpawnScript(lua_script, true); + } + zone->AddWidget(id, widget); total++; @@ -1447,23 +1479,28 @@ void WorldDatabase::LoadWidgets(ZoneServer* zone){ LogWrite(WIDGET__DEBUG, 0, "Widget", "--Loaded %i Widget(s)", total); } -void WorldDatabase::LoadObjects(ZoneServer* zone){ +void WorldDatabase::LoadObjects(ZoneServer* zone, bool isInstanceType){ Query query; MYSQL_ROW row; Object* object = 0; int32 id = 0; int32 total = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT so.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, s.transport_id, s.size_offset, so.device_id, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type\n" - "FROM spawn s\n" - "INNER JOIN spawn_objects so\n" + + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT so.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, s.transport_id, s.size_offset, so.device_id, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_objects%s so\n" "ON s.id = so.spawn_id\n" - "INNER JOIN spawn_location_entry le\n" + "INNER JOIN spawn_location_entry%s le\n" "ON so.spawn_id = le.spawn_id\n" - "INNER JOIN spawn_location_placement lp\n" + "INNER JOIN spawn_location_placement%s lp\n" "ON le.spawn_location_id = lp.spawn_location_id\n" "WHERE lp.zone_id = %u and (lp.instance_id = 0 or lp.instance_id = %u)\n" - "GROUP BY s.id", - zone->GetZoneID(), zone->GetInstanceID()); + "GROUP BY s.id", + houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), zone->GetZoneID(), zone->GetInstanceID()); while(result && (row = mysql_fetch_row(result))){ @@ -1519,6 +1556,11 @@ void WorldDatabase::LoadObjects(ZoneServer* zone){ object->SetLootDropType(atoul(row[26])); + if(row[27]){ + std::string lua_script = std::string(row[27]); + object->SetSpawnScript(lua_script, true); + } + zone->AddObject(id, object); total++; @@ -1528,23 +1570,28 @@ void WorldDatabase::LoadObjects(ZoneServer* zone){ LogWrite(OBJECT__DEBUG, 0, "Object", "--Loaded %i Object(s)", total); } -void WorldDatabase::LoadGroundSpawns(ZoneServer* zone){ +void WorldDatabase::LoadGroundSpawns(ZoneServer* zone, bool isInstanceType){ Query query; MYSQL_ROW row; GroundSpawn* spawn = 0; int32 id = 0; int32 total = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT sg.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, sg.number_harvests, sg.num_attempts_per_harvest, sg.groundspawn_id, sg.collection_skill, s.size_offset, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.aaxp_rewards, s.loot_tier, s.loot_drop_type\n" - "FROM spawn s\n" - "INNER JOIN spawn_ground sg\n" + + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT sg.spawn_id, s.name, s.race, s.model_type, s.command_primary, s.command_secondary, s.targetable, s.size, s.show_name, s.visual_state, s.attackable, s.show_level, s.show_command_icon, s.display_hand_icon, s.faction_id, s.collision_radius, sg.number_harvests, sg.num_attempts_per_harvest, sg.groundspawn_id, sg.collection_skill, s.size_offset, s.expansion_flag, s.holiday_flag, s.disable_sounds, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_ground%s sg\n" "ON s.id = sg.spawn_id\n" - "INNER JOIN spawn_location_entry le\n" + "INNER JOIN spawn_location_entry%s le\n" "ON sg.spawn_id = le.spawn_id\n" - "INNER JOIN spawn_location_placement lp\n" + "INNER JOIN spawn_location_placement%s lp\n" "ON le.spawn_location_id = lp.spawn_location_id\n" "WHERE lp.zone_id = %u and (lp.instance_id = 0 or lp.instance_id = %u)\n" "GROUP BY s.id", - zone->GetZoneID(), zone->GetInstanceID()); + houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), houseTable.c_str(), zone->GetZoneID(), zone->GetInstanceID()); while(result && (row = mysql_fetch_row(result))){ int32 gsXpackFlag = atoul(row[21]); @@ -1601,6 +1648,11 @@ void WorldDatabase::LoadGroundSpawns(ZoneServer* zone){ spawn->SetLootDropType(atoul(row[26])); + if(row[27]){ + std::string lua_script = std::string(row[27]); + spawn->SetSpawnScript(lua_script, true); + } + zone->AddGroundSpawn(id, spawn); total++; LogWrite(GROUNDSPAWN__DEBUG, 5, "GSpawn", "---Loading GroundSpawn: '%s' (%u)", spawn->appearance.name, id); @@ -3551,6 +3603,8 @@ void WorldDatabase::LoadSpawns(ZoneServer* zone) LogWrite(SPAWN__TRACE, 0, "Spawn", "Enter LoadSpawns"); + int8 isInstanceType = (zone->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + if(zone->GetInstanceID() == 0) { npcs = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_npcs sn where sn.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%u and slp.instance_id=%u ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_NPC); objects = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_objects so where so.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%u and slp.instance_id=%u ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_OBJECT); @@ -3561,6 +3615,19 @@ void WorldDatabase::LoadSpawns(ZoneServer* zone) spawn_group_associations = LoadSpawnLocationGroupAssociations(zone); spawn_group_chances = LoadSpawnGroupChances(zone); } + else if(isInstanceType) { + npcs = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement_houses slp, spawn_location_name_houses sln, spawn_location_entry_houses sle, spawn_npcs_houses sn where sn.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_NPC); + objects = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement_houses slp, spawn_location_name_houses sln, spawn_location_entry_houses sle, spawn_objects_houses so where so.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_OBJECT); + widgets = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement_houses slp, spawn_location_name_houses sln, spawn_location_entry_houses sle, spawn_widgets_houses sw where sw.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_WIDGET); + signs = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement_houses slp, spawn_location_name_houses sln, spawn_location_entry_houses sle, spawn_signs_houses ss where ss.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_SIGN); + ground_spawns = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement_houses slp, spawn_location_name_houses sln, spawn_location_entry_houses sle, spawn_ground_houses sg where sg.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_GROUNDSPAWN); + + npcs += ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_npcs sn where sn.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_NPC); + objects += ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_objects so where so.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_OBJECT); + widgets += ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_widgets sw where sw.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_WIDGET); + signs += ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_signs ss where ss.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_SIGN); + ground_spawns += ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_ground sg where sg.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_GROUNDSPAWN); + } else { npcs = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_npcs sn where sn.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_NPC); objects = ProcessSpawnLocations(zone, "SELECT sln.id, sle.id, slp.x, slp.y, slp.z, slp.x_offset, slp.y_offset, slp.z_offset, slp.heading, sle.spawn_id, sle.spawnpercentage, slp.respawn, slp.respawn_offset_low, slp.respawn_offset_high, slp.duplicated_spawn, slp.grid_id, slp.id, slp.expire_timer, slp.expire_offset, slp.pitch, slp.roll, sle.condition, slp.lvl_override, slp.hp_override, slp.mp_override, slp.str_override, slp.sta_override, slp.wis_override, slp.int_override, slp.agi_override, slp.heat_override, slp.cold_override, slp.magic_override, slp.mental_override, slp.divine_override, slp.disease_override, slp.poison_override, difficulty_override FROM spawn_location_placement slp, spawn_location_name sln, spawn_location_entry sle, spawn_objects so where so.spawn_id = sle.spawn_id and sln.id = sle.spawn_location_id and sln.id = slp.spawn_location_id and slp.zone_id=%i and (slp.instance_id = 0 or slp.instance_id=%u) ORDER BY sln.id, sle.id", SPAWN_ENTRY_TYPE_OBJECT); @@ -3570,17 +3637,22 @@ void WorldDatabase::LoadSpawns(ZoneServer* zone) spawn_groups = LoadSpawnLocationGroups(zone); spawn_group_associations = LoadSpawnLocationGroupAssociations(zone); spawn_group_chances = LoadSpawnGroupChances(zone); - } - LogWrite(SPAWN__INFO, 0, "Spawn", "Loaded for zone '%s' (%u):\n\t%u NPC(s), %u Object(s), %u Widget(s)\n\t%u Sign(s), %u Ground Spawn(s), %u Spawn Group(s)\n\t%u Spawn Group Association(s), %u Spawn Group Chance(s)", zone->GetZoneName(), zone->GetZoneID(), npcs, objects, widgets, signs, ground_spawns, spawn_groups, spawn_group_associations, spawn_group_chances); + LogWrite(SPAWN__INFO, 0, "Spawn", "Loaded for zone '%s' (%u):\n\t%u NPC(s), %u Object(s), %u Widget(s)\n\t%u Sign(s), %u Ground Spawn(s), %u Spawn Group(s)\n\t%u Spawn Group Association(s), %u Spawn Group Chance(s), %u InstanceHouse", zone->GetZoneName(), zone->GetZoneID(), npcs, objects, widgets, signs, ground_spawns, spawn_groups, spawn_group_associations, spawn_group_chances, isInstanceType); LogWrite(SPAWN__TRACE, 0, "Spawn", "Exit LoadSpawns"); } bool WorldDatabase::UpdateSpawnLocationSpawns(Spawn* spawn) { Query query; - query.RunQuery2(Q_UPDATE, "update spawn_location_placement set x=%f, y=%f, z=%f, heading=%f, x_offset=%f, y_offset=%f, z_offset=%f, respawn=%u, expire_timer=%u, expire_offset=%u, grid_id=%u, pitch=%f, roll=%f where id = %u", - spawn->GetX(), spawn->GetY(), spawn->GetZ(), spawn->GetHeading(), spawn->GetXOffset(), spawn->GetYOffset(), spawn->GetZOffset(), spawn->GetRespawnTime(), spawn->GetExpireTime(), spawn->GetExpireOffsetTime(), spawn->GetLocation(), spawn->GetPitch(), spawn->GetRoll(), spawn->GetSpawnLocationPlacementID()); + int8 isInstanceType = (spawn && spawn->GetZone() && spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + + query.RunQuery2(Q_UPDATE, "update spawn_location_placement%s set x=%f, y=%f, z=%f, heading=%f, x_offset=%f, y_offset=%f, z_offset=%f, respawn=%u, expire_timer=%u, expire_offset=%u, grid_id=%u, pitch=%f, roll=%f where id = %u", + houseTable.c_str(), spawn->GetX(), spawn->GetY(), spawn->GetZ(), spawn->GetHeading(), spawn->GetXOffset(), spawn->GetYOffset(), spawn->GetZOffset(), spawn->GetRespawnTime(), spawn->GetExpireTime(), spawn->GetExpireOffsetTime(), spawn->GetLocation(), spawn->GetPitch(), spawn->GetRoll(), spawn->GetSpawnLocationPlacementID()); if (query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF) { LogWrite(WORLD__ERROR, 0, "World", "Error in UpdateSpawnLocationSpawns query '%s': %s", query.GetQuery(), query.GetError()); return false; @@ -3588,10 +3660,10 @@ bool WorldDatabase::UpdateSpawnLocationSpawns(Spawn* spawn) { return true; } -bool WorldDatabase::UpdateSpawnWidget(int32 widget_id, char* queryString) { +bool WorldDatabase::UpdateSpawnWidget(int32 widget_id, char* queryString,bool is_house) { Query query; - query.RunQuery2(Q_UPDATE, "update spawn_widgets set %s where widget_id = %u", - queryString, widget_id); + query.RunQuery2(Q_UPDATE, "update spawn_widgets%s set %s where widget_id = %u", + is_house ? "_houses" : "", queryString, widget_id); if (query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF) { LogWrite(WORLD__ERROR, 0, "World", "Error in UpdateSpawnWidget query '%s': %s", query.GetQuery(), query.GetError()); return false; @@ -3666,11 +3738,11 @@ void WorldDatabase::LoadRevivePoints(vector* revive_points, int32 } } -int32 WorldDatabase::GetNextSpawnIDInZone(int32 zone_id) +int32 WorldDatabase::GetNextSpawnIDInZone(int32 zone_id, bool isInstanceType) { Query query; int32 ret = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT MAX(id) FROM spawn where id LIKE '%i____'", zone_id); + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT MAX(id) FROM spawn%s where id LIKE '%i____'", isInstanceType ? "_houses" : "", zone_id); if(result && mysql_num_rows(result) > 0) { MYSQL_ROW row; row = mysql_fetch_row(result); @@ -3692,14 +3764,18 @@ bool WorldDatabase::SaveSpawnInfo(Spawn* spawn){ string suffix = getSafeEscapeString(spawn->GetSuffixTitle()); string prefix = getSafeEscapeString(spawn->GetPrefixTitle()); string last_name = getSafeEscapeString(spawn->GetLastName()); + + int8 isInstanceType = (spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } if(spawn->GetDatabaseID() == 0){ + + int32 new_spawn_id = GetNextSpawnIDInZone(spawn->GetZone()->GetZoneID(), isInstanceType); - int8 isInstanceType = (spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); - - int32 new_spawn_id = GetNextSpawnIDInZone(spawn->GetZone()->GetZoneID()); - - query.RunQuery2(Q_INSERT, "insert into spawn (id, name, race, model_type, size, targetable, show_name, command_primary, command_secondary, visual_state, attackable, show_level, show_command_icon, display_hand_icon, faction_id, collision_radius, hp, power, prefix, suffix, last_name, is_instanced_spawn, merchant_min_level, merchant_max_level) values(%u, '%s', %i, %i, %i, %i, %i, %u, %u, %i, %i, %i, %i, %i, %u, %i, %u, %u, '%s', '%s', '%s', %u, %u, %u)", - new_spawn_id, name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->GetSize(), spawn->appearance.targetable, spawn->appearance.display_name, spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, 0, spawn->appearance.pos.collision_radius, spawn->GetTotalHP(), spawn->GetTotalPower(), prefix.c_str(), suffix.c_str(), last_name.c_str(), isInstanceType, spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel()); + query.RunQuery2(Q_INSERT, "insert into spawn%s (id, name, race, model_type, size, targetable, show_name, command_primary, command_secondary, visual_state, attackable, show_level, show_command_icon, display_hand_icon, faction_id, collision_radius, hp, power, prefix, suffix, last_name, is_instanced_spawn, merchant_min_level, merchant_max_level) values(%u, '%s', %i, %i, %i, %i, %i, %u, %u, %i, %i, %i, %i, %i, %u, %i, %u, %u, '%s', '%s', '%s', %u, %u, %u)", + houseTable.c_str(), new_spawn_id, name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->GetSize(), spawn->appearance.targetable, spawn->appearance.display_name, spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, 0, spawn->appearance.pos.collision_radius, spawn->GetTotalHP(), spawn->GetTotalPower(), prefix.c_str(), suffix.c_str(), last_name.c_str(), isInstanceType, spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel()); if( new_spawn_id > 0 ) spawn->SetDatabaseID(new_spawn_id); // use the new zone_id range @@ -3709,40 +3785,40 @@ bool WorldDatabase::SaveSpawnInfo(Spawn* spawn){ return false; // else, hang your head in shame as you are an utter failure if(spawn->IsNPC()){ - query.RunQuery2(Q_INSERT, "insert into spawn_npcs (spawn_id, min_level, max_level, enc_level, class_, gender, min_group_size, max_group_size, hair_type_id, facial_hair_type_id, wing_type_id, chest_type_id, legs_type_id, soga_hair_type_id, soga_facial_hair_type_id, soga_model_type, heroic_flag, action_state, mood_state, initial_state, activity_status, hide_hood, emote_state) values(%u, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i)", - spawn->GetDatabaseID(), spawn->GetLevel(), spawn->GetLevel(), spawn->GetDifficulty(), spawn->GetAdventureClass(), spawn->GetGender(), 0, 0, ((NPC*)spawn)->features.hair_type, ((NPC*)spawn)->features.hair_face_type, + query.RunQuery2(Q_INSERT, "insert into spawn_npcs%s (spawn_id, min_level, max_level, enc_level, class_, gender, min_group_size, max_group_size, hair_type_id, facial_hair_type_id, wing_type_id, chest_type_id, legs_type_id, soga_hair_type_id, soga_facial_hair_type_id, soga_model_type, heroic_flag, action_state, mood_state, initial_state, activity_status, hide_hood, emote_state) values(%u, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i, %i)", + houseTable.c_str(), spawn->GetDatabaseID(), spawn->GetLevel(), spawn->GetLevel(), spawn->GetDifficulty(), spawn->GetAdventureClass(), spawn->GetGender(), 0, 0, ((NPC*)spawn)->features.hair_type, ((NPC*)spawn)->features.hair_face_type, ((NPC*)spawn)->features.wing_type, ((NPC*)spawn)->features.chest_type, ((NPC*)spawn)->features.legs_type, ((NPC*)spawn)->features.soga_hair_type, ((NPC*)spawn)->features.soga_hair_face_type, spawn->appearance.soga_model_type, spawn->appearance.heroic_flag, spawn->GetActionState(), spawn->GetMoodState(), spawn->GetInitialState(), spawn->GetActivityStatus(), spawn->appearance.hide_hood, spawn->appearance.emote_state); } else if(spawn->IsObject()){ - query.RunQuery2(Q_INSERT, "insert into spawn_objects (spawn_id) values(%u)", spawn->GetDatabaseID()); + query.RunQuery2(Q_INSERT, "insert into spawn_objects%s (spawn_id) values(%u)", houseTable.c_str(), spawn->GetDatabaseID()); } else if(spawn->IsWidget()){ Widget* widget = (Widget*)spawn; - query.RunQuery2(Q_INSERT, "insert into spawn_widgets (spawn_id, widget_id) values(%u, %u)", spawn->GetDatabaseID(), widget->GetWidgetID()); + query.RunQuery2(Q_INSERT, "insert into spawn_widgets%s (spawn_id, widget_id) values(%u, %u)", houseTable.c_str(), spawn->GetDatabaseID(), widget->GetWidgetID()); } else if(spawn->IsSign()){ - query.RunQuery2(Q_INSERT, "insert into spawn_signs (spawn_id, description) values(%u, 'change me')", spawn->GetDatabaseID()); + query.RunQuery2(Q_INSERT, "insert into spawn_signs%s (spawn_id, description) values(%u, 'change me')", houseTable.c_str(), spawn->GetDatabaseID()); } else if (spawn->IsGroundSpawn()) { - query.RunQuery2(Q_INSERT, "insert into spawn_ground (spawn_id) values(%u)", spawn->GetDatabaseID()); + query.RunQuery2(Q_INSERT, "insert into spawn_ground%s (spawn_id) values(%u)", houseTable.c_str(), spawn->GetDatabaseID()); } } else{ if(spawn->IsNPC()){ - query.RunQuery2(Q_UPDATE, "update spawn_npcs, spawn set name='%s', min_level=%i, max_level=%i, enc_level=%i, race=%i, model_type=%i, class_=%i, gender=%i, show_name=%i, attackable=%i, show_level=%i, targetable=%i, show_command_icon=%i, display_hand_icon=%i, hair_type_id=%i, facial_hair_type_id=%i, wing_type_id=%i, chest_type_id=%i, legs_type_id=%i, soga_hair_type_id=%i, soga_facial_hair_type_id=%i, soga_model_type=%i, size=%i, hp=%u, heroic_flag=%i, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, action_state=%i, mood_state=%i, initial_state=%i, activity_status=%i, alignment=%i, faction_id=%u, hide_hood=%i, emote_state=%i, suffix ='%s', prefix='%s', last_name='%s', merchant_min_level = %u, merchant_max_level = %u where spawn_npcs.spawn_id = spawn.id and spawn.id = %u", - name.c_str(), spawn->GetLevel(), spawn->GetLevel(), spawn->GetDifficulty(), spawn->GetRace(), spawn->GetModelType(), + query.RunQuery2(Q_UPDATE, "update spawn_npcs%s, spawn set name='%s', min_level=%i, max_level=%i, enc_level=%i, race=%i, model_type=%i, class_=%i, gender=%i, show_name=%i, attackable=%i, show_level=%i, targetable=%i, show_command_icon=%i, display_hand_icon=%i, hair_type_id=%i, facial_hair_type_id=%i, wing_type_id=%i, chest_type_id=%i, legs_type_id=%i, soga_hair_type_id=%i, soga_facial_hair_type_id=%i, soga_model_type=%i, size=%i, hp=%u, heroic_flag=%i, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, action_state=%i, mood_state=%i, initial_state=%i, activity_status=%i, alignment=%i, faction_id=%u, hide_hood=%i, emote_state=%i, suffix ='%s', prefix='%s', last_name='%s', merchant_min_level = %u, merchant_max_level = %u where spawn_npcs%s.spawn_id = spawn.id and spawn.id = %u", + houseTable.c_str(), name.c_str(), spawn->GetLevel(), spawn->GetLevel(), spawn->GetDifficulty(), spawn->GetRace(), spawn->GetModelType(), spawn->GetAdventureClass(), spawn->GetGender(), spawn->appearance.display_name, spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.targetable, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, ((NPC*)spawn)->features.hair_type, ((NPC*)spawn)->features.hair_face_type, ((NPC*)spawn)->features.wing_type, ((NPC*)spawn)->features.chest_type, ((NPC*)spawn)->features.legs_type, ((NPC*)spawn)->features.soga_hair_type, ((NPC*)spawn)->features.soga_hair_face_type, spawn->appearance.soga_model_type, spawn->GetSize(), spawn->GetTotalHPBase(), spawn->appearance.heroic_flag, spawn->GetTotalPowerBase(), spawn->GetCollisionRadius(), spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->GetActionState(), spawn->GetMoodState(), spawn->GetInitialState(), spawn->GetActivityStatus(), ((NPC*)spawn)->GetAlignment(), spawn->GetFactionID(), spawn->appearance.hide_hood, spawn->appearance.emote_state, - suffix.c_str(), prefix.c_str(), last_name.c_str(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), spawn->GetDatabaseID()); + suffix.c_str(), prefix.c_str(), last_name.c_str(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), houseTable.c_str(), spawn->GetDatabaseID()); } else if(spawn->IsObject()){ - query.RunQuery2(Q_UPDATE, "update spawn_objects, spawn set name='%s', model_type=%i, show_name=%i, targetable=%i, size=%i, command_primary=%u, command_secondary=%u, visual_state=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, collision_radius=%i, hp = %u, power = %u, device_id = %i, merchant_min_level = %u, merchant_max_level = %u where spawn_objects.spawn_id = spawn.id and spawn.id = %u", - name.c_str(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.targetable, spawn->GetSize(), spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, - spawn->GetCollisionRadius(), spawn->GetTotalHP(), spawn->GetTotalPower(), ((Object*)spawn)->GetDeviceID(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), spawn->GetDatabaseID()); + query.RunQuery2(Q_UPDATE, "update spawn_objects%s, spawn set name='%s', model_type=%i, show_name=%i, targetable=%i, size=%i, command_primary=%u, command_secondary=%u, visual_state=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, collision_radius=%i, hp = %u, power = %u, device_id = %i, merchant_min_level = %u, merchant_max_level = %u where spawn_objects%s.spawn_id = spawn.id and spawn.id = %u", + houseTable.c_str(), name.c_str(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.targetable, spawn->GetSize(), spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, + spawn->GetCollisionRadius(), spawn->GetTotalHP(), spawn->GetTotalPower(), ((Object*)spawn)->GetDeviceID(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), houseTable.c_str(), spawn->GetDatabaseID()); } else if(spawn->IsWidget()){ Widget* widget = (Widget*)spawn; @@ -3750,25 +3826,25 @@ bool WorldDatabase::SaveSpawnInfo(Spawn* spawn){ char* closeSound = 0; if (widget->GetOpenSound() != NULL) openSound = (char*)widget->GetOpenSound(); else openSound = (char*)string("0").c_str(); if (widget->GetCloseSound() != NULL) closeSound = (char*)widget->GetCloseSound(); else closeSound = (char*)string("0").c_str(); - query.RunQuery2(Q_UPDATE, "update spawn_widgets, spawn set name='%s', race=%i, model_type=%i, show_name=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, size=%i, hp=%u, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, faction_id=%u, suffix ='%s', prefix='%s', last_name='%s',widget_id = %u,widget_x = %f,widget_y = %f,widget_z = %f,include_heading = %u,include_location = %u,icon = %u,type='%s',open_heading = %f,closed_heading = %f,open_x = %f,open_y = %f,open_z = %f,action_spawn_id = %u,open_sound_file='%s',close_sound_file='%s',open_duration = %u,close_x = %f,close_y=%f,close_z=%f,linked_spawn_id = %u,house_id = %u, merchant_min_level = %u, merchant_max_level = %u where spawn_widgets.spawn_id = spawn.id and spawn.id = %u", - name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, spawn->GetSize(), + query.RunQuery2(Q_UPDATE, "update spawn_widgets%s, spawn set name='%s', race=%i, model_type=%i, show_name=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, size=%i, hp=%u, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, faction_id=%u, suffix ='%s', prefix='%s', last_name='%s',widget_id = %u,widget_x = %f,widget_y = %f,widget_z = %f,include_heading = %u,include_location = %u,icon = %u,type='%s',open_heading = %f,closed_heading = %f,open_x = %f,open_y = %f,open_z = %f,action_spawn_id = %u,open_sound_file='%s',close_sound_file='%s',open_duration = %u,close_x = %f,close_y=%f,close_z=%f,linked_spawn_id = %u,house_id = %u, merchant_min_level = %u, merchant_max_level = %u where spawn_widgets%s.spawn_id = spawn.id and spawn.id = %u", + houseTable.c_str(), name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, spawn->GetSize(), spawn->GetTotalHP(), spawn->GetTotalPower(), spawn->GetCollisionRadius(), spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->GetFactionID(), suffix.c_str(), prefix.c_str(), last_name.c_str(), widget->GetWidgetID(), widget->GetX(), widget->GetY(), widget->GetZ(), widget->GetIncludeHeading(), widget->GetIncludeLocation(), widget->GetIconValue(), Widget::GetWidgetTypeNameByTypeID(widget->GetWidgetType()).c_str(), widget->GetOpenHeading(), widget->GetClosedHeading(), widget->GetOpenX(), widget->GetOpenY(), widget->GetOpenZ(), widget->GetActionSpawnID(), openSound, closeSound,widget->GetOpenDuration(), widget->GetCloseX(),widget->GetCloseY(),widget->GetCloseZ(),widget->GetLinkedSpawnID(),widget->GetHouseID(), - spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), spawn->GetDatabaseID()); + spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), houseTable.c_str(), spawn->GetDatabaseID()); } else if(spawn->IsSign()){ Sign* sign = (Sign*)spawn; - query.RunQuery2(Q_UPDATE, "update spawn_signs, spawn set name='%s', race=%i, model_type=%i, show_name=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, size=%i, hp=%u, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, faction_id=%u, suffix ='%s', prefix='%s', last_name='%s', `type`='%s', zone_id = %u, widget_id = %u, title='%s', widget_x = %f, widget_y = %f, widget_z = %f, icon = %u, description='%s', sign_distance = %f, zone_x = %f, zone_y = %f, zone_z = %f, zone_heading = %f, include_heading = %u, include_location = %u, merchant_min_level = %u, merchant_max_level = %u, language = %u where spawn_signs.spawn_id = spawn.id and spawn.id = %u", - name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, spawn->GetSize(), + query.RunQuery2(Q_UPDATE, "update spawn_signs%s, spawn set name='%s', race=%i, model_type=%i, show_name=%i, attackable=%i, show_level=%i, show_command_icon=%i, display_hand_icon=%i, size=%i, hp=%u, power=%u, collision_radius=%i, command_primary=%u, command_secondary=%u, visual_state=%i, faction_id=%u, suffix ='%s', prefix='%s', last_name='%s', `type`='%s', zone_id = %u, widget_id = %u, title='%s', widget_x = %f, widget_y = %f, widget_z = %f, icon = %u, description='%s', sign_distance = %f, zone_x = %f, zone_y = %f, zone_z = %f, zone_heading = %f, include_heading = %u, include_location = %u, merchant_min_level = %u, merchant_max_level = %u, language = %u where spawn_signs%s.spawn_id = spawn.id and spawn.id = %u", + houseTable.c_str(), name.c_str(), spawn->GetRace(), spawn->GetModelType(), spawn->appearance.display_name, spawn->appearance.attackable, spawn->appearance.show_level, spawn->appearance.show_command_icon, spawn->appearance.display_hand_icon, spawn->GetSize(), spawn->GetTotalHP(), spawn->GetTotalPower(), spawn->GetCollisionRadius(), spawn->GetPrimaryCommandListID(), spawn->GetSecondaryCommandListID(), spawn->GetVisualState(), spawn->GetFactionID(), suffix.c_str(), prefix.c_str(), last_name.c_str(), sign->GetSignType() == SIGN_TYPE_GENERIC ? "Generic" : "Zone", sign->GetSignZoneID(), sign->GetWidgetID(), sign->GetSignTitle(), sign->GetWidgetX(), sign->GetWidgetY(), sign->GetWidgetZ(), sign->GetIconValue(), sign->GetSignDescription(), sign->GetSignDistance(), sign->GetSignZoneX(), sign->GetSignZoneY(), sign->GetSignZoneZ(), sign->GetSignZoneHeading(), sign->GetIncludeHeading(), - sign->GetIncludeLocation(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), sign->GetLanguage(), spawn->GetDatabaseID()); + sign->GetIncludeLocation(), spawn->GetMerchantMinLevel(), spawn->GetMerchantMaxLevel(), sign->GetLanguage(), houseTable.c_str(), spawn->GetDatabaseID()); } } if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF){ @@ -3794,7 +3870,7 @@ int32 WorldDatabase::SaveCombinedSpawnLocation(ZoneServer* zone, Spawn* in_spawn spawnLocationID = spawn_location_id; if(!name) name = "Combine SpawnGroup Generated"; - if(!CreateNewSpawnLocation(spawn_location_id, name)){ + if(!CreateNewSpawnLocation(spawn_location_id, name, (zone->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE))){ safe_delete(spawns); return 0; } @@ -3841,21 +3917,28 @@ bool WorldDatabase::SaveSpawnEntry(Spawn* spawn, const char* spawn_location_name Query query; Query query2; int32 count = 0; + + int8 isInstanceType = (spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + if(create_spawnlocation){ - count = GetSpawnLocationCount(spawn->GetSpawnLocationID()); + count = GetSpawnLocationCount(spawn->GetSpawnLocationID(), spawn); if(count == 0){ - if(!CreateNewSpawnLocation(spawn->GetSpawnLocationID(), spawn_location_name)) + if(!CreateNewSpawnLocation(spawn->GetSpawnLocationID(), spawn_location_name, isInstanceType)) return false; } } - query.RunQuery2(Q_INSERT, "insert into spawn_location_entry (spawn_id, spawn_location_id, spawnpercentage) values(%u, %u, %i)", spawn->GetDatabaseID(), spawn->GetSpawnLocationID(), percent); + query.RunQuery2(Q_INSERT, "insert into spawn_location_entry%s (spawn_id, spawn_location_id, spawnpercentage) values(%u, %u, %i)", houseTable.c_str(), spawn->GetDatabaseID(), spawn->GetSpawnLocationID(), percent); if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF){ LogWrite(SPAWN__ERROR, 0, "Spawn", "Error in SaveSpawnEntry query '%s': %s", query.GetQuery(), query.GetError()); return false; } if(save_zonespawn){ - query2.RunQuery2(Q_INSERT, "insert into spawn_location_placement (zone_id, instance_id, spawn_location_id, x, y, z, x_offset, y_offset, z_offset, heading, grid_id) values(%u, %u, %u, %f, %f, %f, %f, %f, %f, %f, %u)", spawn->GetZone()->GetZoneID(), spawn->GetZone()->GetInstanceID(), spawn->GetSpawnLocationID(), spawn->GetX(), spawn->GetY(), spawn->GetZ(),x_offset, y_offset, z_offset, spawn->GetHeading(), spawn->GetLocation()); + query2.RunQuery2(Q_INSERT, "insert into spawn_location_placement%s (zone_id, instance_id, spawn_location_id, x, y, z, x_offset, y_offset, z_offset, heading, grid_id) values(%u, %u, %u, %f, %f, %f, %f, %f, %f, %f, %u)", houseTable.c_str(), spawn->GetZone()->GetZoneID(), spawn->GetZone()->GetInstanceID(), spawn->GetSpawnLocationID(), spawn->GetX(), spawn->GetY(), spawn->GetZ(),x_offset, y_offset, z_offset, spawn->GetHeading(), spawn->GetLocation()); if(query2.GetErrorNumber() && query2.GetError() && query2.GetErrorNumber() < 0xFFFFFFFF){ LogWrite(SPAWN__ERROR, 0, "Spawn", "Error in SaveSpawnEntry query '%s': %s", query2.GetQuery(), query2.GetError()); return false; @@ -3901,12 +3984,12 @@ float WorldDatabase::GetSpawnLocationPlacementOffsetZ(int32 location_id) { return ret; } -bool WorldDatabase::CreateNewSpawnLocation(int32 id, const char* name){ +bool WorldDatabase::CreateNewSpawnLocation(int32 id, const char* name, bool isHouseType){ Query query; if(!name) name = "Unknown Spawn Location Name"; string str_name = getSafeEscapeString(name); - query.RunQuery2(Q_INSERT, "insert into spawn_location_name (id, name) values(%u, '%s')", id, str_name.c_str()); + query.RunQuery2(Q_INSERT, "insert into spawn_location_name%s (id, name) values(%u, '%s')", isHouseType ? "_houses" : "", id, str_name.c_str()); if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF){ LogWrite(SPAWN__ERROR, 0, "Spawn", "Error in CreateNewSpawnLocation query '%s': %s", query.GetQuery(), query.GetError()); return false; @@ -3918,10 +4001,16 @@ int32 WorldDatabase::GetSpawnLocationCount(int32 location, Spawn* spawn){ Query query; int32 ret = 0; MYSQL_RES* result = 0; + + int8 isInstanceType = (spawn && spawn->GetZone() && spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } if(spawn) - result = query.RunQuery2(Q_SELECT, "SELECT count(id) FROM spawn_location_entry where spawn_location_id=%u and spawn_id=%u", location, spawn->GetDatabaseID()); + result = query.RunQuery2(Q_SELECT, "SELECT count(id) FROM spawn_location_entry%s where spawn_location_id=%u and spawn_id=%u", houseTable.c_str(), location, spawn->GetDatabaseID()); else - result = query.RunQuery2(Q_SELECT, "SELECT count(id) FROM spawn_location_entry where spawn_location_id=%u", location); + result = query.RunQuery2(Q_SELECT, "SELECT count(id) FROM spawn_location_entry%s where spawn_location_id=%u", houseTable.c_str(), location); if(result && mysql_num_rows(result) > 0){ MYSQL_ROW row; while(result && (row = mysql_fetch_row(result)) && row[0]){ @@ -3931,10 +4020,10 @@ int32 WorldDatabase::GetSpawnLocationCount(int32 location, Spawn* spawn){ return ret; } -int32 WorldDatabase::GetNextSpawnLocation(){ +int32 WorldDatabase::GetNextSpawnLocation(bool isInstanceType){ Query query; int32 ret = 0; - MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT max(id) FROM spawn_location_name"); + MYSQL_RES* result = query.RunQuery2(Q_SELECT, "SELECT max(id) FROM spawn_location_name%s", isInstanceType ? "_houses" : ""); if(result && mysql_num_rows(result) > 0){ MYSQL_ROW row; while(result && (row = mysql_fetch_row(result)) && row[0]){ @@ -3950,11 +4039,17 @@ bool WorldDatabase::RemoveSpawnFromSpawnLocation(Spawn* spawn){ Query query2; int32 count = GetSpawnLocationCount(spawn->GetSpawnLocationID(), spawn); - query.RunQuery2(Q_DELETE, "delete FROM spawn_location_placement where spawn_location_id=%u", spawn->GetSpawnLocationID()); + int8 isInstanceType = (spawn && spawn->GetZone() && spawn->GetZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + + query.RunQuery2(Q_DELETE, "delete FROM spawn_location_placement%s where spawn_location_id=%u", houseTable.c_str(), spawn->GetSpawnLocationID()); if(count == 1) - query.RunQuery2(Q_DELETE, "delete FROM spawn_location_name where id=%u", spawn->GetSpawnLocationID()); + query.RunQuery2(Q_DELETE, "delete FROM spawn_location_name%s where id=%u", houseTable.c_str(), spawn->GetSpawnLocationID()); - query2.RunQuery2(Q_DELETE, "delete FROM spawn_location_entry where spawn_id=%u and spawn_location_id = %u", spawn->GetDatabaseID(), spawn->GetSpawnLocationID()); + query2.RunQuery2(Q_DELETE, "delete FROM spawn_location_entry%s where spawn_id=%u and spawn_location_id = %u", houseTable.c_str(), spawn->GetDatabaseID(), spawn->GetSpawnLocationID()); if(query.GetErrorNumber() && query.GetError() && query.GetErrorNumber() < 0xFFFFFFFF){ LogWrite(WORLD__ERROR, 0, "World", "Error in RemoveSpawnFromSpawnLocation query '%s': %s", query.GetQuery(), query.GetError()); return false; @@ -5288,7 +5383,8 @@ bool WorldDatabase::DeleteCharacter(int32 account_id, int32 character_id){ query.AddQueryAsync(character_id, this, Q_DELETE, "delete from character_custom_spell_dataindex where charid=%u", character_id); query.AddQueryAsync(character_id, this, Q_DELETE, "delete from character_custom_spell_display where charid=%u", character_id); query.AddQueryAsync(character_id, this, Q_DELETE, "delete from character_custom_spell_data where charid=%u", character_id); - + broker.RemoveSeller(character_id); + return true; } @@ -7129,13 +7225,17 @@ bool WorldDatabase::LoadNPC(ZoneServer* zone, int32 spawn_id) { NPC* npc = nullptr; int32 id = 0; DatabaseResult result; - - database_new.Select(&result, "SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str\n" - "FROM spawn s\n" - "INNER JOIN spawn_npcs npc\n" + int8 isInstanceType = (zone->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE); + std::string houseTable(""); + if(isInstanceType) { + houseTable.append("_houses"); + } + database_new.Select(&result, "SELECT npc.spawn_id, s.name, npc.min_level, npc.max_level, npc.enc_level, s.race, s.model_type, npc.class_, npc.gender, s.command_primary, s.command_secondary, s.show_name, npc.min_group_size, npc.max_group_size, npc.hair_type_id, npc.facial_hair_type_id, npc.wing_type_id, npc.chest_type_id, npc.legs_type_id, npc.soga_hair_type_id, npc.soga_facial_hair_type_id, s.attackable, s.show_level, s.targetable, s.show_command_icon, s.display_hand_icon, s.hp, s.power, s.size, s.collision_radius, npc.action_state, s.visual_state, npc.mood_state, npc.initial_state, npc.activity_status, s.faction_id, s.sub_title, s.merchant_id, s.merchant_type, s.size_offset, npc.attack_type, npc.ai_strategy+0, npc.spell_list_id, npc.secondary_spell_list_id, npc.skill_list_id, npc.secondary_skill_list_id, npc.equipment_list_id, npc.str, npc.sta, npc.wis, npc.intel, npc.agi, npc.heat, npc.cold, npc.magic, npc.mental, npc.divine, npc.disease, npc.poison, npc.aggro_radius, npc.cast_percentage, npc.randomize, npc.soga_model_type, npc.heroic_flag, npc.alignment, npc.elemental, npc.arcane, npc.noxious, s.savagery, s.dissonance, npc.hide_hood, npc.emote_state, s.prefix, s.suffix, s.last_name, s.disable_sounds, s.merchant_min_level, s.merchant_max_level, s.aaxp_rewards, s.loot_tier, s.loot_drop_type, npc.scared_by_strong_players, npc.action_state_str, s.lua_script\n" + "FROM spawn%s s\n" + "INNER JOIN spawn_npcs%s npc\n" "ON npc.spawn_id = s.id\n" "WHERE s.id = %u", - spawn_id); + houseTable.c_str(), houseTable.c_str(), spawn_id); if (result.GetNumRows() > 0 && result.Next()) { id = result.GetInt32(0); @@ -7273,6 +7373,11 @@ bool WorldDatabase::LoadNPC(ZoneServer* zone, int32 spawn_id) { npc->GetInfoStruct()->set_action_state(action_state_str); } + if(!result.IsNull(83)){ + std::string lua_script = std::string(result.GetString(83)); + npc->SetSpawnScript(lua_script, true); + } + zone->AddNPC(id, npc); //skipped spells/skills/equipment as it is all loaded, the following rely on a spawn to load @@ -7761,9 +7866,6 @@ void WorldDatabase::GetHouseSpawnInstanceData(ZoneServer* zone, Spawn* spawn) if (!spawn) return; - if (zone->house_object_database_lookup.count(spawn->GetModelType()) < 1) - zone->house_object_database_lookup.Put(spawn->GetModelType(), spawn->GetDatabaseID()); - DatabaseResult result; database_new.Select(&result, "SELECT pickup_item_id, pickup_unique_item_id\n" @@ -8851,7 +8953,6 @@ void WorldDatabase::SaveSignMark(int32 char_id, int32 sign_id, char* char_name, LogWrite(SIGN__DEBUG, 0, "Sign", "ERROR in WorldDatabase::SaveSignMark"); return; } - } string WorldDatabase::GetSignMark(int32 char_id, int32 sign_id, char* char_name) { @@ -8884,4 +8985,121 @@ int32 WorldDatabase::GetMysqlExpCurve(int level) { } //return 1 so we dont break shit later divide by 0 and all that. return 1; +} + +bool WorldDatabase::RemoveBrokerItem(int32 cid, int64 uid, int32 quantity) { + if (quantity <= 0) + return false; + + string update = string("UPDATE broker_items SET count = count - %u WHERE character_id = %u AND unique_id = %llu AND count >= %u"); + Query q; + q.RunQuery2( + Q_UPDATE, + update.c_str(), quantity, cid, uid, quantity + ); + + if (q.GetErrorNumber() && q.GetError() && q.GetErrorNumber() < 0xFFFFFFFF) { + LogWrite(WORLD__ERROR, 0, "Broker", + "DB error removing %u from item %llu for char %u: %s", + quantity, uid, cid, q.GetError()); + return false; + } + + // exactly one row updated ⇒ we had enough stock (and trigger will + // delete it if count hit zero) + return (q.GetAffectedRows() == 1); +} + +int32 WorldDatabase::LoadBrokerSellers(BrokerManager &broker) { + Query query; + MYSQL_ROW row; + int32 count = 0; + + MYSQL_RES * result = query.RunQuery2( + Q_SELECT, + "SELECT character_id, seller_name, house_id, sale_enabled, sell_from_inventory, coin_session, coin_total FROM broker_sellers" + ); + + while (result && (row = mysql_fetch_row(result))) { + int32 cid = atoul(row[0]); + std::string name = row[1] ? row[1] : ""; + int32 hid = strtoull(row[2], NULL, 0); + bool en = atoi(row[3]) != 0; + bool inv = atoi(row[4]) != 0; + int64 coin_session = strtoull(row[5], NULL, 0); + int64 coin_total = strtoull(row[6], NULL, 0); + + broker.LoadSeller(cid, name, hid, true, inv, coin_session, coin_total); + ++count; + + LogWrite(PLAYER__ERROR, 5, "Broker", + "--Loaded broker player: %u (%s), house=%u, sale=%u, inv=%u", + cid, name.c_str(), hid, en, inv + ); + } + + return count; +} + +int32 WorldDatabase::LoadBrokerItems(BrokerManager &broker) { + Query query; + MYSQL_ROW row; + int32 count = 0; + + MYSQL_RES * result = query.RunQuery2( + Q_SELECT, + "SELECT unique_id, character_id, house_id, item_id," + " cost_copper, for_sale, inv_slot_id, slot_id, count, from_inventory, creator " + " FROM broker_items" + ); + + while (result && (row = mysql_fetch_row(result))) { + SaleItem it{}; + it.unique_id = strtoull(row[0], NULL, 0); + it.character_id = atoi(row[1]); + it.house_id = atoi(row[2]); + it.item_id = strtoull(row[3], NULL, 0); + it.cost_copper = atoi(row[4]); + it.for_sale = atoi(row[5]) != 0; + it.inv_slot_id = atoi(row[6]); + it.slot_id = atoi(row[7]); + it.count = atoi(row[8]); + it.from_inventory = atoul(row[9]); + if(row[10]) + it.creator = std::string(row[10]); + + broker.LoadItem(it); + ++count; + + LogWrite(PLAYER__ERROR, 5, "Broker", + "--Loaded broker item: uid=%llu, char=%u, item=%llu," + " price=%u, count=%u", + it.unique_id, it.character_id, + it.item_id, it.cost_copper, + it.count + ); + } + + return count; +} + +int32 WorldDatabase::LoadBrokerData(BrokerManager &broker) { + int32 p = LoadBrokerSellers(broker); + int32 i = LoadBrokerItems(broker); + return p + i; +} + +bool WorldDatabase::UpdateHouseSpawnScript(int32 dbid, std::string scriptName) { + string update = string("UPDATE spawn_houses SET lua_script='%s' where id=%u"); + Query q; + q.RunQuery2( + Q_UPDATE, + update.c_str(), getSafeEscapeString(scriptName.c_str()).c_str(), dbid + ); + + if (q.GetErrorNumber() && q.GetError() && q.GetErrorNumber() < 0xFFFFFFFF) { + return false; + } + + return (q.GetAffectedRows() == 1); } \ No newline at end of file diff --git a/source/WorldServer/WorldDatabase.h b/source/WorldServer/WorldDatabase.h index ab53169..6c61261 100644 --- a/source/WorldServer/WorldDatabase.h +++ b/source/WorldServer/WorldDatabase.h @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef EQ2WORLD_EMU_DATABASE_H #define EQ2WORLD_EMU_DATABASE_H @@ -50,6 +51,7 @@ #include "Rules/Rules.h" #include "Languages.h" #include "World.h" +#include "Broker/BrokerManager.h" using namespace std; @@ -175,13 +177,13 @@ public: void SaveWorldTime(WorldTime* time); bool SaveSpawnInfo(Spawn* spawn); - int32 GetNextSpawnIDInZone(int32 zone_id); + int32 GetNextSpawnIDInZone(int32 zone_id, bool isInstanceType = false); bool SaveSpawnEntry(Spawn* spawn, const char* spawn_location_name, int8 percent, float x_offset, float y_offset, float z_offset, bool save_zonespawn = true, bool create_spawnlocation = true); float GetSpawnLocationPlacementOffsetX(int32 location_id); float GetSpawnLocationPlacementOffsetY(int32 location_id); float GetSpawnLocationPlacementOffsetZ(int32 location_id); - int32 GetNextSpawnLocation(); - bool CreateNewSpawnLocation(int32 id, const char* name); + int32 GetNextSpawnLocation(bool isInstanceType = false); + bool CreateNewSpawnLocation(int32 id, const char* name, bool isHouseType = false); bool RemoveSpawnFromSpawnLocation(Spawn* spawn); int32 GetSpawnLocationCount(int32 location, Spawn* spawn = 0); vector* GetSpawnNameList(const char* in_name); @@ -240,7 +242,7 @@ public: void UpdateStartingSkillbar(int32 char_id, int8 class_id, int8 race_id); void UpdateStartingTitles(int32 char_id, int8 class_id, int8 race_id, int8 gender_id); bool UpdateSpawnLocationSpawns(Spawn* spawn); - bool UpdateSpawnWidget(int32 widget_id, char* query); + bool UpdateSpawnWidget(int32 widget_id, char* query, bool is_house = false); bool CheckVersionTable(); void LoadFactionAlliances(); void LoadFactionList(); @@ -275,16 +277,16 @@ public: void LoadGroundSpawnItems(ZoneServer* zone); void LoadSpawns(ZoneServer* zone); int8 GetAppearanceType(string type); - void LoadNPCs(ZoneServer* zone); + void LoadNPCs(ZoneServer* zone, bool isInstanceType = false); void LoadSpiritShards(ZoneServer* zone); int32 LoadAppearances(ZoneServer* zone, Client* client = 0); int32 LoadNPCSpells(); int32 LoadNPCSkills(ZoneServer* zone); int32 LoadNPCEquipment(ZoneServer* zone); - void LoadObjects(ZoneServer* zone); - void LoadGroundSpawns(ZoneServer* zone); - void LoadWidgets(ZoneServer* zone); - void LoadSigns(ZoneServer* zone); + void LoadObjects(ZoneServer* zone, bool isInstanceType = false); + void LoadGroundSpawns(ZoneServer* zone, bool isInstanceType = false); + void LoadWidgets(ZoneServer* zone, bool isInstanceType = false); + void LoadSigns(ZoneServer* zone, bool isInstanceType = false); void ReloadItemList(int32 item_id = 0); void LoadItemList(int32 item_id = 0); int32 LoadItemStats(int32 item_id = 0); @@ -293,7 +295,8 @@ public: int32 LoadItemLevelOverride(int32 item_id = 0); int32 LoadItemEffects(int32 item_id = 0); int32 LoadBookPages(int32 item_id = 0); - int32 LoadNextUniqueItemID(); + int64 LoadNextUniqueItemID(); + void ResetNextUniqueItemID(); int32 LoadSkillItems(int32 item_id = 0); int32 LoadRangeWeapons(int32 item_id = 0); int32 LoadThrownWeapons(int32 item_id = 0); @@ -662,6 +665,17 @@ public: void LoadCharacterSpellEffects(int32 char_id, Client *client, int8 db_spell_type); int32 GetMysqlExpCurve(int level); + + bool RemoveBrokerItem(int32 cid, int64 uid, int32 quantity); + int32 LoadBrokerSellers(BrokerManager &broker); + int32 LoadBrokerItems(BrokerManager &broker); + int32 LoadBrokerData(BrokerManager &broker); + + void ClearSellerSession(int32 character_id); + void AddToSellerSession(int32 character_id, int64 amount); + int64 GetSellerSession(int32 character_id); + + bool UpdateHouseSpawnScript(int32 dbid, std::string scriptName); private: DatabaseNew database_new; std::map zone_names; diff --git a/source/WorldServer/client.cpp b/source/WorldServer/client.cpp index 0899576..2029683 100644 --- a/source/WorldServer/client.cpp +++ b/source/WorldServer/client.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2005 - 2025 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -94,6 +94,7 @@ #include "AltAdvancement/AltAdvancement.h" #include "Bots/Bot.h" #include "VisualStates.h" +#include "Broker/BrokerManager.h" extern WorldDatabase database; extern const char* ZONE_NAME; @@ -130,6 +131,7 @@ extern MasterRecipeBookList master_recipebook_list; extern VisualStates visual_states; extern PeerManager peer_manager; extern HTTPSClientPool peer_https_pool; +extern BrokerManager broker; using namespace std; @@ -242,6 +244,10 @@ Client::Client(EQStream* ieqs) : underworld_cooldown_timer(5000), pos_update(125 recipe_xor_packet = nullptr; recipe_packet_count = 0; recipe_orig_packet_size = 0; + gm_store_search = false; + SetShopWindowStatus(false); + search_page = 0; + firstlogin_transmit = false; } Client::~Client() { @@ -306,7 +312,7 @@ void Client::RemoveClientFromZone() { zone_list.RemoveClientFromMap(player->GetName(), this); safe_delete(camp_timer); - safe_delete(search_items); + ClearItemSearch(); safe_delete(current_rez.expire_timer); safe_delete(pending_last_name); safe_delete_array(incoming_paperdoll.image_bytes); @@ -838,8 +844,10 @@ void Client::SendCharInfo() { if (player->GetHP() < player->GetTotalHP() || player->GetPower() < player->GetTotalPower()) GetCurrentZone()->AddDamagedSpawn(player); - if (firstlogin) + if (firstlogin) { + firstlogin_transmit = true; firstlogin = false; + } player->ClearProcs(); items = player->GetEquippedItemList(); @@ -910,6 +918,8 @@ void Client::SendCharInfo() { GetPlayer()->SetSaveSpellEffects(false); GetPlayer()->SetCharSheetChanged(true); GetPlayer()->SetReturningFromLD(false); + + broker.LockActiveItemsForClient(this); } void Client::SendZoneSpawns() { @@ -1491,7 +1501,7 @@ bool Client::HandlePacket(EQApplicationPacket* app) { spawn->position_changed = true; _snprintf(query, 256, "open_heading=%f,include_heading=1", newHeading); - if (database.UpdateSpawnWidget(widget->GetWidgetID(), query)) + if (database.UpdateSpawnWidget(widget->GetWidgetID(), query, GetCurrentZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE)) SimpleMessage(CHANNEL_COLOR_YELLOW, "Successfully saved widget open heading information."); } else @@ -1508,7 +1518,7 @@ bool Client::HandlePacket(EQApplicationPacket* app) { spawn->position_changed = true; _snprintf(query, 256, "closed_heading=%f,include_heading=1", newHeading); - if (database.UpdateSpawnWidget(widget->GetWidgetID(), query)) + if (database.UpdateSpawnWidget(widget->GetWidgetID(), query, GetCurrentZone()->GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE)) SimpleMessage(CHANNEL_COLOR_YELLOW, "Successfully saved widget close heading information."); if (spawn->GetSpawnLocationID()) @@ -1921,6 +1931,26 @@ bool Client::HandlePacket(EQApplicationPacket* app) { if (GetPlayer()->IsDeletedSpawn()) { GetPlayer()->SetDeletedSpawn(false); } + + if(firstlogin_transmit) { + if (auto info = broker.GetSellerInfo(GetPlayer()->GetCharacterID())) { + auto logs = broker.GetSellerLog(GetPlayer()->GetCharacterID()); + for (auto const & log : logs) { + SendHouseSaleLog(log.message, + 0, + 0, + 0); + } + } + firstlogin_transmit = false; + } + if(HasOwnerOrEditAccess()) { // we are in their own house + int64 coin_session = database.GetSellerSession(GetPlayer()->GetCharacterID()); + if(coin_session) { + OpenShopWindow(nullptr, false, 1); + } + } + ResetZoningCoords(); SetReadyForUpdates(); GetPlayer()->SendSpawnChanges(true); @@ -2130,9 +2160,7 @@ bool Client::HandlePacket(EQApplicationPacket* app) { EQ2_CommandString remote(app->pBuffer, app->size); LogWrite(PACKET__DEBUG, 1, "Packet", "RemoteCmdMsg Packet dump:"); -#if EQDEBUG >= 9 DumpPacket(app); -#endif commands.Process(remote.handler, &remote.command, this); } else //bad client, disconnect @@ -2448,10 +2476,12 @@ bool Client::HandlePacket(EQApplicationPacket* app) { int32 spawn_index = 0; if (GetVersion() <= 561) { - spawn_index = packet->getType_int32_ByName("house_id"); + house_id = packet->getType_int32_ByName("house_id"); + spawn_index = GetPlayer()->GetTarget() ? GetPlayer()->GetTarget()->GetID() : 0; } else { house_id = packet->getType_int64_ByName("house_id"); + spawn_index = packet->getType_int32_ByName("spawn_id"); } ZoneChangeDetails zone_details; if (GetHouseZoneServer(&zone_details, spawn_index, house_id)) { @@ -2904,8 +2934,8 @@ bool Client::HandleLootItem(Spawn* entity, Item* item, Spawn* target, bool overr return false; } - if (lootingPlayer->item_list.HasFreeSlot() || lootingPlayer->item_list.CanStack(item)) { - if (lootingPlayer->item_list.AssignItemToFreeSlot(item)) { + if (lootingPlayer->item_list.HasFreeSlot() || lootingPlayer->item_list.CanStack(item, false)) { + if (lootingPlayer->item_list.AssignItemToFreeSlot(item, true)) { if (item->CheckFlag2(HEIRLOOM)) { // TODO: RAID Support GroupMemberInfo* gmi = lootingClient->GetPlayer()->GetGroupMemberInfo(); @@ -3396,6 +3426,16 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { item = GetPlayer()->GetAppearanceEquipmentList()->GetItemFromUniqueID(id); if (!item) item = master_item_list.GetItem(id); + if (!item && HasOwnerOrEditAccess()) + item = GetPlayer()->item_list.GetVaultItemFromUniqueID(id, true); + if(!item && GetMerchantTransactionID()) { + Spawn* merchant = GetPlayer()->GetZone()->GetSpawnByID(GetMerchantTransactionID()); + if(merchant && merchant->GetHouseCharacterID() && merchant->GetPickupUniqueItemID()) { + if(auto itemInfo = broker.GetActiveItem(merchant->GetHouseCharacterID(), id)) { + item = master_item_list.GetItem(itemInfo->item_id); + } + } + } if (item) {// && sent_item_details.count(id) == 0){ MItemDetails.writelock(__FUNCTION__, __LINE__); sent_item_details[id] = true; @@ -3407,7 +3447,7 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { delete item; } else { - LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest: Unknown Item ID = %u", id); + LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest#0: Unknown Item ID = %u", id); DumpPacket(app); } } @@ -3430,6 +3470,16 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { item = GetPlayer()->GetAppearanceEquipmentList()->GetItemFromUniqueID(unique_id); if (!item) item = master_item_list.GetItem(id); + + if(!item && GetMerchantTransactionID()) { + Spawn* merchant = GetPlayer()->GetZone()->GetSpawnByID(GetMerchantTransactionID()); + if(merchant && merchant->GetHouseCharacterID() && merchant->GetPickupUniqueItemID()) { + if(auto itemInfo = broker.GetActiveItem(merchant->GetHouseCharacterID(), id)) { + item = master_item_list.GetItem(itemInfo->item_id); + } + } + } + if (item) { MItemDetails.writelock(__FUNCTION__, __LINE__); sent_item_details[id] = true; @@ -3438,7 +3488,7 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { QueuePacket(app); } else { - LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest: Unknown Item ID = %u", id); + LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest#1: Unknown Item ID = %u", id); DumpPacket(app); } } @@ -3462,6 +3512,16 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { //int16 unknown5 = request->getType_sint16_ByName("unknown5"); //printf("Type: (%i) Unknown_0: (%u) Unknown_1: (%u) Unknown2: (%i) Unique ID: (%u) Unknown5: (%i) Item ID: (%u)\n",type,unknown_0,unknown_1,unknown2,unique_id,unknown5,id); Item* item = master_item_list.GetItem(id); + + if(!item && GetMerchantTransactionID()) { + Spawn* merchant = GetPlayer()->GetZone()->GetSpawnByID(GetMerchantTransactionID()); + if(merchant && merchant->GetHouseCharacterID() && merchant->GetPickupUniqueItemID()) { + if(auto itemInfo = broker.GetActiveItem(merchant->GetHouseCharacterID(), id)) { + item = master_item_list.GetItem(itemInfo->item_id); + } + } + } + if (item) { //only display popup for non merchant links EQ2Packet* app = item->serialize(GetVersion(), (request->getType_int8_ByName("show_popup") != 0), GetPlayer(), true, 0, 0, GetVersion() > 561 ? true : false); @@ -3469,7 +3529,7 @@ void Client::HandleExamineInfoRequest(EQApplicationPacket* app) { QueuePacket(app); } else { - LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest: Unknown Item ID = %u", id); + LogWrite(WORLD__ERROR, 0, "World", "HandleExamineInfoRequest#2: Unknown Item ID = %u", id); DumpPacket(app); } } @@ -3798,7 +3858,7 @@ bool Client::Process(bool zone_process) { if (temp_placement_timer.Check()) { if (GetTempPlacementSpawn() && GetPlayer()->WasSentSpawn(GetTempPlacementSpawn()->GetID()) && !hasSentTempPlacementSpawn) { int8 placement = 0; - int32 uniqueID = GetPlacementUniqueItemID(); + int64 uniqueID = GetPlacementUniqueItemID(); Item* uniqueItem = GetPlayer()->item_list.GetItemFromUniqueID(uniqueID); if (uniqueItem && uniqueItem->houseitem_info) placement = uniqueItem->houseitem_info->house_location; @@ -8107,7 +8167,9 @@ bool Client::AddItem(Item* item, bool* item_deleted, AddItemType type) { return false; } - + + OpenShopWindow(nullptr); + return true; } @@ -8301,6 +8363,9 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) { Guild* guild = GetPlayer()->GetGuild(); if (spawn && spawn->GetMerchantID() > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY)) && spawn->IsClientInMerchantLevelRange(this)) { + if((spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)) + return; + int32 total_sell_price = 0; int32 total_status_sell_price = 0; //for status float multiplier = CalculateBuyMultiplier(spawn->GetMerchantID()); @@ -8316,11 +8381,15 @@ void Client::SellItem(int32 item_id, int16 quantity, int32 unique_id) { if (!item) item = player->item_list.GetItemFromID(item_id); if (item && master_item) { - if(item->details.inv_slot_id == -3 || item->details.inv_slot_id == -4) { + if(item->details.inv_slot_id == InventorySlotType::BANK || item->details.inv_slot_id == InventorySlotType::SHARED_BANK) { SimpleMessage(CHANNEL_COLOR_RED, "You cannot sell an item in the bank."); return; } - if (item->details.item_locked || item->details.equip_slot_id) + if(GetPlayer()->item_list.IsItemInSlotType(item, InventorySlotType::HOUSE_VAULT)) { + SimpleMessage(CHANNEL_COLOR_RED, "You cannot sell an item in the house vault."); + return; + } + if (item->IsItemLocked() || item->details.equip_slot_id) { SimpleMessage(CHANNEL_COLOR_RED, "You cannot sell the item in use."); return; @@ -8418,6 +8487,9 @@ void Client::BuyBack(int32 item_id, int16 quantity) { Spawn* spawn = GetCurrentZone()->GetSpawnByID(GetMerchantTransactionID()); if (spawn && spawn->GetMerchantID() > 0 && (!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK)) && spawn->IsClientInMerchantLevelRange(this)) { + if((spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)) + return; + deque::iterator itr; BuyBackItem* buyback = 0; BuyBackItem* closest = 0; @@ -8446,7 +8518,7 @@ void Client::BuyBack(int32 item_id, int16 quantity) { sint64 dispFlags = 0; if (item && item->GetItemScript() && lua_interface && lua_interface->RunItemScript(item->GetItemScript(), "buyback_display_flags", item, player, nullptr, &dispFlags) && (dispFlags & DISPLAY_FLAG_NO_BUY)) SimpleMessage(CHANNEL_NARRATIVE, "You do not meet all the requirements to buy this item."); - else if (!player->item_list.HasFreeSlot() && !player->item_list.CanStack(item)) + else if (!player->item_list.HasFreeSlot() && !player->item_list.CanStack(item, false)) SimpleMessage(CHANNEL_COLOR_RED, "You do not have any slots available for this item."); else if (player->RemoveCoins(closest->quantity * closest->price)) { bool removed = false; @@ -8489,9 +8561,15 @@ void Client::BuyBack(int32 item_id, int16 quantity) { void Client::BuyItem(int32 item_id, int16 quantity) { // Get the merchant we are buying from + LogWrite(CCLIENT__ERROR, 0, "Client", "buy item %u quantity %u", item_id, quantity); + Spawn* spawn = GetCurrentZone()->GetSpawnByID(GetMerchantTransactionID()); // Make sure the spawn has a merchant list - if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) { + if(spawn && spawn->GetHouseCharacterID()) { + broker.BuyItem(this, spawn->GetHouseCharacterID(), item_id, quantity); + SendMerchantWindow(spawn, false); + } + else if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) { int32 total_buy_price = 0; float multiplier = CalculateBuyMultiplier(spawn->GetMerchantID()); int32 sell_price = 0; @@ -8538,7 +8616,7 @@ void Client::BuyItem(int32 item_id, int16 quantity) { total_buy_price = sell_price * quantity; item = new Item(master_item); item->details.count = quantity; - if (!player->item_list.HasFreeSlot() && !player->item_list.CanStack(item)) { + if (!player->item_list.HasFreeSlot() && !player->item_list.CanStack(item, false)) { SimpleMessage(CHANNEL_COLOR_RED, "You do not have any slots available for this item."); lua_interface->SetLuaUserDataStale(item); safe_delete(item); @@ -8683,6 +8761,9 @@ void Client::BuyItem(int32 item_id, int16 quantity) { void Client::RepairItem(int32 item_id) { Spawn* spawn = GetCurrentZone()->GetSpawnByID(GetMerchantTransactionID()); if (spawn && (spawn->GetMerchantType() & MERCHANT_TYPE_REPAIR)) { + if((spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)) + return; + Item* item = player->item_list.GetItemFromID(item_id); if (!item) item = player->GetEquipmentList()->GetItemFromItemID(item_id); @@ -8721,6 +8802,9 @@ void Client::RepairItem(int32 item_id) { void Client::RepairAllItems() { Spawn* spawn = GetCurrentZone()->GetSpawnByID(GetMerchantTransactionID()); if (spawn && (spawn->GetMerchantType() & MERCHANT_TYPE_REPAIR)) { + if((spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)) + return; + vector* repairable_items = GetRepairableItems(); if (repairable_items && repairable_items->size() > 0) { vector::iterator itr; @@ -8869,6 +8953,9 @@ void Client::SendAchievementUpdate(bool first_login) { void Client::SendBuyMerchantList(bool sell) { Spawn* spawn = GetCurrentZone()->GetSpawnByID(GetMerchantTransactionID()); if (spawn && spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)) { + if((spawn->GetMerchantType() & MERCHANT_TYPE_LOTTO)) + return; + vector* items = world.GetMerchantItemList(spawn->GetMerchantID(), spawn->GetMerchantType(), player); if (items) { PacketStruct* packet = configReader.getStruct("WS_UpdateMerchant", GetVersion()); @@ -9028,8 +9115,8 @@ void Client::SendSellMerchantList(bool sell) { for (test_itr = items->begin(); test_itr != items->end(); test_itr++) { bool isbagwithitems = false; - if(test_itr->second && (test_itr->second->details.inv_slot_id == -3 || test_itr->second->details.inv_slot_id == -4)) - continue; // omit bank/shared-bank + if(test_itr->second && (test_itr->second->details.inv_slot_id < 0)) + continue; // omit bank/shared-bank/vault/anything with negative inventory slot id if (test_itr->second && test_itr->second->IsBag() && (test_itr->second->details.num_slots - test_itr->second->details.num_free_slots != test_itr->second->details.num_slots)) isbagwithitems = true; @@ -9479,7 +9566,7 @@ void Client::SendGuildCreateWindow() { } } -void Client::AddBuyBack(int32 unique_id, int32 item_id, int16 quantity, int32 price, bool save_needed) { +void Client::AddBuyBack(int64 unique_id, int32 item_id, int16 quantity, int32 price, bool save_needed) { BuyBackItem* item = new BuyBackItem; item->item_id = item_id; item->unique_id = unique_id; @@ -10406,19 +10493,88 @@ void Client::AddSendNewSpells(vector* spells) { } } -void Client::SetItemSearch(vector* items) { +void Client::SetItemSearch(vector* items, map values) { if (items) { - safe_delete(search_items); - search_items = items; + ClearItemSearch(); + { + std::lock_guard L(item_search_mtx_); + search_items = items; + search_values = values; + } } } -vector* Client::GetSearchItems() { - return search_items; +void Client::ClearItemSearch() { + std::lock_guard L(item_search_mtx_); + if(search_items) { + for (auto it = search_items->begin(); it != search_items->end(); /* no increment here */) { + Item* item = *it; + if (item->is_search_store_item) { + safe_delete(item); + // erase returns the next iterator + it = search_items->erase(it); + } else { + ++it; + } + } + } + safe_delete(search_items); +} + +void Client::SendSellerItemByItemUniqueId(int64 unique_id) { + std::lock_guard L(item_search_mtx_); + if (!search_items) return; + + for (Item* item : *search_items) { + if (item && item->details.unique_id == unique_id) { + EQ2Packet* app = item->serialize(GetVersion(), true, GetPlayer()); + QueuePacket(app); + break; + } + } +} + + +void Client::BuySellerItemByItemUniqueId(int64 unique_id, int16 quantity) { + int32 seller_id = 0; + + { + std::lock_guard L(item_search_mtx_); + if (!search_items) return; + + for (Item* item : *search_items) { + if (item && item->details.unique_id == unique_id) { + seller_id = item->seller_char_id; + break; + } + } + } + + if(seller_id) { + if(broker.BuyItem(this, seller_id, unique_id, quantity)) { + vector* items = master_item_list.GetItems(search_values, this); + if(items){ + SetItemSearch(items, search_values); + SearchStore(search_page); + } + } + } + else { + Message(CHANNEL_COLOR_RED, "Could not find seller %u when attempting to buy unique id %u.", seller_id, unique_id); + } +} + +void Client::SetSellerStatus() { + bool sellInv = broker.CanSellFromInventory(GetPlayer()->GetCharacterID()); + bool itemsSelling = broker.IsSellingItems(GetPlayer()->GetCharacterID()); + broker.AddSeller(GetPlayer()->GetCharacterID(), std::string(GetPlayer()->GetName()), + GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, sellInv); } void Client::SearchStore(int32 page) { + std::lock_guard L(item_search_mtx_); + SetSearchPage(page); if (search_items) { PacketStruct* packet = configReader.getStruct("WS_BrokerItems", GetVersion()); if (packet) { @@ -10447,27 +10603,49 @@ void Client::SearchStore(int32 page) { std::string teststr("test "); teststr.append(std::to_string(i)); packet->setArrayDataByName("string_one", teststr.c_str(), i); - packet->setArrayDataByName("string_two", "testtwo", i); - packet->setArrayDataByName("seller_name", "EQ2EMuDev", i); - packet->setArrayDataByName("item_id", item->details.item_id, i); - packet->setArrayDataByName("item_id2", item->details.item_id, i); + if(IsGMStoreSearch()) { + packet->setArrayDataByName("seller_name", "EQ2EMuDev", i); + packet->setArrayDataByName("item_id", item->details.item_id, i); + packet->setArrayDataByName("item_id2", item->details.item_id, i); + packet->setArrayDataByName("sell_price", item->sell_price, i); + if (item->stack_count == 0) + packet->setArrayDataByName("quantity", 1, i); + else + packet->setArrayDataByName("quantity", item->stack_count, i); + packet->setArrayDataByName("string_two", "testtwo", i); + } + else { + packet->setArrayDataByName("seller_name", item->seller_name.c_str(), i); + packet->setArrayDataByName("item_id", item->details.unique_id, i); + packet->setArrayDataByName("item_id2", item->details.unique_id, i); + packet->setArrayDataByName("sell_price", item->broker_price, i); + packet->setArrayDataByName("quantity", item->details.count, i); + if(item->seller_house_id) { + HouseZone* hz = world.GetHouseZone(item->seller_house_id); + if(hz && item->seller_name.size() > 0) { + string name; + name = item->seller_name; + name.append("'s "); + name.append(hz->name); + packet->setArrayDataByName("string_two", name.c_str(), i); + } + } + } + packet->setArrayDataByName("icon", item->GetIcon(GetVersion()), i); //packet->setArrayDataByName("unknown2b", i, i); packet->setArrayDataByName("item_seller_id", 1, i); - if (item->stack_count == 0) - packet->setArrayDataByName("quantity", 1, i); - else - packet->setArrayDataByName("quantity", item->stack_count, i); packet->setArrayDataByName("stack_size", item->stack_count, i); - packet->setArrayDataByName("sell_price", item->sell_price, i); std::string tmpStr(""); tmpStr.append(item->name.c_str()); - tmpStr.append(" ("); - tmpStr.append(std::to_string(item->details.item_id)); - tmpStr.append(")"); + if(IsGMStoreSearch()) { + tmpStr.append(" ("); + tmpStr.append(std::to_string(item->details.item_id)); + tmpStr.append(")"); + } packet->setArrayDataByName("item_name", tmpStr.c_str(), i); packet->setArrayDataByName("req_level", item->generic_info.adventure_default_level, i); @@ -12035,6 +12213,9 @@ bool Client::HandleNewLogin(int32 account_id, int32 access_code) new_client_login = NewLoginState::LOGIN_ALLOWED; } + // vault slots + RefreshVaultSlotCount(); + const char* zone_script = world.GetZoneScript(GetCurrentZone()->GetZoneID()); if (zone_script && lua_interface) lua_interface->RunZoneScript(zone_script, "new_client", GetCurrentZone(), GetPlayer()); @@ -12337,6 +12518,126 @@ void Client::SendDefaultCommand(Spawn* spawn, const char* command, float distanc } } +void Client::RefreshVaultSlotCount() { + std::vector houses = world.GetAllPlayerHouses(GetCharacterID()); + if (!houses.empty()) { + int8 bestSlots = 0; + int32 bestZoneID = 0; + + for (PlayerHouse* ph : houses) { + if (!ph) + continue; + HouseZone* hz = world.GetHouseZone(ph->house_id); + if (!hz) + continue; + + if (hz->vault_slots > bestSlots) { + bestSlots = hz->vault_slots; + bestZoneID = ph->house_id; + } + } + + if (bestZoneID != 0) { + GetPlayer()->GetPlayerInfo()->SetHouseZone(bestZoneID); + GetPlayer()->SetHouseVaultSlots(bestSlots); + } + } +} + + +/* +** + October 18, 2005, 07:17:51 PM Rocawne buys Elemental Vestment (Adept I) for 1 Platinum, 0 Gold, 0 Silver, 0 Copper +October 19, 2005, 02:57:20 AM Radian buys A Rusting Gear for 5 Gold, 75 Silver, 0 Copper + +but then they stop outputting coin that equals 0 + +October 20, 2005, 05:29:33 AM Kilea buys Rules of the Sandscrawler Clan - Page 7 for 9 Gold, 75 Silver +October 21, 2005, 12:29:41 AM Meldo buys Oozing Wound (Apprentice IV) for 10 Gold + +February 14, 2006, 02:59:32 PM Tom buys 13 pu-erh tea leafes for 4 Gold, 55 Silver +**/ +void Client::SendHouseSaleLog(std::string message, int64 coin_session, int64 coin_total, int8 flag) { + PacketStruct* packet = configReader.getStruct("WS_HouseStoreLog", GetVersion()); + if (packet) { + packet->setDataByName("data", message.c_str()); + packet->setDataByName("coin_gain_session", coin_session); + packet->setDataByName("coin_gain_alltime", coin_total); + packet->setDataByName("sales_log_open", flag); + EQ2Packet* outapp = packet->serialize(); + QueuePacket(outapp); + safe_delete(packet); + } +} + +void Client::SetItemSaleStatus(int64 unique_id, bool status) { + broker.SetSaleStatus(GetPlayer()->GetCharacterID(), unique_id, status); + OpenShopWindow(nullptr); +} + +void Client::OpenShopWindow(Spawn* interaction, bool sendAlways, int8 saleLogOnly) { + if (GetCurrentZone()->GetInstanceType() == PERSONAL_HOUSE_INSTANCE && !HasOwnerOrEditAccess()) { + SimpleMessage(CHANNEL_COLOR_RED, "This is not your home!"); + return; + } + + auto info = broker.GetSellerInfo(GetPlayer()->GetCharacterID()); + bool wasOpen = false; + + if(!saleLogOnly) { + wasOpen = GetShopWindowStatus(); + + if(sendAlways) + SetShopWindowStatus(true); + + if(!GetShopWindowStatus() && !sendAlways) + return; // don't send window is not open + + if(!info) + broker.AddSeller(GetPlayer()->GetCharacterID(), std::string(GetPlayer()->GetName()), GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, false); + + GetPlayer()->item_list.GetVaultItems(this, interaction ? interaction->GetID() : GetPlayer()->GetID(), GetPlayer()->GetHouseVaultSlots(), info ? info->sell_from_inventory : false); + + broker.LockActiveItemsForClient(this); + } + + if(!wasOpen && info) { + int64 coin_session = database.GetSellerSession(GetPlayer()->GetCharacterID()); + if(coin_session) { + broker.ResetSellerSessionCoins(GetPlayer()->GetCharacterID()); + std::string msg = FormatCoinReceiveMessage(coin_session, "consigned sales"); + broker.LogSaleMessage(GetPlayer()->GetCharacterID(), msg); + GetPlayer()->AddCoins(coin_session); + SimpleMessage(CHANNEL_NARRATIVE, msg.c_str()); + SendHouseSaleLog(msg, info ? coin_session : 0, info ? info->coin_total : 0, saleLogOnly); + } + else { + SendHouseSaleLog("", 0, info ? info->coin_total : 0, saleLogOnly); + } + } +} + +void Client::SetItemSaleCost(int64 unique_id, int32 platinum, int32 gold, int32 silver, int32 copper) { + int64 cost = platinum * 1000000 + gold * 10000 + silver * 100 + copper; + broker.SetSalePrice(GetPlayer()->GetCharacterID(), unique_id, cost); + OpenShopWindow(nullptr); +} + +void Client::AddItemSale(int64 unique_id, int32 item_id, int64 price, int32 inv_slot_id, int16 slot_id, int16 count, bool inInventory, bool forSale, std::string itemCreator) { + SaleItem it{}; + it.unique_id = unique_id; + it.character_id = GetPlayer()->GetCharacterID(); + it.house_id = GetPlayer()->GetPlayerInfo()->GetHouseZoneID(); + it.item_id = item_id; + it.cost_copper = price; + it.for_sale = forSale; + it.inv_slot_id = inv_slot_id; + it.slot_id = slot_id; + it.count = count; + it.from_inventory = inInventory; + it.creator = itemCreator; + broker.AddItem(it); +} bool Client::HandleHouseEntityCommands(Spawn* spawn, int32 spawnid, string command) { if (GetCurrentZone()->GetInstanceType() != PERSONAL_HOUSE_INSTANCE) @@ -12369,26 +12670,12 @@ bool Client::PopulateHouseSpawn(PacketStruct* place_object) { Spawn* tmp = GetTempPlacementSpawn(); - int32 spawn_group_id = database.GetNextSpawnLocation(); + int32 spawn_group_id = database.GetNextSpawnLocation(true); tmp->SetSpawnLocationID(spawn_group_id); float newHeading = place_object->getType_float_ByName("heading") + 180; int32 spawnDBID = 0; - if (GetCurrentZone()->house_object_database_lookup.count(tmp->GetModelType()) > 0) - { - spawnDBID = GetCurrentZone()->house_object_database_lookup.Get(tmp->GetModelType()); - tmp->SetDatabaseID(spawnDBID); - } - else - { - spawnDBID = database.FindHouseInstanceSpawn(tmp); - if (spawnDBID) - { - GetCurrentZone()->house_object_database_lookup.Put(tmp->GetModelType(), spawnDBID); - tmp->SetDatabaseID(spawnDBID); - } - } tmp->SetX(place_object->getType_float_ByName("x")); tmp->SetY(place_object->getType_float_ByName("y")); @@ -12404,7 +12691,6 @@ bool Client::PopulateHouseSpawn(PacketStruct* place_object) if (!spawnDBID) { - GetCurrentZone()->house_object_database_lookup.Put(tmp->GetModelType(), tmp->GetDatabaseID()); // we need to copy as to not delete the ZoneServer object_list entry this on house item pickup GetCurrentZone()->AddObject(tmp->GetDatabaseID(), ((Object*)tmp)->Copy()); } @@ -12423,33 +12709,41 @@ bool Client::PopulateHouseSpawnFinalize() GetCurrentZone()->AddSpawn(tmp); GetCurrentZone()->SendSpawnChanges(tmp, this); SetTempPlacementSpawn(nullptr); - int32 uniqueID = GetPlacementUniqueItemID(); - Item* uniqueItem = GetPlayer()->item_list.GetItemFromUniqueID(uniqueID); + int64 uniqueID = GetPlacementUniqueItemID(); + Item* uniqueItem = GetPlayer()->item_list.GetVaultItemFromUniqueID(uniqueID); + if(!uniqueItem) { + Message(CHANNEL_COLOR_RED, "Missing unique item!"); + return false; + } + if(!uniqueItem->TryLockItem(LockReason::LockReason_House)) { + Message(CHANNEL_COLOR_RED, "Item could not be locked for house placement!"); + return false; + } + else { + QueuePacket(GetPlayer()->SendInventoryUpdate(GetVersion())); + } + tmp->SetPickupItemID(uniqueItem->details.item_id); tmp->SetPickupUniqueItemID(uniqueID); - - if (uniqueItem) + if (GetCurrentZone()->GetInstanceType() == PERSONAL_HOUSE_INSTANCE) { - if (GetCurrentZone()->GetInstanceType() == PERSONAL_HOUSE_INSTANCE) - { - Query query; - query.RunQuery2(Q_INSERT, "insert into spawn_instance_data set spawn_id = %u, spawn_location_id = %u, pickup_item_id = %u, pickup_unique_item_id = %u", tmp->GetDatabaseID(), tmp->GetSpawnLocationID(), tmp->GetPickupItemID(), uniqueID); - } - - if (uniqueItem->GetItemScript() && - lua_interface->RunItemScript(uniqueItem->GetItemScript(), "placed", uniqueItem, GetPlayer(), tmp)) - { - uniqueItem = GetPlayer()->item_list.GetItemFromUniqueID(uniqueID); - } - - if (uniqueItem) { - database.DeleteItem(GetCharacterID(), uniqueItem, 0); - GetPlayer()->item_list.RemoveItem(uniqueItem, true); - QueuePacket(GetPlayer()->SendInventoryUpdate(GetVersion())); - } - - SetPlacementUniqueItemID(0); + Query query; + query.RunQuery2(Q_INSERT, "insert into spawn_instance_data set spawn_id = %u, spawn_location_id = %u, pickup_item_id = %u, pickup_unique_item_id = %u", tmp->GetDatabaseID(), tmp->GetSpawnLocationID(), tmp->GetPickupItemID(), uniqueID); } + + if (uniqueItem->GetItemScript() && + lua_interface->RunItemScript(uniqueItem->GetItemScript(), "placed", uniqueItem, GetPlayer(), tmp)) + { + uniqueItem = GetPlayer()->item_list.GetItemFromUniqueID(uniqueID); + } + + if (uniqueItem && uniqueItem->generic_info.item_type != ITEM_TYPE_HOUSE_CONTAINER) { + database.DeleteItem(GetCharacterID(), uniqueItem, 0); + GetPlayer()->item_list.RemoveItem(uniqueItem, true); + QueuePacket(GetPlayer()->SendInventoryUpdate(GetVersion())); + } + + SetPlacementUniqueItemID(0); return true; } @@ -13648,6 +13942,18 @@ bool Client::GetHouseZoneServer(ZoneChangeDetails* zone_details, int32 spawn_id, hz = world.GetHouseZone(ph->house_id); } } + if(!ph) { + Spawn* houseWidget = GetPlayer()->GetSpawnByIndex(house_id); + if (houseWidget && houseWidget->IsWidget() && ((Widget*)houseWidget)->GetHouseID()) { + hz = world.GetHouseZone(((Widget*)houseWidget)->GetHouseID()); + if (hz) { + ph = world.GetPlayerHouseByHouseID(GetPlayer()->GetCharacterID(), hz->id); + } + else { + Message(CHANNEL_COLOR_YELLOW, "HouseWidget#2 spawn index %u house zone could not be found.", spawn_id); + } + } + } if (ph && hz) { if (zone_list.GetZoneByInstance(zone_details, ph->instance_id, hz->zone_id)) { @@ -13770,4 +14076,144 @@ bool Client::SendDialogChoice(int32 spawnID, const std::string& windowTextPrompt safe_delete(p); return true; +} + +void Client::SendMerchantWindow(Spawn* spawn, bool sell) { + if(GetVersion() < 561) { + sell = false; // doesn't support in the same way as AoM just open the normal buy/sell window + } + if(spawn) { + SetMerchantTransaction(spawn); + if (spawn->GetHouseCharacterID() > 0) { + if (auto info = broker.GetSellerInfo(spawn->GetHouseCharacterID())) { + PacketStruct* packet = configReader.getStruct("WS_UpdateMerchant", GetVersion()); + if (packet) { + packet->setDataByName("spawn_id", GetPlayer()->GetIDWithPlayerSpawn(spawn)); + int32 i = 0; + int tmp_level = 0; + sint8 item_difficulty = 0; + auto items = broker.GetActiveForSaleItems(spawn->GetHouseCharacterID()); + int32 itemCount = 0; + for (auto const& itm : items) { + if(itm.inv_slot_id != spawn->GetPickupUniqueItemID()) + continue; + if (!itm.for_sale) { + continue; + } + itemCount++; + } + packet->setArrayLengthByName("num_items", itemCount); + + for (auto const& itm : items) { + if(itm.inv_slot_id != spawn->GetPickupUniqueItemID()) + continue; + if (!itm.for_sale) { + continue; + } + + Item* item = master_item_list.GetItem(itm.item_id); + if (!item) + continue; + + packet->setArrayDataByName("item_name", item->name.c_str(), i); + packet->setArrayDataByName("item_id", itm.unique_id, i); + //packet->setArrayDataByName("unique_item_id", item->details.item_id, i); + packet->setArrayDataByName("stack_size", itm.count, i); + packet->setArrayDataByName("icon", item->GetIcon(GetVersion()), i); + if (item->generic_info.adventure_default_level > 0) + tmp_level = item->generic_info.adventure_default_level; + else + tmp_level = item->generic_info.tradeskill_default_level; + packet->setArrayDataByName("level", tmp_level, i); + if (rule_manager.GetZoneRule(GetCurrentZoneID(), R_World, DisplayItemTiers)->GetBool()) { + packet->setArrayDataByName("tier", item->details.tier, i); + } + packet->setArrayDataByName("item_id2", item->details.item_id, i); + item_difficulty = player->GetArrowColor(tmp_level); + if (item_difficulty != ARROW_COLOR_WHITE && item_difficulty != ARROW_COLOR_RED && item_difficulty != ARROW_COLOR_GRAY) + item_difficulty = ARROW_COLOR_WHITE; + + sint64 overrideValue = 0; + if (item->GetItemScript() && lua_interface && lua_interface->RunItemScript(item->GetItemScript(), "item_difficulty", item, player, nullptr, &overrideValue)) + item_difficulty = (sint8)overrideValue; + + item_difficulty -= 6; + if (item_difficulty < 0) + item_difficulty *= -1; + + packet->setArrayDataByName("item_difficulty", item_difficulty, i); + packet->setArrayDataByName("quantity", 1, i); + packet->setArrayDataByName("unknown5", 255, i); + packet->setArrayDataByName("stack_size2", itm.count, i); + + sint64 dispFlags = 0; + if (item->GetItemScript() && lua_interface && lua_interface->RunItemScript(item->GetItemScript(), "buy_display_flags", item, player, nullptr, &dispFlags)) + packet->setArrayDataByName("display_flags", (int8)dispFlags, i); + + std::string overrideValueStr; + // classic client isn't properly tracking this field, DoF we don't have it identified yet, but no field to cause any issues (can add later if identified) + if (GetVersion() >= 546 && item->GetItemScript() && lua_interface && lua_interface->RunItemScriptWithReturnString(item->GetItemScript(), "item_description", item, player, &overrideValueStr)) + packet->setArrayDataByName("description", overrideValueStr.c_str(), i); + + packet->setArrayDataByName("price", itm.cost_copper, i); + + i++; + } + } + + if (GetVersion() < 561) { + //buy is 0 so dont need to set it + if (sell) + packet->setDataByName("type", 1); + } + else if (GetVersion() == 561) { + packet->setDataByName("type", 2); + } + else { + if (sell) + packet->setDataByName("type", 130); + else + packet->setDataByName("type", 2); + } + EQ2Packet* outapp = packet->serialize(); + QueuePacket(outapp); + safe_delete(packet); + if(GetVersion() > 561) { + PacketStruct* packet = configReader.getStruct("WS_UpdateMerchant", GetVersion()); + if (packet) { + packet->setDataByName("spawn_id", 0xFFFFFFFF); + packet->setDataByName("type", 16); + EQ2Packet* outapp = packet->serialize(); + if (outapp) + QueuePacket(outapp); + safe_delete(packet); + } + } + } + } + else if (spawn->GetMerchantID() > 0 && spawn->IsClientInMerchantLevelRange(this)){ + SendHailCommand(spawn); + //MerchantFactionMultiplier* multiplier = world.GetMerchantMultiplier(spawn->GetMerchantID()); + //if(!multiplier || (multiplier && GetPlayer()->GetFactions()->GetFactionValue(multiplier->faction_id) >= multiplier->faction_min)){ + SendBuyMerchantList(sell); + if(!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY)) + SendSellMerchantList(sell); + if(!(spawn->GetMerchantType() & MERCHANT_TYPE_NO_BUY_BACK)) + SendBuyBackList(sell); + + if(GetVersion() > 561) { + PacketStruct* packet = configReader.getStruct("WS_UpdateMerchant", GetVersion()); + if (packet) { + packet->setDataByName("spawn_id", 0xFFFFFFFF); + packet->setDataByName("type", 16); + EQ2Packet* outapp = packet->serialize(); + if (outapp) + QueuePacket(outapp); + safe_delete(packet); + } + } + } + if (spawn->GetMerchantType() & MERCHANT_TYPE_REPAIR) + SendRepairList(); + } } \ No newline at end of file diff --git a/source/WorldServer/client.h b/source/WorldServer/client.h index 6650e06..2f20e74 100644 --- a/source/WorldServer/client.h +++ b/source/WorldServer/client.h @@ -1,22 +1,23 @@ -/* - EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) +/* + 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. + 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 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. + 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 . + You should have received a copy of the GNU General Public License + along with EQ2Emulator. If not, see . */ + #ifndef CLIENT_H #define CLIENT_H @@ -77,7 +78,7 @@ struct QueuedQuest { struct BuyBackItem { int32 item_id; - int32 unique_id; + int64 unique_id; int16 quantity; int32 price; bool save_needed; @@ -340,6 +341,7 @@ public: inline int32 GetAccountID() { return account_id; } inline const char* GetAccountName() { return account_name; } inline sint16 GetAdminStatus() { return admin_status; } + inline bool IsGMStoreSearch() { return gm_store_search; } inline int16 GetVersion() { return version; } void SetNameCRC(int32 val) { name_crc = val; } int32 GetNameCRC() { return name_crc; } @@ -348,8 +350,8 @@ public: void SetVersion(int16 new_version) { version = new_version; } void SetAccountID(int32 in_accountid) { account_id = in_accountid; } void SetCharacterID(int32 in_characterid) { character_id = in_characterid; } - void SetAdminStatus(sint16 in_status) { admin_status = in_status; } - + void SetAdminStatus(sint16 in_status) { admin_status = in_status; SetGMStoreSearch(false); } + void SetGMStoreSearch(bool setting_val) { gm_store_search = setting_val; } void DetermineCharacterUpdates(); @@ -429,7 +431,7 @@ public: void BuyBack(int32 item_id, int16 quantity); void RepairItem(int32 item_id); void RepairAllItems(); - void AddBuyBack(int32 unique_id, int32 item_id, int16 quantity, int32 price, bool save_needed = true); + void AddBuyBack(int64 unique_id, int32 item_id, int16 quantity, int32 price, bool save_needed = true); deque* GetBuyBacks(); vector* GetRepairableItems(); vector* GetItemsByEffectType(ItemEffectType type, ItemEffectType secondary_effect = NO_EFFECT_TYPE); @@ -452,9 +454,13 @@ public: void SendNewAdventureSpells(); void SendNewTradeskillSpells(); string GetCoinMessage(int32 total_coins); - void SetItemSearch(vector* items); - vector* GetSearchItems(); + void SetItemSearch(vector* items, map values); + void ClearItemSearch(); + void SearchStore(int32 page); + void SendSellerItemByItemUniqueId(int64 unique_id); + void BuySellerItemByItemUniqueId(int64 unique_id, int16 quantity); + void SetSellerStatus(); void SetPlayer(Player* new_player); void AddPendingQuestAcceptReward(Quest* quest); @@ -564,12 +570,20 @@ public: Spawn* GetTempPlacementSpawn() { return tempPlacementSpawn; } - void SetPlacementUniqueItemID(int32 id) { placement_unique_item_id = id; } - int32 GetPlacementUniqueItemID() { return placement_unique_item_id; } + void SetPlacementUniqueItemID(int64 id) { placement_unique_item_id = id; } + int64 GetPlacementUniqueItemID() { return placement_unique_item_id; } void SetHasOwnerOrEditAccess(bool val) { hasOwnerOrEditAccess = val; } bool HasOwnerOrEditAccess() { return hasOwnerOrEditAccess; } - + void RefreshVaultSlotCount(); + void SendHouseSaleLog(std::string message, int64 coin_session, int64 coin_total, int8 flag); + void SetItemSaleStatus(int64 unique_id, bool status); + void OpenShopWindow(Spawn* interaction, bool sendAlways = false, int8 saleLogOnly = 0); + void SetShopWindowStatus(bool status) { shop_window_open = status; } + bool GetShopWindowStatus() { return shop_window_open; }// false for disabled , true for enabled + void SetItemSaleCost(int64 unique_id, int32 platinum, int32 gold, int32 silver, int32 copper); + void AddItemSale(int64 unique_id, int32 item_id, int64 price, int32 inv_slot_id, int16 slot_id, int16 count, bool inInventory, bool forSale, std::string itemCreator); + bool HandleHouseEntityCommands(Spawn* spawn, int32 spawnid, string command); // find an appropriate spawn to use for the house object, save spawn location/entry data to DB bool PopulateHouseSpawn(PacketStruct* place_object); @@ -725,6 +739,10 @@ public: void SetLastTellName(std::string tellName) { last_tell_name = tellName; } std::string GetLastTellName() { return last_tell_name; } + + void SetSearchPage(int32 page) { search_page = page; } + + void SendMerchantWindow(Spawn* spawn, bool sell); DialogManager dialog_manager; private: void AddRecipeToPlayerPack(Recipe* recipe, PacketStruct* packet, int16* i); @@ -745,6 +763,8 @@ private: Mutex MQuestQueue; Mutex MDeletePlayer; vector* search_items; + map search_values; + int32 search_page; int32 waypoint_id = 0; map waypoints; int32 transport_spawn_id; @@ -775,6 +795,7 @@ private: int32 account_id; int32 character_id; sint16 admin_status; // -2 Banned, -1 Suspended, 0 User, etc. + bool gm_store_search; char account_name[64]; char zone_name[64]; int32 zoneID; @@ -811,8 +832,9 @@ private: float zoning_y; float zoning_z; float zoning_h; - bool firstlogin; - + std::atomic firstlogin; + std::atomic firstlogin_transmit; + enum NewLoginState { LOGIN_NONE, LOGIN_DELAYED, LOGIN_ALLOWED, LOGIN_INITIAL_LOAD, LOGIN_SEND }; NewLoginState new_client_login; // 1 = delayed state, 2 = let client in Timer underworld_cooldown_timer; @@ -857,7 +879,7 @@ private: int32 delayedAccessKey; Timer delayTimer; Spawn* tempPlacementSpawn; - int32 placement_unique_item_id; + int64 placement_unique_item_id; bool hasOwnerOrEditAccess; bool hasSentTempPlacementSpawn; @@ -891,6 +913,10 @@ private: int recipe_orig_packet_size; std::string last_tell_name; + + mutable std::mutex item_search_mtx_; + std::atomic shop_window_open; + map str_values; }; class ClientList { diff --git a/source/WorldServer/net.cpp b/source/WorldServer/net.cpp index 269def4..84e458e 100644 --- a/source/WorldServer/net.cpp +++ b/source/WorldServer/net.cpp @@ -1,6 +1,6 @@ /* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #include "../common/debug.h" #include "../common/Log.h" @@ -101,6 +102,7 @@ LuaInterface* lua_interface = new LuaInterface(); #include "Titles.h" #include "Languages.h" #include "Achievements/Achievements.h" +#include "./Broker/BrokerManager.h" volatile bool RunLoops = true; sint32 numclients = 0; @@ -118,7 +120,6 @@ extern Variables variables; extern PeerManager peer_manager; extern HTTPSClientPool peer_https_pool; ConfigReader configReader; -int32 MasterItemList::next_unique_id = 0; int last_signal = 0; RuleManager rule_manager; MasterTitlesList master_titles_list; @@ -126,7 +127,7 @@ MasterLanguagesList master_languages_list; ChestTrapList chest_trap_list; extern MasterAchievementList master_achievement_list; extern map EQOpcodeVersions; - +extern BrokerManager broker; ThreadReturnType ItemLoad (void* tmp); ThreadReturnType AchievmentLoad (void* tmp); @@ -283,7 +284,6 @@ int main(int argc, char** argv) { // JA: Load all Item info LogWrite(ITEM__INFO, 0, "Items", "Loading Items..."); database.LoadItemList(); - MasterItemList::ResetUniqueID(database.LoadNextUniqueItemID()); LogWrite(SPELL__INFO, 0, "Spells", "Loading Spells..."); database.LoadSpells(); @@ -311,9 +311,6 @@ int main(int argc, char** argv) { database.LoadMerchantInformation(); } - LogWrite(GUILD__INFO, 0, "Guilds", "Loading Guilds..."); - database.LoadGuilds(); - LogWrite(TRADESKILL__INFO, 0, "Recipes", "Loading Recipe Books..."); database.LoadRecipeBooks(); LogWrite(TRADESKILL__INFO, 0, "Recipes", "Loading Recipes..."); @@ -344,7 +341,6 @@ int main(int argc, char** argv) { LogWrite(WORLD__INFO, 0, "World", "Loading House Zone Data..."); database.LoadHouseZones(); - database.LoadPlayerHouses(); LogWrite(WORLD__INFO, 0, "World", "Loading Heroic OP Data..."); database.LoadHOStarters(); @@ -368,10 +364,21 @@ int main(int argc, char** argv) { Sleep(10); LogWrite(WORLD__INFO, 0, "World", "Load threads finished."); } + + LogWrite(GUILD__INFO, 0, "Guilds", "Loading Guilds..."); + database.LoadGuilds(); + + LogWrite(WORLD__INFO, 0, "World", "Loading Player House Data..."); + database.LoadPlayerHouses(); LogWrite(WORLD__INFO, 0, "World", "Total World startup time: %u seconds.", Timer::GetUnixTimeStamp() - t_total); int ret_code = 0; if (eqsf.Open(net.GetWorldPort())) { + world.world_loaded = true; // need this set ahead so peering starts sending data also + + LogWrite(WORLD__INFO, 0, "World", "Loading Broker Data..."); + database.LoadBrokerData(broker); + if (strlen(net.GetWorldAddress()) == 0) LogWrite(NET__INFO, 0, "Net", "World server listening on port %i", net.GetWorldPort()); else @@ -380,7 +387,6 @@ int main(int argc, char** argv) { if(strlen(net.GetInternalWorldAddress())>0) LogWrite(NET__INFO, 0, "Net", "World server listening on: %s:%i", net.GetInternalWorldAddress(), net.GetWorldPort()); - world.world_loaded = true; world.world_uptime = getCurrentTimestamp(); #ifdef WIN32 _beginthread(StartPeerPoll, 0, NULL); @@ -558,8 +564,8 @@ ThreadReturnType ItemLoad (void* tmp) LogWrite(ITEM__INFO, 0, "Items", "Loading Items..."); db.LoadItemList(); - MasterItemList::ResetUniqueID(db.LoadNextUniqueItemID()); - + db.ResetNextUniqueItemID(); + // Relies on the item list so needs to be in the item thread LogWrite(COLLECTION__INFO, 0, "Collect", "Loading Collections..."); db.LoadCollections(); diff --git a/source/WorldServer/zoneserver.cpp b/source/WorldServer/zoneserver.cpp index 27e50b6..8b87ea3 100644 --- a/source/WorldServer/zoneserver.cpp +++ b/source/WorldServer/zoneserver.cpp @@ -31,6 +31,7 @@ using namespace std; #include "Commands/Commands.h" #include "Zone/pathfinder_interface.h" #include "NPC_AI.h" +#include "Broker/BrokerManager.h" #ifdef WIN32 #include @@ -116,7 +117,7 @@ extern Chat chat; extern MasterRaceTypeList race_types_list; extern MasterSpellList master_spell_list; // temp - remove later extern MasterSkillList master_skill_list; - +extern BrokerManager broker; int32 MinInstanceID = 1000; @@ -1641,9 +1642,30 @@ bool ZoneServer::Process() LogWrite(WIDGET__INFO, 0, "Widget", "-Loading Widget data..."); database.LoadWidgets(this); LogWrite(WIDGET__INFO, 0, "Widget", "-Load Widget data complete!"); - + LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Loading Groundspawn data..."); database.LoadGroundSpawns(this); + LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Load Groundspawn data complete!"); + + if(GetInstanceType() == Instance_Type::PERSONAL_HOUSE_INSTANCE) { + LogWrite(NPC__INFO, 0, "NPC", "-Loading House NPC data..."); + database.LoadNPCs(this, true); + LogWrite(NPC__INFO, 0, "NPC", "-Load House NPC data complete!"); + + LogWrite(OBJECT__INFO, 0, "Object", "-Loading House Object data..."); + database.LoadObjects(this, true); + LogWrite(OBJECT__INFO, 0, "Object", "-Load House Object data complete!"); + + LogWrite(SIGN__INFO, 0, "Sign", "-Loading House Sign data..."); + database.LoadSigns(this, true); + LogWrite(SIGN__INFO, 0, "Sign", "-Load House Sign data complete!"); + + LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Loading House Groundspawn data..."); + database.LoadGroundSpawns(this, true); + LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Load House Groundspawn data complete!"); + } + + LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Load Groundspawn entries..."); database.LoadGroundSpawnEntries(this); LogWrite(GROUNDSPAWN__INFO, 0, "GSpawn", "-Load Groundspawn data complete!"); @@ -2542,23 +2564,25 @@ Spawn* ZoneServer::ProcessSpawnLocation(SpawnLocation* spawnlocation, mapentities[i]->spawn_type == SPAWN_ENTRY_TYPE_NPC) - spawnTime = database.CheckSpawnRemoveInfo(instNPCs,spawnlocation->entities[i]->spawn_location_id); - else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_OBJECT) - spawnTime = database.CheckSpawnRemoveInfo(instObjSpawns,spawnlocation->entities[i]->spawn_location_id); - else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_WIDGET) - spawnTime = database.CheckSpawnRemoveInfo(instWidgetSpawns,spawnlocation->entities[i]->spawn_location_id); - else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_SIGN) - spawnTime = database.CheckSpawnRemoveInfo(instSignSpawns,spawnlocation->entities[i]->spawn_location_id); - else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_GROUNDSPAWN) - spawnTime = database.CheckSpawnRemoveInfo(instGroundSpawns,spawnlocation->entities[i]->spawn_location_id); - - if(spawnTime == 0) { // don't respawn - return nullptr; - } - else if(spawnTime > 1) { // if not 1, respawn after time - AddRespawn(spawnlocation->entities[i]->spawn_location_id, spawnTime); - return nullptr; + if(GetInstanceType() != PERSONAL_HOUSE_INSTANCE) { + if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_NPC) + spawnTime = database.CheckSpawnRemoveInfo(instNPCs,spawnlocation->entities[i]->spawn_location_id); + else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_OBJECT) + spawnTime = database.CheckSpawnRemoveInfo(instObjSpawns,spawnlocation->entities[i]->spawn_location_id); + else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_WIDGET) + spawnTime = database.CheckSpawnRemoveInfo(instWidgetSpawns,spawnlocation->entities[i]->spawn_location_id); + else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_SIGN) + spawnTime = database.CheckSpawnRemoveInfo(instSignSpawns,spawnlocation->entities[i]->spawn_location_id); + else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_GROUNDSPAWN) + spawnTime = database.CheckSpawnRemoveInfo(instGroundSpawns,spawnlocation->entities[i]->spawn_location_id); + + if(spawnTime == 0) { // don't respawn + return nullptr; + } + else if(spawnTime > 1) { // if not 1, respawn after time + AddRespawn(spawnlocation->entities[i]->spawn_location_id, spawnTime); + return nullptr; + } } if (spawnlocation->conditional > 0) { @@ -2642,19 +2666,19 @@ Spawn* ZoneServer::ProcessInstanceSpawnLocation(SpawnLocation* spawnlocation, ma if(spawnlocation->entities[i]->spawn_percentage >= rand_number) { if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_NPC && - (spawnTime = database.CheckSpawnRemoveInfo(instNPCs,spawnlocation->entities[i]->spawn_location_id)) > 0) + (GetInstanceType() == PERSONAL_HOUSE_INSTANCE || (spawnTime = database.CheckSpawnRemoveInfo(instNPCs,spawnlocation->entities[i]->spawn_location_id)) > 0)) spawn = AddNPCSpawn(spawnlocation, spawnlocation->entities[i]); else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_GROUNDSPAWN && - (spawnTime = database.CheckSpawnRemoveInfo(instGroundSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0) + (GetInstanceType() == PERSONAL_HOUSE_INSTANCE || (spawnTime = database.CheckSpawnRemoveInfo(instGroundSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0)) spawn = AddGroundSpawn(spawnlocation, spawnlocation->entities[i]); else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_OBJECT && - (spawnTime = database.CheckSpawnRemoveInfo(instObjSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0) + (GetInstanceType() == PERSONAL_HOUSE_INSTANCE || (spawnTime = database.CheckSpawnRemoveInfo(instObjSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0)) spawn = AddObjectSpawn(spawnlocation, spawnlocation->entities[i]); else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_WIDGET && - (spawnTime = database.CheckSpawnRemoveInfo(instWidgetSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0) + (GetInstanceType() == PERSONAL_HOUSE_INSTANCE || (spawnTime = database.CheckSpawnRemoveInfo(instWidgetSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0)) spawn = AddWidgetSpawn(spawnlocation, spawnlocation->entities[i]); else if(spawnlocation->entities[i]->spawn_type == SPAWN_ENTRY_TYPE_SIGN && - (spawnTime = database.CheckSpawnRemoveInfo(instSignSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0) + (GetInstanceType() == PERSONAL_HOUSE_INSTANCE || (spawnTime = database.CheckSpawnRemoveInfo(instSignSpawns,spawnlocation->entities[i]->spawn_location_id)) > 0)) spawn = AddSignSpawn(spawnlocation, spawnlocation->entities[i]); if(spawn && spawn->IsOmittedByDBFlag()) @@ -2669,26 +2693,28 @@ Spawn* ZoneServer::ProcessInstanceSpawnLocation(SpawnLocation* spawnlocation, ma database.GetHouseSpawnInstanceData(this, spawn); const char* script = 0; - - for(int x=0;x<3;x++) - { - switch(x) + + if(spawn && !spawn->GetSpawnScript()) { + for(int x=0;x<3;x++) { - case 0: - script = world.GetSpawnEntryScript(spawnlocation->entities[i]->spawn_entry_id); - break; - case 1: - script = world.GetSpawnLocationScript(spawnlocation->entities[i]->spawn_location_id); - break; - case 2: - script = world.GetSpawnScript(spawnlocation->entities[i]->spawn_id); - break; - } + switch(x) + { + case 0: + script = world.GetSpawnEntryScript(spawnlocation->entities[i]->spawn_entry_id); + break; + case 1: + script = world.GetSpawnLocationScript(spawnlocation->entities[i]->spawn_location_id); + break; + case 2: + script = world.GetSpawnScript(spawnlocation->entities[i]->spawn_id); + break; + } - if(spawn && script && lua_interface->GetSpawnScript(script) != 0) - { - spawn->SetSpawnScript(string(script)); - break; + if(script && lua_interface->GetSpawnScript(script) != 0) + { + spawn->SetSpawnScript(string(script)); + break; + } } } @@ -3610,7 +3636,10 @@ void ZoneServer::RemoveClient(Client* client) bool dismissPets = false; if(client) - { + { + if(client->GetPlayer() && client->GetPlayer()->GetCharacterID()) + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, false); + if (client->GetPlayer()) client_list.RemovePlayerFromInvisHistory(client->GetPlayer()->GetID()); @@ -3705,6 +3734,8 @@ void ZoneServer::RemoveClientImmediately(Client* client) { if(client) { + if(client->GetPlayer() && client->GetPlayer()->GetCharacterID()) + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, false); if(client->GetPlayer()) { if((client->GetPlayer()->GetActivityStatus() & ACTIVITY_STATUS_LINKDEAD) > 0) { client->GetPlayer()->SetActivityStatus(client->GetPlayer()->GetActivityStatus() - ACTIVITY_STATUS_LINKDEAD); @@ -5421,7 +5452,7 @@ void ZoneServer::KillSpawn(bool spawnListLocked, Spawn* dead, Spawn* killer, boo spellProcess->RemoveSpellFromQueue((Player*)dead, true); if (dead->IsNPC()) - ((NPC*)dead)->Brain()->ClearHate(); + ((NPC*)dead)->Brain()->ClearHate(!spawnListLocked); safe_delete(encounter); @@ -5729,9 +5760,13 @@ void ZoneServer::SendInterruptPacket(Spawn* interrupted, LuaSpell* spell, bool f packet = configReader.getStruct(fizzle ? "WS_SpellFizzle" : "WS_Interrupt", client->GetVersion()); if(packet){ packet->setDataByName("spawn_id", client->GetPlayer()->GetIDWithPlayerSpawn(interrupted)); - packet->setArrayLengthByName("num_targets", spell->targets.size()); - for (int32 i = 0; i < spell->targets.size(); i++) - packet->setArrayDataByName("target_id", client->GetPlayer()->GetIDWithPlayerSpawn(client->GetPlayer()->GetZone()->GetSpawnByID(spell->targets[i])), i); + std::vector targets = spell->GetTargets(); // snapshot under lock + int i = 0; + packet->setArrayLengthByName("num_targets", targets.size()); + for (int32 id : targets) { + packet->setArrayDataByName("target_id", client->GetPlayer()->GetIDWithPlayerSpawn(client->GetPlayer()->GetZone()->GetSpawnByID(id)), i); + i++; + } packet->setDataByName("spell_id", spell->spell->GetSpellID()); outapp = packet->serialize(); client->QueuePacket(outapp); @@ -5772,15 +5807,18 @@ void ZoneServer::SendCastSpellPacket(LuaSpell* spell, Entity* caster, int32 spel } packet->setDataByName("spawn_id", caster_id); - packet->setArrayLengthByName("num_targets", spell->targets.size()); - for (int32 i = 0; i < spell->targets.size(); i++) { - int32 target_id = client->GetPlayer()->GetIDWithPlayerSpawn(spell->caster->GetZone()->GetSpawnByID(spell->targets[i])); + std::vector targets = spell->GetTargets(); // snapshot under lock + int i = 0; + packet->setArrayLengthByName("num_targets", targets.size()); + for (int32 id : targets) { + int32 target_id = client->GetPlayer()->GetIDWithPlayerSpawn(spell->caster->GetZone()->GetSpawnByID(id)); if(target_id) { packet->setArrayDataByName("target", target_id, i); } else { packet->setArrayDataByName("target", 0xFFFFFFFF, i); } + i++; } int32 visual_to_use = spell_visual_override > 0 ? spell_visual_override : spell->spell->GetSpellData()->spell_visual; @@ -8989,25 +9027,27 @@ void ZoneServer::SetSpawnScript(SpawnEntry* entry, Spawn* spawn) const char* script = 0; - for (int x = 0; x < 3; x++) - { - switch (x) + if(spawn && !spawn->GetSpawnScript()) { + for (int x = 0; x < 3; x++) { - case 0: - script = world.GetSpawnEntryScript(entry->spawn_entry_id); - break; - case 1: - script = world.GetSpawnLocationScript(entry->spawn_location_id); - break; - case 2: - script = world.GetSpawnScript(entry->spawn_id); - break; - } + switch (x) + { + case 0: + script = world.GetSpawnEntryScript(entry->spawn_entry_id); + break; + case 1: + script = world.GetSpawnLocationScript(entry->spawn_location_id); + break; + case 2: + script = world.GetSpawnScript(entry->spawn_id); + break; + } - if (script && lua_interface && lua_interface->GetSpawnScript(script) != 0) - { - spawn->SetSpawnScript(string(script)); - break; + if (script && lua_interface && lua_interface->GetSpawnScript(script) != 0) + { + spawn->SetSpawnScript(string(script)); + break; + } } } } @@ -9252,6 +9292,8 @@ void ZoneServer::RemoveClientsFromZone(ZoneServer* zone) { MClientList.readlock(__FUNCTION__, __LINE__); for (itr = clients.begin(); itr != clients.end(); itr++) { Client* client = *itr; + if(client->GetPlayer() && client->GetPlayer()->GetCharacterID()) + broker.AddSeller(client->GetPlayer()->GetCharacterID(), std::string(client->GetPlayer()->GetName()), client->GetPlayer()->GetPlayerInfo()->GetHouseZoneID(), true, false); if(client->GetCurrentZone() == zone) { client->SetCurrentZone(nullptr); } @@ -9265,7 +9307,7 @@ void ZoneServer::RemoveClientsFromZone(ZoneServer* zone) { void ZoneServer::SendSubSpawnUpdates(SUBSPAWN_TYPES subtype) { std::map::iterator subitr; MSpawnList.readlock(__FUNCTION__, __LINE__); - for(subitr = subspawn_list[subtype].begin(); subitr != subspawn_list[subtype].end(); subitr++) { + for(subitr = subspawn_list[subtype].begin(); subitr != subspawn_list[subtype].end(); subitr++) { subitr->second->changed = true; subitr->second->info_changed = true; AddChangedSpawn(subitr->second); diff --git a/source/WorldServer/zoneserver.h b/source/WorldServer/zoneserver.h index b0771d8..bf739d7 100644 --- a/source/WorldServer/zoneserver.h +++ b/source/WorldServer/zoneserver.h @@ -1,6 +1,6 @@ -/* +/* EQ2Emulator: Everquest II Server Emulator - Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net) + Copyright (C) 2005 - 2026 EQ2EMulator Development Team (http://www.eq2emu.com formerly http://www.eq2emulator.net) This file is part of EQ2Emulator. @@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License along with EQ2Emulator. If not, see . */ + #ifndef ZONESERVER_H #define ZONESERVER_H @@ -170,7 +171,7 @@ struct TrackedSpawn { struct HouseItem { int32 spawn_id; int32 item_id; - int32 unique_id; + int64 unique_id; Item* item; }; @@ -701,8 +702,6 @@ public: Spawn* GetSpawnFromUniqueItemID(int32 unique_id); void SendHouseItems(Client* client); - MutexMap house_object_database_lookup; // 1st int32 = model type, 2nd int32 = spawn id - int32 GetWatchdogTime() { return watchdogTimestamp; } void SetWatchdogTime(int32 time) { watchdogTimestamp = time; } void CancelThreads(); diff --git a/source/common/MiscFunctions.cpp b/source/common/MiscFunctions.cpp index 2ff25f1..21ec052 100644 --- a/source/common/MiscFunctions.cpp +++ b/source/common/MiscFunctions.cpp @@ -970,4 +970,4 @@ std::tuple convertTimestampDuration(int64 total_mill // Return the result as a tuple return std::make_tuple(days.count(), hours.count(), minutes.count(), seconds.count()); -} +} \ No newline at end of file diff --git a/source/common/MiscFunctions.h b/source/common/MiscFunctions.h index 6b9ae60..b9f39a6 100644 --- a/source/common/MiscFunctions.h +++ b/source/common/MiscFunctions.h @@ -27,6 +27,9 @@ #include #include #include +#include +#include +#include #ifndef ERRBUF_SIZE #define ERRBUF_SIZE 1024 @@ -122,6 +125,44 @@ static bool IsPrivateAddress(uint32_t ip) return false; } +static std::string FormatCoinReceiveMessage(int64 total_copper, const std::string& reason) { + // breakdown into denominations + int64 platinum = total_copper / 1'000'000; + int64 rem = total_copper % 1'000'000; + int64 gold = rem / 10'000; + rem %= 10'000; + int64 silver = rem / 100; + int64 copper = rem % 100; + + std::ostringstream oss; + oss << "You received "; + + bool first = true; + if (platinum > 0) { + oss << platinum << " platinum"; + first = false; + } + if (gold > 0) { + if (!first) oss << ", "; + oss << gold << " gold"; + first = false; + } + if (silver > 0) { + if (!first) oss << ", "; + oss << silver << " silver"; + first = false; + } + // if nothing else or there's copper, show copper + if (copper > 0 || first) { + if (!first) oss << ", "; + oss << copper << " copper"; + } + + oss << " earned through " << reason; + return oss.str(); +} + + template void AddData(Type input, string* datastring){ if(datastring) datastring->append((char*)&input, sizeof(input));