diff --git a/Source/Framework/Constants/AuctionConst.cs b/Source/Framework/Constants/AuctionConst.cs index d812faad9..c10f1db22 100644 --- a/Source/Framework/Constants/AuctionConst.cs +++ b/Source/Framework/Constants/AuctionConst.cs @@ -13,38 +13,86 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - */ + */ namespace Framework.Constants { - public enum AuctionError + public enum AuctionResult { Ok = 0, Inventory = 1, DatabaseError = 2, - NotEnoughtMoney = 3, + NotEnoughMoney = 3, ItemNotFound = 4, HigherBid = 5, BidIncrement = 7, BidOwn = 10, - RestrictedAccount = 13 + RestrictedAccountTrial = 13, + HasRestriction = 17, + AuctionHouseBusy = 18, + AuctionHouseUnavailable = 19, + CommodityPurchaseFailed = 21, + ItemHasQuote = 23 } - public enum AuctionAction + public enum AuctionCommand { SellItem = 0, Cancel = 1, PlaceBid = 2 } - public enum MailAuctionAnswers + public enum AuctionMailType { - Outbidded = 0, + Outbid = 0, Won = 1, - Successful = 2, + Sold = 2, Expired = 3, - CancelledToBidder = 4, - Canceled = 5, - SalePending = 6 + Removed = 4, + Cancelled = 5, + Invoice = 6 + } + + public enum AuctionHouseResultLimits + { + Browse = 500, + Items = 50 + } + + public enum AuctionHouseFilterMask + { + None = 0x0, + UncollectedOnly = 0x1, + UsableOnly = 0x2, + UpgradesOnly = 0x4, + ExactMatch = 0x8, + PoorQuality = 0x10, + CommonQuality = 0x20, + UncommonQuality = 0x40, + RareQuality = 0x80, + EpicQuality = 0x100, + LegendaryQuality = 0x200, + ArtifactQuality = 0x400, + } + + public enum AuctionHouseSortOrder + { + Price = 0, + Name = 1, + Level = 2, + Bid = 3, + Buyout = 4 + } + + public enum AuctionHouseBrowseMode + { + Search = 0, + SpecificKeys = 1 + } + + public enum AuctionHouseListType + { + Commodities = 1, + Items = 2 } } diff --git a/Source/Framework/Constants/Network/Opcodes.cs b/Source/Framework/Constants/Network/Opcodes.cs index 95042e421..6ad3971e3 100644 --- a/Source/Framework/Constants/Network/Opcodes.cs +++ b/Source/Framework/Constants/Network/Opcodes.cs @@ -47,7 +47,7 @@ namespace Framework.Constants AuctionListBidderItems = 0x34d4, AuctionListItemsByBucketKey = 0x34d1, AuctionListItemsByItemId = 0x34d2, - AuctionListItemsByItemKeys = 0x34d5, + AuctionListBucketsByBucketKeys = 0x34d5, AuctionListOwnerItems = 0x34d3, AuctionPlaceBid = 0x34cf, AuctionRemoveItem = 0x34cd, @@ -813,11 +813,11 @@ namespace Framework.Constants AttackSwingLandedLog = 0x273a, AuctionClosedNotification = 0x2731, AuctionCommandResult = 0x272e, - AuctionCommodityPriceUpdate = 0x28c3, + AuctionCommodityQuote = 0x28c3, + AuctionFavoriteItems = 0x28cc, AuctionHelloResponse = 0x272c, AuctionListBidderItemsResult = 0x28c2, - AuctionListBucketItemsResult = 0x28bf, - AuctionListFavoriteItemsResult = 0x28cc, + AuctionListBucketsResult = 0x28bf, AuctionListItemsResult = 0x28c0, AuctionListOwnerItemsResult = 0x28c1, AuctionOutbidNotification = 0x2730, diff --git a/Source/Framework/Database/Databases/CharacterDatabase.cs b/Source/Framework/Database/Databases/CharacterDatabase.cs index 05f4d68f5..9c1073745 100644 --- a/Source/Framework/Database/Databases/CharacterDatabase.cs +++ b/Source/Framework/Database/Databases/CharacterDatabase.cs @@ -138,6 +138,10 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_CHARACTER_RANDOMBG, "SELECT guid FROM character_battleground_random WHERE guid = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_BANNED, "SELECT guid FROM character_banned WHERE guid = ? AND active = 1"); PrepareStatement(CharStatements.SEL_CHARACTER_QUESTSTATUSREW, "SELECT quest FROM character_queststatus_rewarded WHERE guid = ? AND active = 1"); + PrepareStatement(CharStatements.SEL_CHARACTER_FAVORITE_AUCTIONS, "SELECT `order`, itemId, itemLevel, battlePetSpeciesId, suffixItemNameDescriptionId FROM character_favorite_auctions WHERE guid = ? ORDER BY `order`"); + PrepareStatement(CharStatements.INS_CHARACTER_FAVORITE_AUCTION, "INSERT INTO character_favorite_auctions (guid, `order`, itemId, itemLevel, battlePetSpeciesId, suffixItemNameDescriptionId) VALUE (?, ?, ?, ?, ?, ?)"); + PrepareStatement(CharStatements.DEL_CHARACTER_FAVORITE_AUCTION, "DELETE FROM character_favorite_auctions WHERE guid = ? AND `order` = ?"); + PrepareStatement(CharStatements.DEL_CHARACTER_FAVORITE_AUCTIONS_BY_CHAR, "DELETE FROM character_favorite_auctions WHERE guid = ?"); PrepareStatement(CharStatements.SEL_ACCOUNT_INSTANCELOCKTIMES, "SELECT instanceId, releaseTime FROM account_instance_times WHERE accountId = ?"); PrepareStatement(CharStatements.SEL_CHARACTER_ACTIONS_SPEC, "SELECT button, action, type FROM character_action WHERE guid = ? AND spec = ? ORDER BY button"); @@ -152,11 +156,17 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_MAILITEMS_AZERITE_MILESTONE_POWER, "SELECT iamp.itemGuid, iamp.azeriteItemMilestonePowerId FROM item_instance_azerite_milestone_power iamp INNER JOIN mail_items mi ON iamp.itemGuid = mi.item_guid INNER JOIN mail m ON mi.mail_id = m.id WHERE m.receiver = ?"); PrepareStatement(CharStatements.SEL_MAILITEMS_AZERITE_UNLOCKED_ESSENCE, "SELECT iaue.itemGuid, iaue.azeriteEssenceId, iaue.`rank` FROM item_instance_azerite_unlocked_essence iaue INNER JOIN mail_items mi ON iaue.itemGuid = mi.item_guid INNER JOIN mail m ON mi.mail_id = m.id WHERE m.receiver = ?"); PrepareStatement(CharStatements.SEL_MAILITEMS_AZERITE_EMPOWERED, "SELECT iae.itemGuid, iae.azeritePowerId1, iae.azeritePowerId2, iae.azeritePowerId3, iae.azeritePowerId4, iae.azeritePowerId5 FROM item_instance_azerite_empowered iae INNER JOIN mail_items mi ON iae.itemGuid = mi.item_guid INNER JOIN mail m ON mi.mail_id = m.id WHERE m.receiver = ?"); - PrepareStatement(CharStatements.SEL_AUCTION_ITEMS, "SELECT " + SelectItemInstanceContent + " FROM auctionhouse ah JOIN item_instance ii ON ah.itemguid = ii.guid LEFT JOIN item_instance_gems ig ON ii.guid = ig.itemGuid LEFT JOIN item_instance_transmog iit ON ii.guid = iit.itemGuid LEFT JOIN item_instance_modifiers im ON ii.guid = im.itemGuid"); - PrepareStatement(CharStatements.SEL_AUCTIONS, "SELECT id, auctioneerguid, itemguid, itemEntry, count, itemowner, buyoutprice, time, buyguid, lastbid, startbid, deposit FROM auctionhouse ah INNER JOIN item_instance ii ON ii.guid = ah.itemguid"); - PrepareStatement(CharStatements.INS_AUCTION, "INSERT INTO auctionhouse (id, auctioneerguid, itemguid, itemowner, buyoutprice, time, buyguid, lastbid, startbid, deposit) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - PrepareStatement(CharStatements.DEL_AUCTION, "DELETE FROM auctionhouse WHERE id = ?"); - PrepareStatement(CharStatements.UPD_AUCTION_BID, "UPDATE auctionhouse SET buyguid = ?, lastbid = ? WHERE id = ?"); + PrepareStatement(CharStatements.SEL_AUCTION_ITEMS, "SELECT " + SelectItemInstanceContent + ", ii.owner_guid, ai.auctionId FROM auction_items ai INNER JOIN item_instance ii ON ai.itemGuid = ii.guid LEFT JOIN item_instance_gems ig ON ii.guid = ig.itemGuid LEFT JOIN item_instance_transmog iit ON ii.guid = iit.itemGuid LEFT JOIN item_instance_modifiers im ON ii.guid = im.itemGuid"); + PrepareStatement(CharStatements.SEL_AUCTIONS, "SELECT id, auctionHouseId, owner, bidder, minBid, buyoutOrUnitPrice, deposit, bidAmount, startTime, endTime FROM auctionhouse"); + PrepareStatement(CharStatements.INS_AUCTION_ITEMS, "INSERT INTO auction_items (auctionId, itemGuid) VALUES (?, ?)"); + PrepareStatement(CharStatements.DEL_AUCTION_ITEMS_BY_ITEM, "DELETE FROM auction_items WHERE itemGuid = ?"); + PrepareStatement(CharStatements.SEL_AUCTION_BIDDERS, "SELECT auctionId, playerGuid FROM auction_bidders"); + PrepareStatement(CharStatements.INS_AUCTION_BIDDER, "INSERT INTO auction_bidders (auctionId, playerGuid) VALUES (?, ?)"); + PrepareStatement(CharStatements.DEL_AUCTION_BIDDER_BY_PLAYER, "DELETE FROM auction_bidders WHERE playerGuid = ?"); + PrepareStatement(CharStatements.INS_AUCTION, "INSERT INTO auctionhouse (id, auctionHouseId, owner, bidder, minBid, buyoutOrUnitPrice, deposit, bidAmount, startTime, endTime) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + PrepareStatement(CharStatements.DEL_AUCTION, "DELETE a, ab, ai FROM auctionhouse a LEFT JOIN auction_items ai ON a.id = ai.auctionId LEFT JOIN auction_bidders ab ON a.id = ab.auctionId WHERE a.id = ?"); + PrepareStatement(CharStatements.UPD_AUCTION_BID, "UPDATE auctionhouse SET bidder = ?, bidAmount = ? WHERE id = ?"); + PrepareStatement(CharStatements.UPD_AUCTION_EXPIRATION, "UPDATE auctionhouse SET endTime = ? WHERE id = ?"); PrepareStatement(CharStatements.INS_MAIL, "INSERT INTO mail(id, messageType, stationery, mailTemplateId, sender, receiver, subject, body, has_items, expire_time, deliver_time, money, cod, checked) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); PrepareStatement(CharStatements.DEL_MAIL_BY_ID, "DELETE FROM mail WHERE id = ?"); PrepareStatement(CharStatements.INS_MAIL_ITEM, "INSERT INTO mail_items(mail_id, item_guid, receiver) VALUES (?, ?, ?)"); @@ -535,7 +545,7 @@ namespace Framework.Database PrepareStatement(CharStatements.DEL_CHAR_AURA_FROZEN, "DELETE FROM character_aura WHERE spell = 9454 AND guid = ?"); PrepareStatement(CharStatements.SEL_CHAR_INVENTORY_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM character_inventory ci INNER JOIN item_instance ii ON ii.guid = ci.item WHERE itemEntry = ?"); PrepareStatement(CharStatements.SEL_MAIL_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM mail_items mi INNER JOIN item_instance ii ON ii.guid = mi.item_guid WHERE itemEntry = ?"); - PrepareStatement(CharStatements.SEL_AUCTIONHOUSE_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM auctionhouse ah INNER JOIN item_instance ii ON ii.guid = ah.itemguid WHERE itemEntry = ?"); + PrepareStatement(CharStatements.SEL_AUCTIONHOUSE_COUNT_ITEM, "SELECT COUNT(*) FROM auction_items ai INNER JOIN item_instance ii ON ii.guid = ai.itemGuid WHERE ii.itemEntry = ?"); PrepareStatement(CharStatements.SEL_GUILD_BANK_COUNT_ITEM, "SELECT COUNT(itemEntry) FROM guild_bank_item gbi INNER JOIN item_instance ii ON ii.guid = gbi.item_guid WHERE itemEntry = ?"); PrepareStatement(CharStatements.SEL_CHAR_INVENTORY_ITEM_BY_ENTRY, "SELECT ci.item, cb.slot AS bag, ci.slot, ci.guid, c.account, c.name FROM characters c " + "INNER JOIN character_inventory ci ON ci.guid = c.guid " + @@ -544,7 +554,7 @@ namespace Framework.Database PrepareStatement(CharStatements.SEL_MAIL_ITEMS_BY_ENTRY, "SELECT mi.item_guid, m.sender, m.receiver, cs.account, cs.name, cr.account, cr.name " + "FROM mail m INNER JOIN mail_items mi ON mi.mail_id = m.id INNER JOIN item_instance ii ON ii.guid = mi.item_guid " + "INNER JOIN characters cs ON cs.guid = m.sender INNER JOIN characters cr ON cr.guid = m.receiver WHERE ii.itemEntry = ? LIMIT ?"); - PrepareStatement(CharStatements.SEL_AUCTIONHOUSE_ITEM_BY_ENTRY, "SELECT ah.itemguid, ah.itemowner, c.account, c.name FROM auctionhouse ah INNER JOIN characters c ON c.guid = ah.itemowner INNER JOIN item_instance ii ON ii.guid = ah.itemguid WHERE ii.itemEntry = ? LIMIT ?"); + PrepareStatement(CharStatements.SEL_AUCTIONHOUSE_ITEM_BY_ENTRY, "SELECT ai.itemGuid, c.guid, c.account, c.name FROM auctionhouse ah INNER JOIN auction_items ai ON ah.id = ai.auctionId INNER JOIN characters c ON c.guid = ah.owner INNER JOIN item_instance ii ON ii.guid = ai.itemGuid WHERE ii.itemEntry = ? LIMIT ?"); PrepareStatement(CharStatements.SEL_GUILD_BANK_ITEM_BY_ENTRY, "SELECT gi.item_guid, gi.guildid, g.name FROM guild_bank_item gi INNER JOIN guild g ON g.guildid = gi.guildid INNER JOIN item_instance ii ON ii.guid = gi.item_guid WHERE ii.itemEntry = ? LIMIT ?"); PrepareStatement(CharStatements.DEL_CHAR_ACHIEVEMENT, "DELETE FROM character_achievement WHERE guid = ?"); PrepareStatement(CharStatements.DEL_CHAR_ACHIEVEMENT_PROGRESS, "DELETE FROM character_achievement_progress WHERE guid = ?"); @@ -863,6 +873,10 @@ namespace Framework.Database SEL_CHARACTER_RANDOMBG, SEL_CHARACTER_BANNED, SEL_CHARACTER_QUESTSTATUSREW, + SEL_CHARACTER_FAVORITE_AUCTIONS, + INS_CHARACTER_FAVORITE_AUCTION, + DEL_CHARACTER_FAVORITE_AUCTION, + DEL_CHARACTER_FAVORITE_AUCTIONS_BY_CHAR, SEL_ACCOUNT_INSTANCELOCKTIMES, SEL_MAILITEMS, SEL_MAILITEMS_ARTIFACT, @@ -874,7 +888,13 @@ namespace Framework.Database INS_AUCTION, DEL_AUCTION, UPD_AUCTION_BID, + UPD_AUCTION_EXPIRATION, SEL_AUCTIONS, + INS_AUCTION_ITEMS, + DEL_AUCTION_ITEMS_BY_ITEM, + SEL_AUCTION_BIDDERS, + INS_AUCTION_BIDDER, + DEL_AUCTION_BIDDER_BY_PLAYER, INS_MAIL, DEL_MAIL_BY_ID, INS_MAIL_ITEM, diff --git a/Source/Framework/Database/SQLTransaction.cs b/Source/Framework/Database/SQLTransaction.cs index 003b84a46..6e441ea6f 100644 --- a/Source/Framework/Database/SQLTransaction.cs +++ b/Source/Framework/Database/SQLTransaction.cs @@ -53,7 +53,7 @@ namespace Framework.Database m_trans = trans; } - public bool Execute(MySqlBase mySqlBase) + public virtual bool Execute(MySqlBase mySqlBase) { MySqlErrorCode errorCode = TryExecute(mySqlBase); if (errorCode == MySqlErrorCode.None) @@ -87,7 +87,7 @@ namespace Framework.Database { public TransactionWithResultTask(SQLTransaction trans) : base(trans) { } - public new bool Execute(MySqlBase mySqlBase) + public override bool Execute(MySqlBase mySqlBase) { MySqlErrorCode errorCode = TryExecute(mySqlBase); if (errorCode == MySqlErrorCode.None) diff --git a/Source/Framework/Dynamic/OptionalType.cs b/Source/Framework/Dynamic/OptionalType.cs index cf123ef59..b992d28c9 100644 --- a/Source/Framework/Dynamic/OptionalType.cs +++ b/Source/Framework/Dynamic/OptionalType.cs @@ -44,11 +44,6 @@ namespace Framework.Dynamic Value = default(T); } - public static implicit operator Optional (T value) - { - return value; - } - public static explicit operator T(Optional value) { return (T)value; diff --git a/Source/Game/Achievements/CriteriaHandler.cs b/Source/Game/Achievements/CriteriaHandler.cs index 3264da607..5ed50a6bd 100644 --- a/Source/Game/Achievements/CriteriaHandler.cs +++ b/Source/Game/Achievements/CriteriaHandler.cs @@ -2210,8 +2210,8 @@ namespace Game.Achievements return false; case CriteriaAdditionalCondition.ItemModifiedAppearance: // 200 { - Tuple hasAppearance = referencePlayer.GetSession().GetCollectionMgr().HasItemAppearance(reqValue); - if (!hasAppearance.Item1 || hasAppearance.Item2) + var hasAppearance = referencePlayer.GetSession().GetCollectionMgr().HasItemAppearance(reqValue); + if (!hasAppearance.PermAppearance || hasAppearance.TempAppearance) return false; break; } diff --git a/Source/Game/AuctionHouse/AuctionManager.cs b/Source/Game/AuctionHouse/AuctionManager.cs index 7f5d75c9c..0c7260349 100644 --- a/Source/Game/AuctionHouse/AuctionManager.cs +++ b/Source/Game/AuctionHouse/AuctionManager.cs @@ -25,14 +25,41 @@ using Game.Network.Packets; using System; using System.Collections.Generic; using System.Collections.Concurrent; +using System.Linq; +using Framework.Networking; +using Framework.IO; +using System.Collections; namespace Game { public class AuctionManager : Singleton { const int AH_MINIMUM_DEPOSIT = 100; + const int MIN_AUCTION_TIME = 12 * Time.Hour; - AuctionManager() { } + AuctionHouseObject mHordeAuctions; + AuctionHouseObject mAllianceAuctions; + AuctionHouseObject mNeutralAuctions; + AuctionHouseObject mGoblinAuctions; + + Dictionary _pendingAuctionsByPlayer = new Dictionary(); + + Dictionary _itemsByGuid = new Dictionary(); + + uint _replicateIdGenerator; + + Dictionary _playerThrottleObjects = new Dictionary(); + DateTime _playerThrottleObjectsCleanupTime; + + AuctionManager() + { + mHordeAuctions = new AuctionHouseObject(6); + mAllianceAuctions = new AuctionHouseObject(2); + mNeutralAuctions = new AuctionHouseObject(1); + mGoblinAuctions = new AuctionHouseObject(7); + _replicateIdGenerator = 0; + _playerThrottleObjectsCleanupTime = GameTime.GetGameTimeSteadyPoint() + TimeSpan.FromHours(1); + } public AuctionHouseObject GetAuctionsMap(uint factionTemplateId) { @@ -43,230 +70,104 @@ namespace Game FactionTemplateRecord uEntry = CliDB.FactionTemplateStorage.LookupByKey(factionTemplateId); if (uEntry == null) return mNeutralAuctions; - else if ((uEntry.FactionGroup & (int)FactionMasks.Alliance) != 0) + else if (uEntry.FactionGroup.HasAnyFlag((byte)FactionMasks.Alliance)) return mAllianceAuctions; - else if ((uEntry.FactionGroup & (int)FactionMasks.Horde) != 0) + else if (uEntry.FactionGroup.HasAnyFlag((byte)FactionMasks.Horde)) return mHordeAuctions; else return mNeutralAuctions; } - public ulong GetAuctionDeposit(AuctionHouseRecord entry, uint time, Item pItem, uint count) + public AuctionHouseObject GetAuctionsById(uint auctionHouseId) { - uint MSV = pItem.GetTemplate().GetSellPrice(); - - if (MSV <= 0) - return AH_MINIMUM_DEPOSIT * (uint)WorldConfig.GetFloatValue(WorldCfg.RateAuctionDeposit); - - float multiplier = MathFunctions.CalculatePct((float)entry.DepositRate, 3); - uint timeHr = (((time / 60) / 60) / 12); - ulong deposit = (ulong)(((multiplier * MSV * count / 3) * timeHr * 3) * WorldConfig.GetFloatValue(WorldCfg.RateAuctionDeposit)); - - Log.outDebug(LogFilter.Auctionhouse, $"MSV: {MSV}"); - Log.outDebug(LogFilter.Auctionhouse, $"Items: {count}"); - Log.outDebug(LogFilter.Auctionhouse, $"Multiplier: {multiplier}"); - Log.outDebug(LogFilter.Auctionhouse, $"Deposit: {deposit}"); - - if (deposit < AH_MINIMUM_DEPOSIT * WorldConfig.GetFloatValue(WorldCfg.RateAuctionDeposit)) - return AH_MINIMUM_DEPOSIT * (uint)WorldConfig.GetFloatValue(WorldCfg.RateAuctionDeposit); - else - return deposit; + switch (auctionHouseId) + { + case 1: + return mNeutralAuctions; + case 2: + return mAllianceAuctions; + case 6: + return mHordeAuctions; + case 7: + return mGoblinAuctions; + default: + break; + } + return mNeutralAuctions; } - public void SendAuctionWonMail(AuctionEntry auction, SQLTransaction trans) + public Item GetAItem(ObjectGuid itemGuid) { - Item item = GetAItem(auction.itemGUIDLow); - if (!item) - return; - - uint bidderAccId; - ObjectGuid bidderGuid = ObjectGuid.Create(HighGuid.Player, auction.bidder); - Player bidder = Global.ObjAccessor.FindPlayer(bidderGuid); - // data for gm.log - string bidderName = ""; - bool logGmTrade; - - if (bidder) - { - bidderAccId = bidder.GetSession().GetAccountId(); - bidderName = bidder.GetName(); - logGmTrade = bidder.GetSession().HasPermission(RBACPermissions.LogGmTrade); - } - else - { - bidderAccId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(bidderGuid); - logGmTrade = Global.AccountMgr.HasPermission(bidderAccId, RBACPermissions.LogGmTrade, Global.WorldMgr.GetRealm().Id.Realm); - - if (logGmTrade && !Global.CharacterCacheStorage.GetCharacterNameByGuid(bidderGuid, out bidderName)) - bidderName = Global.ObjectMgr.GetCypherString(CypherStrings.Unknown); - } - - if (logGmTrade) - { - ObjectGuid ownerGuid = ObjectGuid.Create(HighGuid.Player, auction.owner); - string ownerName; - if (!Global.CharacterCacheStorage.GetCharacterNameByGuid(ownerGuid, out ownerName)) - ownerName = Global.ObjectMgr.GetCypherString(CypherStrings.Unknown); - - uint ownerAccId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(ownerGuid); - - Log.outCommand(bidderAccId, $"GM {bidderName} (Account: {bidderAccId}) won item in auction: {item.GetTemplate().GetName()} (Entry: {item.GetEntry()} Count: {item.GetCount()}) and pay money: {auction.bid}. Original owner {ownerName} (Account: {ownerAccId})"); - } - - // receiver exist - if (bidder || bidderAccId != 0) - { - // set owner to bidder (to prevent delete item with sender char deleting) - // owner in `data` will set at mail receive and item extracting - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_ITEM_OWNER); - stmt.AddValue(0, auction.bidder); - stmt.AddValue(1, item.GetGUID().GetCounter()); - trans.Append(stmt); - - if (bidder) - { - bidder.GetSession().SendAuctionWonNotification(auction, item); - // FIXME: for offline player need also - bidder.UpdateCriteria(CriteriaTypes.WonAuctions, 1); - } - - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.Won), AuctionEntry.BuildAuctionMailBody(auction.owner, auction.bid, auction.buyout, 0, 0)) - .AddItem(item) - .SendMailTo(trans, new MailReceiver(bidder, auction.bidder), new MailSender(auction), MailCheckMask.Copied); - } - else - { - // bidder doesn't exist, delete the item - Global.AuctionMgr.RemoveAItem(auction.itemGUIDLow, true); - } + return _itemsByGuid.LookupByKey(itemGuid); } - public void SendAuctionSalePendingMail(AuctionEntry auction, SQLTransaction trans) + public ulong GetCommodityAuctionDeposit(ItemTemplate item, TimeSpan time, uint quantity) { - ObjectGuid owner_guid = ObjectGuid.Create(HighGuid.Player, auction.owner); - Player owner = Global.ObjAccessor.FindPlayer(owner_guid); - uint owner_accId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(owner_guid); - // owner exist (online or offline) - if (owner || owner_accId != 0) - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.SalePending), AuctionEntry.BuildAuctionMailBody(auction.bidder, auction.bid, auction.buyout, auction.deposit, auction.GetAuctionCut())) - .SendMailTo(trans, new MailReceiver(owner, auction.owner), new MailSender(auction), MailCheckMask.Copied); + uint sellPrice = item.GetSellPrice(); + return (ulong)((Math.Ceiling(Math.Floor(Math.Max(0.15 * quantity * sellPrice, 100.0)) / MoneyConstants.Silver) * MoneyConstants.Silver) * (time.Minutes / (MIN_AUCTION_TIME / Time.Minute))); } - //call this method to send mail to auction owner, when auction is successful, it does not clear ram - public void SendAuctionSuccessfulMail(AuctionEntry auction, SQLTransaction trans) + public ulong GetItemAuctionDeposit(Player player, Item item, TimeSpan time) { - ObjectGuid owner_guid = ObjectGuid.Create(HighGuid.Player, auction.owner); - Player owner = Global.ObjAccessor.FindPlayer(owner_guid); - uint owner_accId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(owner_guid); - Item item = GetAItem(auction.itemGUIDLow); - - // owner exist - if (owner || owner_accId != 0) - { - ulong profit = auction.bid + auction.deposit - auction.GetAuctionCut(); - - //FIXME: what do if owner offline - if (owner && item) - { - owner.UpdateCriteria(CriteriaTypes.GoldEarnedByAuctions, profit); - owner.UpdateCriteria(CriteriaTypes.HighestAuctionSold, auction.bid); - // send auction owner notification, bidder must be current! - owner.GetSession().SendAuctionClosedNotification(auction, WorldConfig.GetIntValue(WorldCfg.MailDeliveryDelay), true, item); - } - - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.Successful), AuctionEntry.BuildAuctionMailBody(auction.bidder, auction.bid, auction.buyout, auction.deposit, auction.GetAuctionCut())) - .AddMoney(profit) - .SendMailTo(trans, new MailReceiver(owner, auction.owner), new MailSender(auction), MailCheckMask.Copied, WorldConfig.GetUIntValue(WorldCfg.MailDeliveryDelay)); - } + uint sellPrice = item.GetSellPrice(player); + return (ulong)((Math.Ceiling(Math.Floor(Math.Max(sellPrice * 0.15, 100.0)) / MoneyConstants.Silver) * MoneyConstants.Silver) * (time.Minutes / (MIN_AUCTION_TIME / Time.Minute))); } - //does not clear ram - public void SendAuctionExpiredMail(AuctionEntry auction, SQLTransaction trans) + public string BuildItemAuctionMailSubject(AuctionMailType type, AuctionPosting auction) { - //return an item in auction to its owner by mail - Item item = GetAItem(auction.itemGUIDLow); - if (!item) - return; - - ObjectGuid owner_guid = ObjectGuid.Create(HighGuid.Player, auction.owner); - Player owner = Global.ObjAccessor.FindPlayer(owner_guid); - uint owner_accId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(owner_guid); - // owner exist - if (owner || owner_accId != 0) - { - if (owner) - owner.GetSession().SendAuctionClosedNotification(auction, 0f, false, item); - - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.Expired), AuctionEntry.BuildAuctionMailBody(0, 0, auction.buyout, auction.deposit, 0)) - .AddItem(item) - .SendMailTo(trans, new MailReceiver(owner, auction.owner), new MailSender(auction), MailCheckMask.Copied, 0); - } - else - { - // owner doesn't exist, delete the item - Global.AuctionMgr.RemoveAItem(auction.itemGUIDLow, true); - } + return BuildAuctionMailSubject(auction.Items[0].GetEntry(), type, auction.Id, auction.GetTotalItemCount(), + auction.Items[0].GetModifier(ItemModifier.BattlePetSpeciesId), auction.Items[0].GetContext(), auction.Items[0].m_itemData.BonusListIDs); } - //this function sends mail to old bidder - public void SendAuctionOutbiddedMail(AuctionEntry auction, ulong newPrice, Player newBidder, SQLTransaction trans) + public string BuildCommodityAuctionMailSubject(AuctionMailType type, uint itemId, uint itemCount) { - ObjectGuid oldBidder_guid = ObjectGuid.Create(HighGuid.Player, auction.bidder); - Player oldBidder = Global.ObjAccessor.FindPlayer(oldBidder_guid); - - uint oldBidder_accId = 0; - if (oldBidder == null) - oldBidder_accId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(oldBidder_guid); - - Item item = GetAItem(auction.itemGUIDLow); - - // old bidder exist - if (oldBidder || oldBidder_accId != 0) - { - if (oldBidder && item) - oldBidder.GetSession().SendAuctionOutBidNotification(auction, item); - - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.Outbidded), AuctionEntry.BuildAuctionMailBody(auction.owner, auction.bid, auction.buyout, auction.deposit, auction.GetAuctionCut())) - .AddMoney(auction.bid) - .SendMailTo(trans, new MailReceiver(oldBidder, auction.bidder), new MailSender(auction), MailCheckMask.Copied); - } + return BuildAuctionMailSubject(itemId, type, 0, itemCount, 0, ItemContext.None, null); } - //this function sends mail, when auction is cancelled to old bidder - public void SendAuctionCancelledToBidderMail(AuctionEntry auction, SQLTransaction trans) + public string BuildAuctionMailSubject(uint itemId, AuctionMailType type, uint auctionId, uint itemCount, uint battlePetSpeciesId, ItemContext context, List bonusListIds) { - ObjectGuid bidder_guid = ObjectGuid.Create(HighGuid.Player, auction.bidder); - Player bidder = Global.ObjAccessor.FindPlayer(bidder_guid); + string str = $"{itemId}:0:{(uint)type}:{auctionId}:{itemCount}:{battlePetSpeciesId}:0:0:0:0:{(uint)context}:{bonusListIds.Count}"; - uint bidder_accId = 0; - if (!bidder) - bidder_accId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(bidder_guid); + foreach (var bonusListId in bonusListIds) + str += ':' + bonusListId; - // bidder exist - if (bidder || bidder_accId != 0) - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.CancelledToBidder), AuctionEntry.BuildAuctionMailBody(auction.owner, auction.bid, auction.buyout, auction.deposit, 0)) - .AddMoney(auction.bid) - .SendMailTo(trans, new MailReceiver(bidder, auction.bidder), new MailSender(auction), MailCheckMask.Copied); + return str; } - public void LoadAuctionItems() + public string BuildAuctionWonMailBody(ObjectGuid guid, ulong bid, ulong buyout) + { + return $"{guid.ToString()}:{bid}:{buyout}:0"; + } + + public string BuildAuctionSoldMailBody(ObjectGuid guid, ulong bid, ulong buyout, uint deposit, ulong consignment) + { + return $"{guid.ToString()}:{bid}:{buyout}:{deposit}:{consignment}:0"; + } + + public string BuildAuctionInvoiceMailBody(ObjectGuid guid, ulong bid, ulong buyout, uint deposit, ulong consignment, uint moneyDelay, uint eta) + { + return $"{guid.ToString()}:{bid}:{buyout}:{deposit}:{consignment}:{moneyDelay}:{eta}:0"; + } + + public void LoadAuctions() { uint oldMSTime = Time.GetMSTime(); // need to clear in case we are reloading - mAitems.Clear(); - - // data needs to be at first place for Item.LoadFromDB - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_AUCTION_ITEMS); - SQLResult result = DB.Characters.Query(stmt); + _itemsByGuid.Clear(); + SQLResult result = DB.Characters.Query(DB.Characters.GetPreparedStatement(CharStatements.SEL_AUCTION_ITEMS)); if (result.IsEmpty()) { - Log.outInfo(LogFilter.ServerLoading, "Loaded 0 auction items. DB table `auctionhouse` or `item_instance` is empty!"); + Log.outInfo(LogFilter.ServerLoading, "Loaded 0 auctions. DB table `auctionhouse` is empty."); return; } + // data needs to be at first place for Item.LoadFromDB uint count = 0; + MultiMap itemsByAuction = new MultiMap(); + MultiMap biddersByAuction = new MultiMap(); + do { ulong itemGuid = result.Read(0); @@ -275,66 +176,110 @@ namespace Game ItemTemplate proto = Global.ObjectMgr.GetItemTemplate(itemEntry); if (proto == null) { - Log.outError(LogFilter.Server, "AuctionHouseMgr:LoadAuctionItems: Unknown item (GUID: {0} item entry: {1}) in auction, skipped.", itemGuid, itemEntry); + Log.outError(LogFilter.Misc, $"AuctionHouseMgr.LoadAuctionItems: Unknown item (GUID: {itemGuid} item entry: #{itemEntry}) in auction, skipped."); continue; } - Item item = Bag.NewItemOrBag(proto); - if (!item.LoadFromDB(itemGuid, ObjectGuid.Empty, result.GetFields(), itemEntry)) - continue; - - AddAItem(item); - ++count; - } - while (result.NextRow()); - - Log.outInfo(LogFilter.ServerLoading, "Loaded {0} auction items in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); - } - - public void LoadAuctions() - { - uint oldMSTime = Time.GetMSTime(); - - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_AUCTIONS); - SQLResult result = DB.Characters.Query(stmt); - - if (result.IsEmpty()) - { - Log.outInfo(LogFilter.ServerLoading, "Loaded 0 auctions. DB table `auctionhouse` is empty."); - return; - } - - uint count = 0; - SQLTransaction trans = new SQLTransaction(); - do - { - AuctionEntry aItem = new AuctionEntry(); - if (!aItem.LoadFromDB(result.GetFields())) + Item item = Item.NewItemOrBag(proto); + if (!item.LoadFromDB(itemGuid, ObjectGuid.Create(HighGuid.Player, result.Read(43)), result.GetFields(), itemEntry)) { - aItem.DeleteFromDB(trans); + item.Dispose(); continue; } - GetAuctionsMap(aItem.factionTemplateId).AddAuction(aItem); + uint auctionId = result.Read(44); + itemsByAuction.Add(auctionId, item); + ++count; } while (result.NextRow()); - DB.Characters.CommitTransaction(trans); + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} auction items in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); - Log.outInfo(LogFilter.ServerLoading, "Loaded {0} auctions in {1} ms", count, Time.GetMSTimeDiffToNow(oldMSTime)); + oldMSTime = Time.GetMSTime(); + count = 0; + result = DB.Characters.Query(DB.Characters.GetPreparedStatement(CharStatements.SEL_AUCTION_BIDDERS)); + if (!result.IsEmpty()) + { + do + { + biddersByAuction.Add(result.Read(0), ObjectGuid.Create(HighGuid.Player, result.Read(1))); + + } while (result.NextRow()); + } + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} auction bidders in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); + + oldMSTime = Time.GetMSTime(); + count = 0; + + result = DB.Characters.Query(DB.Characters.GetPreparedStatement(CharStatements.SEL_AUCTIONS)); + if (!result.IsEmpty()) + { + SQLTransaction trans = new SQLTransaction(); + do + { + AuctionPosting auction = new AuctionPosting(); + auction.Id = result.Read(0); + uint auctionHouseId = result.Read(1); + + AuctionHouseObject auctionHouse = GetAuctionsById(auctionHouseId); + if (auctionHouse == null) + { + Log.outError(LogFilter.Misc, $"Auction {auction.Id} has wrong auctionHouseId {auctionHouseId}"); + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_AUCTION); + stmt.AddValue(0, auction.Id); + trans.Append(stmt); + continue; + } + + if (!itemsByAuction.ContainsKey(auction.Id)) + { + Log.outError(LogFilter.Misc, $"Auction {auction.Id} has no items"); + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_AUCTION); + stmt.AddValue(0, auction.Id); + trans.Append(stmt); + continue; + } + + auction.Items = itemsByAuction[auction.Id]; + auction.Owner = ObjectGuid.Create(HighGuid.Player, result.Read(2)); + auction.OwnerAccount = ObjectGuid.Create(HighGuid.WowAccount, Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(auction.Owner)); + ulong bidder = result.Read(3); + if (bidder != 0) + auction.Bidder = ObjectGuid.Create(HighGuid.Player, bidder); + + auction.MinBid = result.Read(4); + auction.BuyoutOrUnitPrice = result.Read(5); + auction.Deposit = result.Read(6); + auction.BidAmount = result.Read(7); + auction.StartTime = Time.UnixTimeToDateTime(result.Read(8)); + auction.EndTime = Time.UnixTimeToDateTime(result.Read(9)); + + if (biddersByAuction.ContainsKey(auction.Id)) + auction.BidderHistory = biddersByAuction[auction.Id]; + + auctionHouse.AddAuction(null, auction); + + ++count; + } while (result.NextRow()); + + DB.Characters.CommitTransaction(trans); + } + + Log.outInfo(LogFilter.ServerLoading, $"Loaded {count} auctions in {Time.GetMSTimeDiffToNow(oldMSTime)} ms"); } - public void AddAItem(Item it) + public void AddAItem(Item item) { - Cypher.Assert(it); - Cypher.Assert(!mAitems.ContainsKey(it.GetGUID().GetCounter())); - mAitems[it.GetGUID().GetCounter()] = it; + Cypher.Assert(item); + Cypher.Assert(!_itemsByGuid.ContainsKey(item.GetGUID())); + _itemsByGuid[item.GetGUID()] = item; } - public bool RemoveAItem(ulong id, bool deleteItem = false) + public bool RemoveAItem(ObjectGuid guid, bool deleteItem = false) { - var item = mAitems.LookupByKey(id); + var item = _itemsByGuid.LookupByKey(guid); if (item == null) return false; @@ -344,15 +289,170 @@ namespace Game item.SaveToDB(null); } - mAitems.Remove(id); + _itemsByGuid.Remove(guid); return true; } + public bool PendingAuctionAdd(Player player, uint auctionHouseId, uint auctionId, ulong deposit) + { + if (!_pendingAuctionsByPlayer.ContainsKey(player.GetGUID())) + _pendingAuctionsByPlayer[player.GetGUID()] = new PlayerPendingAuctions(); + + + var pendingAuction = _pendingAuctionsByPlayer[player.GetGUID()]; + // Get deposit so far + ulong totalDeposit = 0; + foreach (PendingAuctionInfo thisAuction in pendingAuction.Auctions) + totalDeposit += thisAuction.Deposit; + + // Add this deposit + totalDeposit += deposit; + + if (!player.HasEnoughMoney(totalDeposit)) + return false; + + pendingAuction.Auctions.Add(new PendingAuctionInfo(auctionId, auctionHouseId, deposit)); + return true; + } + + public int PendingAuctionCount(Player player) + { + var itr = _pendingAuctionsByPlayer.LookupByKey(player.GetGUID()); + if (itr != null) + return itr.Auctions.Count; + + return 0; + } + + public void PendingAuctionProcess(Player player) + { + var playerPendingAuctions = _pendingAuctionsByPlayer.LookupByKey(player.GetGUID()); + if (playerPendingAuctions == null) + return; + + ulong totaldeposit = 0; + var auctionIndex = 0; + for (; auctionIndex < playerPendingAuctions.Auctions.Count; ++auctionIndex) + { + var pendingAuction = playerPendingAuctions.Auctions[auctionIndex]; + if (!player.HasEnoughMoney(totaldeposit + pendingAuction.Deposit)) + break; + + totaldeposit += pendingAuction.Deposit; + } + + // expire auctions we cannot afford + if (auctionIndex < playerPendingAuctions.Auctions.Count) + { + SQLTransaction trans = new SQLTransaction(); + + do + { + PendingAuctionInfo pendingAuction = playerPendingAuctions.Auctions[auctionIndex]; + AuctionPosting auction = GetAuctionsById(pendingAuction.AuctionHouseId).GetAuction(pendingAuction.AuctionId); + if (auction != null) + auction.EndTime = GameTime.GetGameTimeSystemPoint(); + + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_AUCTION_EXPIRATION); + stmt.AddValue(0, (uint)GameTime.GetGameTime()); + stmt.AddValue(1, pendingAuction.AuctionId); + trans.Append(stmt); + ++auctionIndex; + } while (auctionIndex < playerPendingAuctions.Auctions.Count); + + DB.Characters.CommitTransaction(trans); + } + + _pendingAuctionsByPlayer.Remove(player.GetGUID()); + player.ModifyMoney(-(long)totaldeposit); + } + + void UpdatePendingAuctions() + { + foreach (var pair in _pendingAuctionsByPlayer) + { + ObjectGuid playerGUID = pair.Key; + Player player = Global.ObjAccessor.FindConnectedPlayer(playerGUID); + if (player != null) + { + // Check if there were auctions since last update process if not + if (PendingAuctionCount(player) == pair.Value.LastAuctionsSize) + PendingAuctionProcess(player); + else + _pendingAuctionsByPlayer[playerGUID].LastAuctionsSize = PendingAuctionCount(player); + } + else + { + // Expire any auctions that we couldn't get a deposit for + Log.outWarn(LogFilter.Auctionhouse, $"Player {playerGUID.ToString()} was offline, unable to retrieve deposit!"); + + SQLTransaction trans = new SQLTransaction(); + foreach (PendingAuctionInfo pendingAuction in pair.Value.Auctions) + { + AuctionPosting auction = GetAuctionsById(pendingAuction.AuctionHouseId).GetAuction(pendingAuction.AuctionId); + if (auction != null) + auction.EndTime = GameTime.GetGameTimeSystemPoint(); + + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_AUCTION_EXPIRATION); + stmt.AddValue(0, (uint)GameTime.GetGameTime()); + stmt.AddValue(1, pendingAuction.AuctionId); + trans.Append(stmt); + } + DB.Characters.CommitTransaction(trans); + _pendingAuctionsByPlayer.Remove(playerGUID); + } + } + } + public void Update() { mHordeAuctions.Update(); mAllianceAuctions.Update(); mNeutralAuctions.Update(); + mGoblinAuctions.Update(); + + DateTime now = GameTime.GetGameTimeSteadyPoint(); + if (now >= _playerThrottleObjectsCleanupTime) + { + foreach (var pair in _playerThrottleObjects.ToList()) + { + if (pair.Value.PeriodEnd < now) + _playerThrottleObjects.Remove(pair.Key); + } + + _playerThrottleObjectsCleanupTime = now + TimeSpan.FromHours(1); + } + } + + public uint GenerateReplicationId() + { + return ++_replicateIdGenerator; + } + + public AuctionThrottleResult CheckThrottle(Player player, AuctionCommand command = AuctionCommand.SellItem) + { + DateTime now = GameTime.GetGameTimeSteadyPoint(); + + if (!_playerThrottleObjects.ContainsKey(player.GetGUID())) + _playerThrottleObjects[player.GetGUID()] = new PlayerThrottleObject(); + + var throttleObject = _playerThrottleObjects[player.GetGUID()]; + if (now > throttleObject.PeriodEnd) + { + throttleObject.PeriodEnd = now + TimeSpan.FromMinutes(1); + throttleObject.QueriesRemaining = 100; + } + + if (throttleObject.QueriesRemaining == 0) + { + player.GetSession().SendAuctionCommandResult(0, command, AuctionResult.AuctionHouseBusy, throttleObject.PeriodEnd - now); + return new AuctionThrottleResult(TimeSpan.Zero, true); + } + + if ((--throttleObject.QueriesRemaining) == 0) + return new AuctionThrottleResult(throttleObject.PeriodEnd - now, false); + else + return new AuctionThrottleResult(TimeSpan.FromMilliseconds(WorldConfig.GetIntValue(WorldCfg.AuctionSearchDelay)), false); } public AuctionHouseRecord GetAuctionHouseEntry(uint factionTemplateId) @@ -372,11 +472,11 @@ namespace Game // but no easy way convert creature faction to player race faction for specific city switch (factionTemplateId) { - case 120: - houseid = 7; + case 120: + houseid = 7; break; // booty bay, Blackwater Auction House - case 474: - houseid = 7; + case 474: + houseid = 7; break; // gadgetzan, Blackwater Auction House case 855: houseid = 7; @@ -402,398 +502,1479 @@ namespace Game return CliDB.AuctionHouseStorage.LookupByKey(houseid); } - public Item GetAItem(ulong id) + class PendingAuctionInfo { - return mAitems.LookupByKey(id); + public uint AuctionId; + public uint AuctionHouseId; + public ulong Deposit; + + public PendingAuctionInfo(uint auctionId, uint auctionHouseId, ulong deposit) + { + AuctionId = auctionId; + AuctionHouseId = auctionHouseId; + Deposit = deposit; + } } - AuctionHouseObject mHordeAuctions = new AuctionHouseObject(); - AuctionHouseObject mAllianceAuctions = new AuctionHouseObject(); - AuctionHouseObject mNeutralAuctions = new AuctionHouseObject(); + class PlayerPendingAuctions + { + public List Auctions = new List(); + public int LastAuctionsSize; + } - Dictionary mAitems = new Dictionary(); + class PlayerThrottleObject + { + public DateTime PeriodEnd; + public byte QueriesRemaining = 100; + } } public class AuctionHouseObject { - public void AddAuction(AuctionEntry auction) + public AuctionHouseObject(uint auctionHouseId) { - Cypher.Assert(auction != null); + _auctionHouse = CliDB.AuctionHouseStorage.LookupByKey(auctionHouseId); + } + + public uint GetAuctionHouseId() + { + return _auctionHouse.Id; + } + + public AuctionPosting GetAuction(uint auctionId) + { + return _itemsByAuctionId.LookupByKey(auctionId); + } + + public void AddAuction(SQLTransaction trans, AuctionPosting auction) + { + AuctionsBucketKey key = AuctionsBucketKey.ForItem(auction.Items[0]); + + AuctionsBucketData bucket = _buckets.LookupByKey(key); + if (bucket == null) + { + // we don't have any item for this key yet, create new bucket + bucket = new AuctionsBucketData(); + bucket.Key = key; + + ItemTemplate itemTemplate = auction.Items[0].GetTemplate(); + bucket.ItemClass = (byte)itemTemplate.GetClass(); + bucket.ItemSubClass = (byte)itemTemplate.GetSubClass(); + bucket.InventoryType = (byte)itemTemplate.GetInventoryType(); + bucket.RequiredLevel = (byte)auction.Items[0].GetRequiredLevel(); + for (LocaleConstant locale = LocaleConstant.enUS; locale < LocaleConstant.Total; ++locale) + { + if (locale == LocaleConstant.None) + continue; + + bucket.FullName[(int)locale] = auction.Items[0].GetName(locale); + } + + _buckets.Add(key, bucket); + } + + // update cache fields + ulong priceToDisplay = auction.BuyoutOrUnitPrice != 0 ? auction.BuyoutOrUnitPrice : auction.BidAmount; + if (bucket.MinPrice == 0 || priceToDisplay < bucket.MinPrice) + bucket.MinPrice = priceToDisplay; + + var itemModifiedAppearance = auction.Items[0].GetItemModifiedAppearance(); + if (itemModifiedAppearance != null) + { + int index = 0; + for (var i = 0; i < bucket.ItemModifiedAppearanceId.Length; ++i) + { + if (bucket.ItemModifiedAppearanceId[i].Id == itemModifiedAppearance.Id) + { + index = i; + break; + } + } + + bucket.ItemModifiedAppearanceId[index] = (itemModifiedAppearance.Id, bucket.ItemModifiedAppearanceId[index].Item2 + 1); + } + + uint quality; + + if (auction.Items[0].GetModifier(ItemModifier.BattlePetSpeciesId) == 0) + quality = (byte)auction.Items[0].GetQuality(); + else + { + quality = (auction.Items[0].GetModifier(ItemModifier.BattlePetBreedData) >> 24) & 0xFF; + foreach (Item item in auction.Items) + { + byte battlePetLevel = (byte)item.GetModifier(ItemModifier.BattlePetLevel); + if (bucket.MinBattlePetLevel == 0) + bucket.MinBattlePetLevel = battlePetLevel; + else if (bucket.MinBattlePetLevel > battlePetLevel) + bucket.MinBattlePetLevel = battlePetLevel; + + bucket.MaxBattlePetLevel = Math.Max(bucket.MaxBattlePetLevel, battlePetLevel); + } + } + + bucket.QualityMask |= (AuctionHouseFilterMask)(1 << ((int)quality + 4)); + ++bucket.QualityCounts[quality]; + + if (trans != null) + { + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_AUCTION); + stmt.AddValue(0, auction.Id); + stmt.AddValue(1, _auctionHouse.Id); + stmt.AddValue(2, auction.Owner.GetCounter()); + stmt.AddValue(3, ObjectGuid.Empty.GetCounter()); + stmt.AddValue(4, auction.MinBid); + stmt.AddValue(5, auction.BuyoutOrUnitPrice); + stmt.AddValue(6, auction.Deposit); + stmt.AddValue(7, auction.BidAmount); + stmt.AddValue(8, (uint)Time.DateTimeToUnixTime(auction.StartTime)); + stmt.AddValue(9, (uint)Time.DateTimeToUnixTime(auction.EndTime)); + trans.Append(stmt); + + foreach (Item item in auction.Items) + { + stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_AUCTION_ITEMS); + stmt.AddValue(0, auction.Id); + stmt.AddValue(1, item.GetGUID().GetCounter()); + trans.Append(stmt); + } + } + + foreach (Item item in auction.Items) + Global.AuctionHouseMgr.AddAItem(item); + + auction.Bucket = bucket; + _playerOwnedAuctions.Add(auction.Owner, auction.Id); + foreach (ObjectGuid bidder in auction.BidderHistory) + _playerBidderAuctions.Add(bidder, auction.Id); + + _itemsByAuctionId[auction.Id] = auction; + + AuctionPosting.Sorter insertSorter = new AuctionPosting.Sorter(LocaleConstant.enUS, new AuctionSortDef[] { new AuctionSortDef(AuctionHouseSortOrder.Price, false) }, 1); + var auctionIndex = bucket.Auctions.BinarySearch(auction, insertSorter); + if (auctionIndex < 0) + auctionIndex = ~auctionIndex; + bucket.Auctions.Insert(auctionIndex, auction); - AuctionsMap[auction.Id] = auction; Global.ScriptMgr.OnAuctionAdd(this, auction); } - public bool RemoveAuction(AuctionEntry auction) + public void RemoveAuction(SQLTransaction trans, AuctionPosting auction, AuctionPosting auctionPosting = null) { + AuctionsBucketData bucket = auction.Bucket; + + bucket.Auctions.RemoveAll(auct => auct.Id == auction.Id); + if (!bucket.Auctions.Empty()) + { + // update cache fields + ulong priceToDisplay = auction.BuyoutOrUnitPrice != 0 ? auction.BuyoutOrUnitPrice : auction.BidAmount; + if (bucket.MinPrice == priceToDisplay) + { + bucket.MinPrice = ulong.MaxValue; + foreach (AuctionPosting remainingAuction in bucket.Auctions) + bucket.MinPrice = Math.Min(bucket.MinPrice, remainingAuction.BuyoutOrUnitPrice != 0 ? remainingAuction.BuyoutOrUnitPrice : remainingAuction.BidAmount); + } + + var itemModifiedAppearance = auction.Items[0].GetItemModifiedAppearance(); + if (itemModifiedAppearance != null) + { + int index = -1; + for (var i = 0; i < bucket.ItemModifiedAppearanceId.Length; ++i) + { + if (bucket.ItemModifiedAppearanceId[i].Item1 == itemModifiedAppearance.Id) + { + index = i; + break; + } + } + + if (index != -1) + if (--bucket.ItemModifiedAppearanceId[index].Count == 0) + bucket.ItemModifiedAppearanceId[index].Id = 0; + } + + uint quality; + + if (auction.Items[0].GetModifier(ItemModifier.BattlePetSpeciesId) == 0) + { + quality = (uint)auction.Items[0].GetQuality(); + } + else + { + quality = (auction.Items[0].GetModifier(ItemModifier.BattlePetBreedData) >> 24) & 0xFF; + bucket.MinBattlePetLevel = 0; + bucket.MaxBattlePetLevel = 0; + foreach (AuctionPosting remainingAuction in bucket.Auctions) + { + foreach (Item item in remainingAuction.Items) + { + byte battlePetLevel = (byte)item.GetModifier(ItemModifier.BattlePetLevel); + if (bucket.MinBattlePetLevel == 0) + bucket.MinBattlePetLevel = battlePetLevel; + else if (bucket.MinBattlePetLevel > battlePetLevel) + bucket.MinBattlePetLevel = battlePetLevel; + + bucket.MaxBattlePetLevel = Math.Max(bucket.MaxBattlePetLevel, battlePetLevel); + } + } + } + + if (--bucket.QualityCounts[quality] == 0) + bucket.QualityMask &= (AuctionHouseFilterMask)(~(1 << ((int)quality + 4))); + } + else + _buckets.Remove(bucket.Key); + + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_AUCTION); + stmt.AddValue(0, auction.Id); + trans.Append(stmt); + + foreach (Item item in auction.Items) + Global.AuctionHouseMgr.RemoveAItem(item.GetGUID()); + Global.ScriptMgr.OnAuctionRemove(this, auction); - return AuctionsMap.TryRemove(auction.Id, out _); + + _playerOwnedAuctions.Remove(auction.Owner, auction.Id); + foreach (ObjectGuid bidder in auction.BidderHistory) + _playerBidderAuctions.Remove(bidder, auction.Id); + + _itemsByAuctionId.Remove(auction.Id); } public void Update() { - long curTime = GameTime.GetGameTime(); - // Handle expired auctions + DateTime curTime = GameTime.GetGameTimeSystemPoint(); + DateTime curTimeSteady = GameTime.GetGameTimeSteadyPoint(); + ///- Handle expired auctions - // If storage is empty, no need to update. next == NULL in this case. - if (AuctionsMap.Empty()) + // Clear expired throttled players + foreach (var key in _replicateThrottleMap.Keys.ToList()) + if (_replicateThrottleMap[key].NextAllowedReplication <= curTimeSteady) + _replicateThrottleMap.Remove(key); + + foreach (var key in _commodityQuotes.Keys.ToList()) + if (_commodityQuotes[key].ValidTo < curTimeSteady) + _commodityQuotes.Remove(key); + + if (_itemsByAuctionId.Empty()) return; SQLTransaction trans = new SQLTransaction(); - foreach (var auction in AuctionsMap.Values) + foreach (var auction in _itemsByAuctionId.Values) { - // filter auctions expired on next update - if (auction.expire_time > curTime + 60) + ///- filter auctions expired on next update + if (auction.EndTime > curTime.AddMinutes(1)) continue; - // Either cancel the auction if there was no bidder - if (auction.bidder == 0 && auction.bid == 0) + ///- Either cancel the auction if there was no bidder + if (auction.Bidder.IsEmpty()) { - Global.AuctionMgr.SendAuctionExpiredMail(auction, trans); + SendAuctionExpired(auction, trans); Global.ScriptMgr.OnAuctionExpire(this, auction); } - // Or perform the transaction + ///- Or perform the transaction else { //we should send an "item sold" message if the seller is online //we send the item to the winner //we send the money to the seller - Global.AuctionMgr.SendAuctionSuccessfulMail(auction, trans); - Global.AuctionMgr.SendAuctionWonMail(auction, trans); + SendAuctionWon(auction, null, trans); + SendAuctionSold(auction, null, trans); Global.ScriptMgr.OnAuctionSuccessful(this, auction); } - // In any case clear the auction - auction.DeleteFromDB(trans); - - Global.AuctionMgr.RemoveAItem(auction.itemGUIDLow); - RemoveAuction(auction); + ///- In any case clear the auction + RemoveAuction(trans, auction); } // Run DB changes DB.Characters.CommitTransaction(trans); } - public void BuildListBidderItems(AuctionListBidderItemsResult packet, Player player) + public void BuildListBuckets(AuctionListBucketsResult listBucketsResult, Player player, string name, byte minLevel, byte maxLevel, AuctionHouseFilterMask filters, Optional classFilters, + byte[] knownPetBits, int knownPetBitsCount, byte maxKnownPetLevel, uint offset, AuctionSortDef[] sorts, int sortCount) { - foreach (var Aentry in AuctionsMap.Values) + List knownAppearanceIds = new List(); + BitArray knownPetSpecies = new BitArray(knownPetBits); + // prepare uncollected filter for more efficient searches + if (filters.HasFlag(AuctionHouseFilterMask.UncollectedOnly)) { - if (Aentry != null && Aentry.bidder == player.GetGUID().GetCounter()) - Aentry.BuildAuctionInfo(packet.Items, false); + knownAppearanceIds = player.GetSession().GetCollectionMgr().GetAppearanceIds(); + //todo fix me + //if (knownPetSpecies.size() < CliDB.BattlePetSpeciesStorage.GetNumRows()) + //knownPetSpecies.resize(CliDB.BattlePetSpeciesStorage.GetNumRows()); } - } - public void BuildListOwnerItems(AuctionListOwnerItemsResult packet, Player player) - { - foreach (var Aentry in AuctionsMap.Values) + var sorter = new AuctionsBucketData.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + var builder = new AuctionsResultBuilder(offset, sorter, AuctionHouseResultLimits.Browse); + + foreach (var bucket in _buckets) { - if (Aentry != null && Aentry.owner == player.GetGUID().GetCounter()) - Aentry.BuildAuctionInfo(packet.Items, false); - } - } + AuctionsBucketData bucketData = bucket.Value; + if (!name.IsEmpty()) + { + if (filters.HasFlag(AuctionHouseFilterMask.ExactMatch)) + { + if (bucketData.FullName[(int)player.GetSession().GetSessionDbcLocale()] != name) + continue; + } + else + { + if (!bucketData.FullName[(int)player.GetSession().GetSessionDbcLocale()].Contains(name)) + continue; + } + } - public void BuildListAuctionItems(AuctionListItemsResult packet, Player player, string searchedname, uint listfrom, byte levelmin, byte levelmax, AuctionHouseFilterMask filters, Optional classFilters) - { - long curTime = GameTime.GetGameTime(); - - foreach (var Aentry in AuctionsMap.Values) - { - // Skip expired auctions - if (Aentry.expire_time < curTime) + if (minLevel != 0 && bucketData.RequiredLevel < minLevel) continue; - Item item = Global.AuctionMgr.GetAItem(Aentry.itemGUIDLow); - if (!item) + if (maxLevel != 0 && bucketData.RequiredLevel > maxLevel) + continue; + + if (!filters.HasFlag(bucketData.QualityMask)) continue; - ItemTemplate proto = item.GetTemplate(); if (classFilters.HasValue) { // if we dont want any class filters, Optional is not initialized // if we dont want this class included, SubclassMask is set to FILTER_SKIP_CLASS // if we want this class and did not specify and subclasses, its set to FILTER_SKIP_SUBCLASS // otherwise full restrictions apply - if (classFilters.Value.Classes[(int)proto.GetClass()].SubclassMask == AuctionSearchClassFilters.FilterType.SkipClass) + if (classFilters.Value.Classes[bucketData.ItemClass].SubclassMask == AuctionSearchClassFilters.FilterType.SkipClass) continue; - if (classFilters.Value.Classes[(int)proto.GetClass()].SubclassMask != AuctionSearchClassFilters.FilterType.SkipSubclass) + if (classFilters.Value.Classes[bucketData.ItemClass].SubclassMask != AuctionSearchClassFilters.FilterType.SkipSubclass) { - if (!Convert.ToBoolean((int)classFilters.Value.Classes[(int)proto.GetClass()].SubclassMask & (1u << (int)proto.GetSubClass()))) + if (!classFilters.Value.Classes[bucketData.ItemClass].SubclassMask.HasAnyFlag((AuctionSearchClassFilters.FilterType)(1 << bucketData.ItemSubClass))) continue; - if (!Convert.ToBoolean(classFilters.Value.Classes[(int)proto.GetClass()].InvTypes[(int)proto.GetSubClass()] & (1u << (int)proto.GetInventoryType()))) + if (!classFilters.Value.Classes[bucketData.ItemClass].InvTypes[bucketData.ItemSubClass].HasAnyFlag(1u << bucketData.InventoryType)) continue; } } - if (!filters.HasFlag((AuctionHouseFilterMask)(1 << ((int)proto.GetQuality() + 4)))) - continue; - - if (levelmin != 0 && (item.GetRequiredLevel() < levelmin || (levelmax != 0 && item.GetRequiredLevel() > levelmax))) - continue; - - if (filters.HasFlag(AuctionHouseFilterMask.UsableOnly) && player.CanUseItem(item) != InventoryResult.Ok) - continue; - - // Allow search by suffix (ie: of the Monkey) or partial name (ie: Monkey) - // No need to do any of this if no search term was entered - if (!string.IsNullOrEmpty(searchedname)) + if (filters.HasFlag(AuctionHouseFilterMask.UncollectedOnly)) { - string name = proto.GetName(player.GetSession().GetSessionDbcLocale()); - if (string.IsNullOrEmpty(name)) + // appearances - by ItemAppearanceId, not ItemModifiedAppearanceId + if (bucketData.InventoryType != (byte)InventoryType.NonEquip && bucketData.InventoryType != (byte)InventoryType.Bag) + { + bool hasAll = true; + foreach (var bucketAppearance in bucketData.ItemModifiedAppearanceId) + { + var itemModifiedAppearance = CliDB.ItemModifiedAppearanceStorage.LookupByKey(bucketAppearance.Item1); + if (itemModifiedAppearance != null) + { + if (!knownAppearanceIds.Contains(itemModifiedAppearance.ItemAppearanceID)) + { + hasAll = false; + break; + } + } + } + + if (hasAll) + continue; + } + // caged pets + else if (bucket.Key.BattlePetSpeciesId != 0) + { + if (knownPetSpecies.Get(bucket.Key.BattlePetSpeciesId)) + continue; + } + // toys + else if (Global.DB2Mgr.IsToyItem(bucket.Key.ItemId)) + { + if (player.GetSession().GetCollectionMgr().HasToy(bucket.Key.ItemId)) + continue; + } + // mounts + // recipes + // pet items + else if (bucketData.ItemClass == (int)ItemClass.Consumable || bucketData.ItemClass == (int)ItemClass.Recipe || bucketData.ItemClass == (int)ItemClass.Miscellaneous) + { + ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(bucket.Key.ItemId); + if (itemTemplate.Effects.Count >= 2 && (itemTemplate.Effects[0].SpellID == 483 || itemTemplate.Effects[0].SpellID == 55884)) + { + if (player.HasSpell((uint)itemTemplate.Effects[1].SpellID)) + continue; + + var battlePetSpecies = Global.SpellMgr.GetBattlePetSpecies((uint)itemTemplate.Effects[1].SpellID); + if (battlePetSpecies != null) + if (knownPetSpecies.Get((int)battlePetSpecies.Id)) + continue; + } + } + } + + if (filters.HasFlag(AuctionHouseFilterMask.UsableOnly)) + { + if (bucketData.RequiredLevel != 0 && player.GetLevel() < bucketData.RequiredLevel) + continue; + + if (player.CanUseItem(Global.ObjectMgr.GetItemTemplate(bucket.Key.ItemId), true) != InventoryResult.Ok) + continue; + + // cannot learn caged pets whose level exceeds highest level of currently owned pet + if (bucketData.MinBattlePetLevel != 0 && bucketData.MinBattlePetLevel > maxKnownPetLevel) continue; } - // Add the item if no search term or if entered search term was found - if (packet.Items.Count < 50 && packet.TotalCount >= listfrom) - Aentry.BuildAuctionInfo(packet.Items, true); + // TODO: this one needs to access loot history to know highest item level for every inventory type + //if (filters.HasFlag(AuctionHouseFilterMask.UpgradesOnly)) + //{ + //} - ++packet.TotalCount; + builder.AddItem(bucketData); + } + + foreach (AuctionsBucketData resultBucket in builder.GetResultRange()) + { + BucketInfo bucketInfo = new BucketInfo(); + resultBucket.BuildBucketInfo(bucketInfo, player); + listBucketsResult.Buckets.Add(bucketInfo); + } + + listBucketsResult.HasMoreResults = builder.HasMoreResults(); + } + + public void BuildListBuckets(AuctionListBucketsResult listBucketsResult, Player player, AuctionBucketKey[] keys, int keysCount, AuctionSortDef[] sorts, int sortCount) + { + List buckets = new List(); + for (int i = 0; i < keysCount; ++i) + { + var bucketData = _buckets.LookupByKey(new AuctionsBucketKey(keys[i])); + if (bucketData != null) + buckets.Add(bucketData); + } + + AuctionsBucketData.Sorter sorter = new AuctionsBucketData.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + buckets.Sort(sorter); + + foreach (AuctionsBucketData resultBucket in buckets) + { + BucketInfo bucketInfo = new BucketInfo(); + resultBucket.BuildBucketInfo(bucketInfo, player); + listBucketsResult.Buckets.Add(bucketInfo); + } + + listBucketsResult.HasMoreResults = false; + } + + public void BuildListBidderItems(AuctionListBidderItemsResult listBidderItemsResult, Player player, uint offset, AuctionSortDef[] sorts, int sortCount) + { + // always full list + List auctions = new List(); + foreach (var auctionId in _playerBidderAuctions.LookupByKey(player.GetGUID())) + { + AuctionPosting auction = _itemsByAuctionId.LookupByKey(auctionId); + if (auction != null) + auctions.Add(auction); + } + + AuctionPosting.Sorter sorter = new AuctionPosting.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + auctions.Sort(sorter); + + foreach (var resultAuction in auctions) + { + AuctionItem auctionItem = new AuctionItem(); + resultAuction.BuildAuctionItem(auctionItem, true, true, true, false); + listBidderItemsResult.Items.Add(auctionItem); + } + + listBidderItemsResult.HasMoreResults = false; + } + + public void BuildListAuctionItems(AuctionListItemsResult listItemsResult, Player player, AuctionsBucketKey bucketKey, uint offset, AuctionSortDef[] sorts, int sortCount) + { + listItemsResult.TotalCount = 0; + AuctionsBucketData bucket = _buckets.LookupByKey(bucketKey); + if (bucket != null) + { + var sorter = new AuctionPosting.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + var builder = new AuctionsResultBuilder(offset, sorter, AuctionHouseResultLimits.Items); + + foreach (var auction in bucket.Auctions) + { + builder.AddItem(auction); + foreach (Item item in auction.Items) + listItemsResult.TotalCount += item.GetCount(); + } + + foreach (AuctionPosting resultAuction in builder.GetResultRange()) + { + AuctionItem auctionItem = new AuctionItem(); + resultAuction.BuildAuctionItem(auctionItem, false, false, resultAuction.OwnerAccount != player.GetSession().GetAccountGUID(), resultAuction.Bidder.IsEmpty()); + listItemsResult.Items.Add(auctionItem); + } + + listItemsResult.HasMoreResults = builder.HasMoreResults(); } } - public AuctionEntry GetAuction(uint id) + public void BuildListAuctionItems(AuctionListItemsResult listItemsResult, Player player, uint itemId, uint offset, AuctionSortDef[] sorts, int sortCount) { - return AuctionsMap.LookupByKey(id); + var sorter = new AuctionPosting.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + var builder = new AuctionsResultBuilder(offset, sorter, AuctionHouseResultLimits.Items); + + listItemsResult.TotalCount = 0; + var bucketData = _buckets.LookupByKey(new AuctionsBucketKey(itemId, 0, 0, 0)); + if (bucketData != null) + { + foreach (AuctionPosting auction in bucketData.Auctions) + { + builder.AddItem(auction); + foreach (Item item in auction.Items) + listItemsResult.TotalCount += item.GetCount(); + } + } + + foreach (AuctionPosting resultAuction in builder.GetResultRange()) + { + AuctionItem auctionItem = new AuctionItem(); + resultAuction.BuildAuctionItem(auctionItem, false, true, resultAuction.OwnerAccount != player.GetSession().GetAccountGUID(), + resultAuction.Bidder.IsEmpty()); + + listItemsResult.Items.Add(auctionItem); + } + + listItemsResult.HasMoreResults = builder.HasMoreResults(); } - ConcurrentDictionary AuctionsMap = new ConcurrentDictionary(); + public void BuildListOwnerItems(AuctionListOwnerItemsResult listOwnerItemsResult, Player player, uint offset, AuctionSortDef[] sorts, int sortCount) + { + // always full list + List auctions = new List(); + foreach (var auctionId in _playerOwnedAuctions.LookupByKey(player.GetGUID())) + { + AuctionPosting auction = _itemsByAuctionId.LookupByKey(auctionId); + if (auction != null) + auctions.Add(auction); + } + + AuctionPosting.Sorter sorter = new AuctionPosting.Sorter(player.GetSession().GetSessionDbcLocale(), sorts, sortCount); + auctions.Sort(sorter); + + foreach (var resultAuction in auctions) + { + AuctionItem auctionItem = new AuctionItem(); + resultAuction.BuildAuctionItem(auctionItem, true, true, false, false); + listOwnerItemsResult.Items.Add(auctionItem); + } + + listOwnerItemsResult.HasMoreResults = false; + } + + public void BuildReplicate(AuctionReplicateResponse replicateResponse, Player player, uint global, uint cursor, uint tombstone, uint count) + { + DateTime curTime = GameTime.GetGameTimeSteadyPoint(); + + var throttleData = _replicateThrottleMap.LookupByKey(player.GetGUID()); + if (throttleData == null) + { + throttleData = new PlayerReplicateThrottleData(); + throttleData.NextAllowedReplication = curTime + TimeSpan.FromSeconds(WorldConfig.GetIntValue(WorldCfg.AuctionGetallDelay)); + throttleData.Global = Global.AuctionHouseMgr.GenerateReplicationId(); + } + else + { + if (throttleData.Global != global || throttleData.Cursor != cursor || throttleData.Tombstone != tombstone) + return; + + if (!throttleData.IsReplicationInProgress() && throttleData.NextAllowedReplication > curTime) + return; + } + + if (_itemsByAuctionId.Empty() || count == 0) + return; + + var keyIndex = _itemsByAuctionId.IndexOfKey(cursor); + foreach (var pair in _itemsByAuctionId.Skip(keyIndex)) + { + AuctionItem auctionItem = new AuctionItem(); + pair.Value.BuildAuctionItem(auctionItem, false, true, true, pair.Value.Bidder.IsEmpty()); + replicateResponse.Items.Add(auctionItem); + if (--count == 0) + break; + } + + replicateResponse.ChangeNumberGlobal = throttleData.Global; + replicateResponse.ChangeNumberCursor = throttleData.Cursor = !replicateResponse.Items.Empty() ? replicateResponse.Items.Last().AuctionID : 0; + replicateResponse.ChangeNumberTombstone = throttleData.Tombstone = count == 0 ? _itemsByAuctionId.First().Key : 0; + _replicateThrottleMap[player.GetGUID()] = throttleData; + } + + public ulong CalcualteAuctionHouseCut(ulong bidAmount) + { + return (ulong)Math.Max((long)(MathFunctions.CalculatePct(bidAmount, _auctionHouse.ConsignmentRate) * WorldConfig.GetFloatValue(WorldCfg.RateAuctionCut)), 0); + } + + public CommodityQuote CreateCommodityQuote(Player player, uint itemId, uint quantity) + { + var bucketData = _buckets.LookupByKey(AuctionsBucketKey.ForCommodity(itemId)); + if (bucketData == null) + return null; + + ulong totalPrice = 0; + uint remainingQuantity = quantity; + foreach (AuctionPosting auction in bucketData.Auctions) + { + foreach (Item auctionItem in auction.Items) + { + if (auctionItem.GetCount() >= remainingQuantity) + { + totalPrice += auction.BuyoutOrUnitPrice * remainingQuantity; + remainingQuantity = 0; + break; + } + + totalPrice += auction.BuyoutOrUnitPrice * auctionItem.GetCount(); + remainingQuantity -= auctionItem.GetCount(); + } + } + + // not enough items on auction house + if (remainingQuantity != 0) + return null; + + if (!player.HasEnoughMoney(totalPrice)) + return null; + + CommodityQuote quote = _commodityQuotes[player.GetGUID()]; + quote.TotalPrice = totalPrice; + quote.Quantity = quantity; + quote.ValidTo = GameTime.GetGameTimeSteadyPoint() + TimeSpan.FromSeconds(30); + return quote; + } + + public void CancelCommodityQuote(ObjectGuid guid) + { + _commodityQuotes.Remove(guid); + } + + public bool BuyCommodity(SQLTransaction trans, Player player, uint itemId, uint quantity, TimeSpan delayForNextAction) + { + var bucketItr = _buckets.LookupByKey(AuctionsBucketKey.ForCommodity(itemId)); + if (bucketItr == null) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + var quote = _commodityQuotes.LookupByKey(player.GetGUID()); + if (quote == null) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + ulong totalPrice = 0; + uint remainingQuantity = quantity; + List auctions = new List(); + for (var i = 0; i < bucketItr.Auctions.Count;) + { + AuctionPosting auction = bucketItr.Auctions[i++]; + auctions.Add(auction); + foreach (Item auctionItem in auction.Items) + { + if (auctionItem.GetCount() >= remainingQuantity) + { + totalPrice += auction.BuyoutOrUnitPrice * remainingQuantity; + remainingQuantity = 0; + i = bucketItr.Auctions.Count; + break; + } + + totalPrice += auction.BuyoutOrUnitPrice * auctionItem.GetCount(); + remainingQuantity -= auctionItem.GetCount(); + } + } + + // not enough items on auction house + if (remainingQuantity != 0) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + // something was bought between creating quote and finalizing transaction + // but we allow lower price if new items were posted at lower price + if (totalPrice > quote.TotalPrice) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + if (!player.HasEnoughMoney(totalPrice)) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + Optional uniqueSeller = new Optional(); + + // prepare items + List items = new List(); + remainingQuantity = quantity; + List removedItemsFromAuction = new List(); + + for (var i = 0; i < bucketItr.Auctions.Count;) + { + AuctionPosting auction = bucketItr.Auctions[i++]; + if (!uniqueSeller.HasValue) + uniqueSeller.Set(auction.Owner); + else if (uniqueSeller.Value != auction.Owner) + uniqueSeller.Set(ObjectGuid.Empty); + + uint boughtFromAuction = 0; + int removedItems = 0; + foreach (Item auctionItem in auction.Items) + { + if (auctionItem.GetCount() >= remainingQuantity) + { + Item clonedItem = auctionItem.CloneItem(remainingQuantity, player); + if (!clonedItem) + { + player.GetSession().SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, delayForNextAction); + return false; + } + + auctionItem.SetCount(auctionItem.GetCount() - remainingQuantity); + auctionItem.FSetState(ItemUpdateState.Changed); + auctionItem.SaveToDB(trans); + items.Add(clonedItem); + boughtFromAuction += remainingQuantity; + remainingQuantity = 0; + i = bucketItr.Auctions.Count; + break; + } + + items.Add(auctionItem); + boughtFromAuction += auctionItem.GetCount(); + remainingQuantity -= auctionItem.GetCount(); + ++removedItems; + } + + removedItemsFromAuction.Add(removedItems); + + if (player.GetSession().HasPermission(RBACPermissions.LogGmTrade)) + { + uint bidderAccId = player.GetSession().GetAccountId(); + if (!Global.CharacterCacheStorage.GetCharacterNameByGuid(auction.Owner, out string ownerName)) + ownerName = Global.ObjectMgr.GetCypherString(CypherStrings.Unknown); + + Log.outCommand(bidderAccId, $"GM {player.GetName()} (Account: {bidderAccId}) bought commodity in auction: {items[0].GetName(Global.WorldMgr.GetDefaultDbcLocale())} (Entry: {items[0].GetEntry()} " + + $"Count: {boughtFromAuction}) and pay money: { auction.BuyoutOrUnitPrice * boughtFromAuction}. Original owner {ownerName} (Account: {Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(auction.Owner)})"); + } + + ulong auctionHouseCut = CalcualteAuctionHouseCut(auction.BuyoutOrUnitPrice * boughtFromAuction); + ulong depositPart = Global.AuctionHouseMgr.GetCommodityAuctionDeposit(items[0].GetTemplate(), (auction.EndTime - auction.StartTime), boughtFromAuction); + ulong profit = auction.BuyoutOrUnitPrice * boughtFromAuction + depositPart - auctionHouseCut; + + Player owner = Global.ObjAccessor.FindConnectedPlayer(auction.Owner); + if (owner != null) + { + owner.UpdateCriteria(CriteriaTypes.GoldEarnedByAuctions, profit); + owner.UpdateCriteria(CriteriaTypes.HighestAuctionSold, profit); + owner.GetSession().SendAuctionClosedNotification(auction, (float)WorldConfig.GetIntValue(WorldCfg.MailDeliveryDelay), true); + } + + new MailDraft(Global.AuctionHouseMgr.BuildCommodityAuctionMailSubject(AuctionMailType.Sold, itemId, boughtFromAuction), + Global.AuctionHouseMgr.BuildAuctionSoldMailBody(player.GetGUID(), auction.BuyoutOrUnitPrice * boughtFromAuction, boughtFromAuction, (uint)depositPart, auctionHouseCut)) + .AddMoney(profit) + .SendMailTo(trans, new MailReceiver(Global.ObjAccessor.FindConnectedPlayer(auction.Owner), auction.Owner), new MailSender(this), MailCheckMask.Copied, WorldConfig.GetUIntValue(WorldCfg.MailDeliveryDelay)); + } + + MailDraft mail = new MailDraft(Global.AuctionHouseMgr.BuildCommodityAuctionMailSubject(AuctionMailType.Won, itemId, quantity), + Global.AuctionHouseMgr.BuildAuctionWonMailBody(uniqueSeller.Value, totalPrice, quantity)); + + foreach (Item item in items) + { + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_AUCTION_ITEMS_BY_ITEM); + stmt.AddValue(0, item.GetGUID().GetCounter()); + trans.Append(stmt); + + item.SetOwnerGUID(player.GetGUID()); + item.SaveToDB(trans); + mail.AddItem(item); + } + + mail.SendMailTo(trans, player, new MailSender(this), MailCheckMask.Copied); + + AuctionWonNotification packet = new AuctionWonNotification(); + packet.Info.Initialize(auctions[0], items[0]); + player.SendPacket(packet); + + for (int i = 0; i < auctions.Count; ++i) + { + if (removedItemsFromAuction[i] == auctions[i].Items.Count) + RemoveAuction(trans, auctions[i]); // bought all items + else if (removedItemsFromAuction[i] != 0) + { + var lastRemovedItemIndex = removedItemsFromAuction[i]; + for (var c = 0; c != removedItemsFromAuction[i]; ++c) + { + Global.AuctionHouseMgr.RemoveAItem(auctions[i].Items[c].GetGUID()); + } + + auctions[i].Items.RemoveRange(0, lastRemovedItemIndex); + } + } + + return true; + } + + // this function notified old bidder that his bid is no longer highest + public void SendAuctionOutbid(AuctionPosting auction, ObjectGuid newBidder, ulong newBidAmount, SQLTransaction trans) + { + Player oldBidder = Global.ObjAccessor.FindConnectedPlayer(auction.Bidder); + + // old bidder exist + if ((oldBidder || Global.CharacterCacheStorage.HasCharacterCacheEntry(auction.Bidder)))// && !sAuctionBotConfig.IsBotChar(auction.Bidder)) + { + if (oldBidder) + { + AuctionOutbidNotification packet = new AuctionOutbidNotification(); + packet.BidAmount = newBidAmount; + packet.MinIncrement = AuctionPosting.CalculateMinIncrement(newBidAmount); + packet.Info.AuctionID = auction.Id; + packet.Info.Bidder = newBidder; + packet.Info.Item = new ItemInstance(auction.Items[0]); + oldBidder.SendPacket(packet); + } + + new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Outbid, auction), "") + .AddMoney(auction.BidAmount) + .SendMailTo(trans, new MailReceiver(oldBidder, auction.Bidder), new MailSender(this), MailCheckMask.Copied); + } + } + + public void SendAuctionWon(AuctionPosting auction, Player bidder, SQLTransaction trans) + { + uint bidderAccId = 0; + if (!bidder) + bidder = Global.ObjAccessor.FindConnectedPlayer(auction.Bidder); // try lookup bidder when called from .Update + + // data for gm.log + string bidderName = ""; + bool logGmTrade = false; + + if (bidder) + { + bidderAccId = bidder.GetSession().GetAccountId(); + bidderName = bidder.GetName(); + logGmTrade = bidder.GetSession().HasPermission(RBACPermissions.LogGmTrade); + } + else + { + bidderAccId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(auction.Bidder); + logGmTrade = Global.AccountMgr.HasPermission(bidderAccId, RBACPermissions.LogGmTrade, Global.WorldMgr.GetRealmId().Realm); + + if (logGmTrade && !Global.CharacterCacheStorage.GetCharacterNameByGuid(auction.Bidder, out bidderName)) + bidderName = Global.ObjectMgr.GetCypherString(CypherStrings.Unknown); + } + + if (logGmTrade) + { + if (!Global.CharacterCacheStorage.GetCharacterNameByGuid(auction.Owner, out string ownerName)) + ownerName = Global.ObjectMgr.GetCypherString(CypherStrings.Unknown); + + uint ownerAccId = Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(auction.Owner); + + Log.outCommand(bidderAccId, $"GM {bidderName} (Account: {bidderAccId}) won item in auction: {auction.Items[0].GetName(Global.WorldMgr.GetDefaultDbcLocale())} (Entry: {auction.Items[0].GetEntry()}" + + $" Count: {auction.GetTotalItemCount()}) and pay money: {auction.BidAmount}. Original owner {ownerName} (Account: {ownerAccId})"); + } + + // receiver exist + if ((bidder != null || bidderAccId != 0))// && !sAuctionBotConfig.IsBotChar(auction.Bidder)) + { + MailDraft mail = new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Won, auction), + Global.AuctionHouseMgr.BuildAuctionWonMailBody(auction.Owner, auction.BidAmount, auction.BuyoutOrUnitPrice)); + + // set owner to bidder (to prevent delete item with sender char deleting) + // owner in `data` will set at mail receive and item extracting + foreach (Item item in auction.Items) + { + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_ITEM_OWNER); + stmt.AddValue(0, auction.Bidder.GetCounter()); + stmt.AddValue(1, item.GetGUID().GetCounter()); + trans.Append(stmt); + + mail.AddItem(item); + } + + if (bidder) + { + AuctionWonNotification packet = new AuctionWonNotification(); + packet.Info.Initialize(auction, auction.Items[0]); + bidder.SendPacket(packet); + + // FIXME: for offline player need also + bidder.UpdateCriteria(CriteriaTypes.WonAuctions, 1); + } + + mail.SendMailTo(trans, new MailReceiver(bidder, auction.Bidder), new MailSender(this), MailCheckMask.Copied); + } + else + { + // bidder doesn't exist, delete the item + foreach (Item item in auction.Items) + Global.AuctionHouseMgr.RemoveAItem(item.GetGUID(), true); + } + } + + //call this method to send mail to auction owner, when auction is successful, it does not clear ram + public void SendAuctionSold(AuctionPosting auction, Player owner, SQLTransaction trans) + { + if (!owner) + owner = Global.ObjAccessor.FindConnectedPlayer(auction.Owner); + + // owner exist + if ((owner || Global.CharacterCacheStorage.HasCharacterCacheEntry(auction.Owner)))// && !sAuctionBotConfig.IsBotChar(auction.Owner)) + { + ulong auctionHouseCut = CalcualteAuctionHouseCut(auction.BidAmount); + ulong profit = auction.BidAmount + auction.Deposit - auctionHouseCut; + + //FIXME: what do if owner offline + if (owner) + { + owner.UpdateCriteria(CriteriaTypes.GoldEarnedByAuctions, profit); + owner.UpdateCriteria(CriteriaTypes.HighestAuctionSold, auction.BidAmount); + //send auction owner notification, bidder must be current! + owner.GetSession().SendAuctionClosedNotification(auction, (float)WorldConfig.GetIntValue(WorldCfg.MailDeliveryDelay), true); + } + + new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Sold, auction), + Global.AuctionHouseMgr.BuildAuctionSoldMailBody(auction.Bidder, auction.BidAmount, auction.BuyoutOrUnitPrice, (uint)auction.Deposit, auctionHouseCut)) + .AddMoney(profit) + .SendMailTo(trans, new MailReceiver(owner, auction.Owner), new MailSender(this), MailCheckMask.Copied, WorldConfig.GetUIntValue(WorldCfg.MailDeliveryDelay)); + } + } + + public void SendAuctionExpired(AuctionPosting auction, SQLTransaction trans) + { + Player owner = Global.ObjAccessor.FindConnectedPlayer(auction.Owner); + // owner exist + if ((owner || Global.CharacterCacheStorage.HasCharacterCacheEntry(auction.Owner)))// && !sAuctionBotConfig.IsBotChar(auction.Owner)) + { + if (owner) + owner.GetSession().SendAuctionClosedNotification(auction, 0.0f, false); + + MailDraft mail = new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Expired, auction), ""); + + foreach (Item item in auction.Items) + mail.AddItem(item); + + mail.SendMailTo(trans, new MailReceiver(owner, auction.Owner), new MailSender(this), MailCheckMask.Copied, 0); + } + else + { + // owner doesn't exist, delete the item + foreach (Item item in auction.Items) + Global.AuctionHouseMgr.RemoveAItem(item.GetGUID(), true); + } + } + + public void SendAuctionRemoved(AuctionPosting auction, Player owner, SQLTransaction trans) + { + MailDraft draft = new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Cancelled, auction), ""); + + foreach (Item item in auction.Items) + draft.AddItem(item); + + draft.SendMailTo(trans, owner, new MailSender(this), MailCheckMask.Copied); + } + + //this function sends mail, when auction is cancelled to old bidder + public void SendAuctionCancelledToBidder(AuctionPosting auction, SQLTransaction trans) + { + Player bidder = Global.ObjAccessor.FindConnectedPlayer(auction.Bidder); + + // bidder exist + if ((bidder || Global.CharacterCacheStorage.HasCharacterCacheEntry(auction.Bidder)))// && !sAuctionBotConfig.IsBotChar(auction.Bidder)) + new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Removed, auction), "") + .AddMoney(auction.BidAmount) + .SendMailTo(trans, new MailReceiver(bidder, auction.Bidder), new MailSender(this), MailCheckMask.Copied); + } + + public void SendAuctionInvoice(AuctionPosting auction, Player owner, SQLTransaction trans) + { + if (!owner) + owner = Global.ObjAccessor.FindConnectedPlayer(auction.Owner); + + // owner exist (online or offline) + if ((owner || Global.CharacterCacheStorage.HasCharacterCacheEntry(auction.Owner)))// && !sAuctionBotConfig.IsBotChar(auction.Owner)) + { + ByteBuffer tempBuffer = new ByteBuffer(); + tempBuffer.WritePackedTime(GameTime.GetGameTime() + WorldConfig.GetIntValue(WorldCfg.MailDeliveryDelay)); + uint eta = tempBuffer.ReadUInt32(); + + new MailDraft(Global.AuctionHouseMgr.BuildItemAuctionMailSubject(AuctionMailType.Invoice, auction), + Global.AuctionHouseMgr.BuildAuctionInvoiceMailBody(auction.Bidder, auction.BidAmount, auction.BuyoutOrUnitPrice, (uint)auction.Deposit, + CalcualteAuctionHouseCut(auction.BidAmount), WorldConfig.GetUIntValue(WorldCfg.MailDeliveryDelay), eta)) + .SendMailTo(trans, new MailReceiver(owner, auction.Owner), new MailSender(this), MailCheckMask.Copied); + } + } + + class PlayerReplicateThrottleData + { + public uint Global; + public uint Cursor; + public uint Tombstone; + public DateTime NextAllowedReplication = DateTime.MinValue; + + public bool IsReplicationInProgress() { return Cursor != Tombstone && Global != 0; } + } + + AuctionHouseRecord _auctionHouse; + + SortedList _itemsByAuctionId = new SortedList(); // ordered for replicate + Dictionary _soldItemsById = new Dictionary(); + SortedDictionary _buckets = new SortedDictionary();// ordered for search by itemid only + Dictionary _commodityQuotes = new Dictionary(); + + MultiMap _playerOwnedAuctions = new MultiMap(); + MultiMap _playerBidderAuctions = new MultiMap(); + + // Map of throttled players for GetAll, and throttle expiry time + // Stored here, rather than player object to maintain persistence after logout + Dictionary _replicateThrottleMap = new Dictionary(); } - public class AuctionEntry + public class AuctionPosting { - public void BuildAuctionInfo(List items, bool listAuctionItems) - { - Item item = Global.AuctionMgr.GetAItem(itemGUIDLow); - if (!item) - { - Log.outError(LogFilter.Server, "AuctionEntry:BuildAuctionInfo: Auction {0} has a non-existent item: {1}", Id, itemGUIDLow); - return; - } - AuctionItem auctionItem = new AuctionItem(); - - auctionItem.AuctionID = (int)Id; - auctionItem.Item = new ItemInstance(item); - auctionItem.BuyoutPrice = buyout; - auctionItem.CensorBidInfo = false; - auctionItem.CensorServerSideInfo = listAuctionItems; - auctionItem.Charges = item.GetSpellCharges(); - auctionItem.Count = (int)item.GetCount(); - auctionItem.DeleteReason = 0; // Always 0 ? - auctionItem.DurationLeft = (int)((expire_time - Time.UnixTime) * Time.InMilliseconds); - auctionItem.EndTime = (uint)expire_time; - auctionItem.Flags = 0; // todo - auctionItem.ItemGuid = item.GetGUID(); - auctionItem.MinBid = startbid; - auctionItem.Owner = ObjectGuid.Create(HighGuid.Player, owner); - auctionItem.OwnerAccountID = ObjectGuid.Create(HighGuid.WowAccount, Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(auctionItem.Owner)); - auctionItem.MinIncrement = bidder != 0 ? GetAuctionOutBid() : 0; - auctionItem.Bidder = bidder != 0 ? ObjectGuid.Create(HighGuid.Player, bidder) : ObjectGuid.Empty; - auctionItem.BidAmount = bidder != 0 ? bid : 0; - - for (EnchantmentSlot c = 0; c < EnchantmentSlot.MaxInspected; c++) - { - if (item.GetEnchantmentId(c) == 0) - continue; - - auctionItem.Enchantments.Add(new ItemEnchantData((int)item.GetEnchantmentId(c), item.GetEnchantmentDuration(c), (int)item.GetEnchantmentCharges(c), (byte)c)); - } - - byte i = 0; - foreach (SocketedGem gemData in item.m_itemData.Gems) - { - if (gemData.ItemId != 0) - { - ItemGemData gem = new ItemGemData(); - gem.Slot = i; - gem.Item = new ItemInstance(gemData); - auctionItem.Gems.Add(gem); - } - ++i; - } - - items.Add(auctionItem); - } - - public ulong GetAuctionCut() - { - long cut = (long)(MathFunctions.CalculatePct(bid, auctionHouseEntry.ConsignmentRate) * WorldConfig.GetFloatValue(WorldCfg.RateAuctionCut)); - return (ulong)Math.Max(cut, 0); - } - - /// - /// the sum of outbid is (1% from current bid)*5, if bid is very small, it is 1c - /// - /// - public ulong GetAuctionOutBid() - { - ulong outbid = MathFunctions.CalculatePct(bid, 5); - return outbid != 0 ? outbid : 1; - } - - public void DeleteFromDB(SQLTransaction trans) - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_AUCTION); - stmt.AddValue(0, Id); - trans.Append(stmt); - } - - public void SaveToDB(SQLTransaction trans) - { - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_AUCTION); - stmt.AddValue(0, Id); - stmt.AddValue(1, auctioneer); - stmt.AddValue(2, itemGUIDLow); - stmt.AddValue(3, owner); - stmt.AddValue(4, buyout); - stmt.AddValue(5, expire_time); - stmt.AddValue(6, bidder); - stmt.AddValue(7, bid); - stmt.AddValue(8, startbid); - stmt.AddValue(9, deposit); - trans.Append(stmt); - } - - public bool LoadFromDB(SQLFields fields) - { - Id = fields.Read(0); - auctioneer = fields.Read(1); - itemGUIDLow = fields.Read(2); - itemEntry = fields.Read(3); - itemCount = fields.Read(4); - owner = fields.Read(5); - buyout = fields.Read(6); - expire_time = fields.Read(7); - bidder = fields.Read(8); - bid = fields.Read(9); - startbid = fields.Read(10); - deposit = fields.Read(11); - - CreatureData auctioneerData = Global.ObjectMgr.GetCreatureData(auctioneer); - if (auctioneerData == null) - { - Log.outError(LogFilter.Server, "Auction {0} has not a existing auctioneer (GUID : {1})", Id, auctioneer); - return false; - } - - CreatureTemplate auctioneerInfo = Global.ObjectMgr.GetCreatureTemplate(auctioneerData.id); - if (auctioneerInfo == null) - { - Log.outError(LogFilter.Server, "Auction {0} has not a existing auctioneer (GUID : {1} Entry: {2})", Id, auctioneer, auctioneerData.id); - return false; - } - - factionTemplateId = auctioneerInfo.Faction; - auctionHouseEntry = Global.AuctionMgr.GetAuctionHouseEntry(factionTemplateId, ref houseId); - if (auctionHouseEntry == null) - { - Log.outError(LogFilter.Server, "Auction {0} has auctioneer (GUID : {1} Entry: {2}) with wrong faction {3}", Id, auctioneer, auctioneerData.id, factionTemplateId); - return false; - } - - // check if sold item exists for guid - // and itemEntry in fact (GetAItem will fail if problematic in result check in AuctionHouseMgr.LoadAuctionItems) - if (!Global.AuctionMgr.GetAItem(itemGUIDLow)) - { - Log.outError(LogFilter.Server, "Auction {0} has not a existing item : {1}", Id, itemGUIDLow); - return false; - } - return true; - } - - public bool LoadFromFieldList(SQLFields fields) - { - // Loads an AuctionEntry item from a field list. Unlike "LoadFromDB()", this one - // does not require the AuctionEntryMap to have been loaded with items. It simply - // acts as a wrapper to fill out an AuctionEntry struct from a field list - - Id = fields.Read(0); - auctioneer = fields.Read(1); - itemGUIDLow = fields.Read(2); - itemEntry = fields.Read(3); - itemCount = fields.Read(4); - owner = fields.Read(5); - buyout = fields.Read(6); - expire_time = fields.Read(7); - bidder = fields.Read(8); - bid = fields.Read(9); - startbid = fields.Read(10); - deposit = fields.Read(11); - - CreatureData auctioneerData = Global.ObjectMgr.GetCreatureData(auctioneer); - if (auctioneerData == null) - { - Log.outError(LogFilter.Server, "AuctionEntry:LoadFromFieldList() - Auction {0} has not a existing auctioneer (GUID : {1})", Id, auctioneer); - return false; - } - - CreatureTemplate auctioneerInfo = Global.ObjectMgr.GetCreatureTemplate(auctioneerData.id); - if (auctioneerInfo == null) - { - Log.outError(LogFilter.Server, "AuctionEntry:LoadFromFieldList() - Auction {0} has not a existing auctioneer (GUID : {1} Entry: {2})", Id, auctioneer, auctioneerData.id); - return false; - } - - factionTemplateId = auctioneerInfo.Faction; - auctionHouseEntry = Global.AuctionMgr.GetAuctionHouseEntry(factionTemplateId); - - if (auctionHouseEntry == null) - { - Log.outError(LogFilter.Server, "AuctionEntry:LoadFromFieldList() - Auction {0} has auctioneer (GUID : {1} Entry: {2}) with wrong faction {3}", Id, auctioneer, auctioneerData.id, factionTemplateId); - return false; - } - - return true; - } - - public string BuildAuctionMailSubject(MailAuctionAnswers response) - { - return $"{itemEntry}:0:{(uint)response}:{Id}:{itemCount}"; - } - - public static string BuildAuctionMailBody(ulong lowGuid, ulong bid, ulong buyout, ulong deposit, ulong cut) - { - return $"{lowGuid}:{bid}:{buyout}:{deposit}:{cut}"; - } - - // helpers - public uint GetHouseId() { return houseId; } - public uint GetHouseFaction() { return auctionHouseEntry.FactionID; } - public uint Id; - public ulong auctioneer; // creature low guid - public ulong itemGUIDLow; - public uint itemEntry; - public uint itemCount; - public ulong owner; - public ulong startbid; //maybe useless - public ulong bid; - public ulong buyout; - public long expire_time; - public ulong bidder; - public ulong deposit; //deposit can be calculated only when creating auction - public uint etime; - uint houseId; - public AuctionHouseRecord auctionHouseEntry; // in AuctionHouse.dbc - public uint factionTemplateId; + public AuctionsBucketData Bucket; + + public List Items = new List(); + public ObjectGuid Owner; + public ObjectGuid OwnerAccount; + public ObjectGuid Bidder; + public ulong MinBid = 0; + public ulong BuyoutOrUnitPrice = 0; + public ulong Deposit = 0; + public ulong BidAmount = 0; + public DateTime StartTime = DateTime.MinValue; + public DateTime EndTime = DateTime.MinValue; + + public List BidderHistory = new List(); + + public bool IsCommodity() + { + return Items.Count > 1 || Items[0].GetTemplate().GetMaxStackSize() > 1; + } + + public uint GetTotalItemCount() + { + return (uint)Items.Sum(item => { return item.GetCount(); }); + } + + public void BuildAuctionItem(AuctionItem auctionItem, bool alwaysSendItem, bool sendKey, bool censorServerInfo, bool censorBidInfo) + { + // SMSG_AUCTION_LIST_BIDDER_ITEMS_RESULT, SMSG_AUCTION_LIST_ITEMS_RESULT (if not commodity), SMSG_AUCTION_LIST_OWNER_ITEMS_RESULT, SMSG_AUCTION_REPLICATE_RESPONSE (if not commodity) + //auctionItem.Item - here to unify comment + + // all (not optional<>) + auctionItem.Count = (int)GetTotalItemCount(); + auctionItem.Flags = 0; + auctionItem.AuctionID = Id; + auctionItem.Owner = Owner; + + // prices set when filled + if (IsCommodity()) + { + if (alwaysSendItem) + { + auctionItem.Item.HasValue = true; + auctionItem.Item.Value = new ItemInstance(Items[0]); + } + + auctionItem.UnitPrice.Set(BuyoutOrUnitPrice); + } + else + { + auctionItem.Item.HasValue = true; + auctionItem.Item.Value = new ItemInstance(Items[0]); + auctionItem.Charges = new[] { Items[0].GetSpellCharges(0), Items[0].GetSpellCharges(1), Items[0].GetSpellCharges(2), Items[0].GetSpellCharges(3), Items[0].GetSpellCharges(4) }.Max(); + for (EnchantmentSlot enchantmentSlot = 0; enchantmentSlot < EnchantmentSlot.MaxInspected; enchantmentSlot++) + { + uint enchantId = Items[0].GetEnchantmentId(enchantmentSlot); + if (enchantId == 0) + continue; + + auctionItem.Enchantments.Add(new ItemEnchantData(enchantId, Items[0].GetEnchantmentDuration(enchantmentSlot), Items[0].GetEnchantmentCharges(enchantmentSlot), (byte)enchantmentSlot)); + } + + for (byte i = 0; i < Items[0].m_itemData.Gems.Size(); ++i) + { + SocketedGem gemData = Items[0].m_itemData.Gems[i]; + if (gemData.ItemId != 0) + { + ItemGemData gem = new ItemGemData(); + gem.Slot = i; + gem.Item = new ItemInstance(gemData); + auctionItem.Gems.Add(gem); + } + } + + if (MinBid != 0) + auctionItem.MinBid.Set(MinBid); + + ulong minIncrement = CalculateMinIncrement(); + if (minIncrement != 0) + auctionItem.MinIncrement.Set(minIncrement); + + if (BuyoutOrUnitPrice != 0) + auctionItem.BuyoutPrice.Set(BuyoutOrUnitPrice); + } + + // all (not optional<>) + auctionItem.DurationLeft = (int)Math.Max((EndTime - GameTime.GetGameTimeSystemPoint()).ToMilliseconds(), 0l); + auctionItem.DeleteReason = 0; + + // SMSG_AUCTION_LIST_ITEMS_RESULT (only if owned) + auctionItem.CensorServerSideInfo = censorServerInfo; + auctionItem.ItemGuid = IsCommodity() ? ObjectGuid.Empty : Items[0].GetGUID(); + auctionItem.OwnerAccountID = OwnerAccount; + auctionItem.EndTime = (uint)Time.DateTimeToUnixTime(EndTime); + + // SMSG_AUCTION_LIST_BIDDER_ITEMS_RESULT, SMSG_AUCTION_LIST_ITEMS_RESULT (if has bid), SMSG_AUCTION_LIST_OWNER_ITEMS_RESULT, SMSG_AUCTION_REPLICATE_RESPONSE (if has bid) + auctionItem.CensorBidInfo = censorBidInfo; + if (!Bidder.IsEmpty()) + { + auctionItem.Bidder.Set(Bidder); + auctionItem.BidAmount.Set(BidAmount); + } + + // SMSG_AUCTION_LIST_BIDDER_ITEMS_RESULT, SMSG_AUCTION_LIST_OWNER_ITEMS_RESULT, SMSG_AUCTION_REPLICATE_RESPONSE (if commodity) + if (sendKey) + auctionItem.AuctionBucketKey.Set(new AuctionBucketKey(AuctionsBucketKey.ForItem(Items[0]))); + } + + public static ulong CalculateMinIncrement(ulong bidAmount) + { + return MathFunctions.CalculatePct(bidAmount / MoneyConstants.Silver, 5) * MoneyConstants.Silver; + } + + public ulong CalculateMinIncrement() { return CalculateMinIncrement(BidAmount); } + + public class Sorter : IComparer + { + public Sorter(LocaleConstant locale, AuctionSortDef[] sorts, int sortCount) + { + _locale = locale; + _sorts = sorts; + _sortCount = sortCount; + } + + public int Compare(AuctionPosting left, AuctionPosting right) + { + for (var i = 0; i < _sortCount; ++i) + { + int ordering = CompareColumns(_sorts[i].SortOrder, left, right); + if (ordering != 0) + return (ordering < 0).CompareTo(!_sorts[i].ReverseSort); + } + + // Auctions are processed in LIFO order + if (left.StartTime != right.StartTime) + return left.StartTime.CompareTo(right.StartTime); + + return left.Id.CompareTo(right.Id); + } + + int CompareColumns(AuctionHouseSortOrder column, AuctionPosting left, AuctionPosting right) + { + switch (column) + { + case AuctionHouseSortOrder.Price: + { + ulong leftPrice = left.BuyoutOrUnitPrice != 0 ? left.BuyoutOrUnitPrice : (left.BidAmount != 0 ? left.BidAmount : left.MinBid); + ulong rightPrice = right.BuyoutOrUnitPrice != 0 ? right.BuyoutOrUnitPrice : (right.BidAmount != 0 ? right.BidAmount : right.MinBid); + return (int)(leftPrice - rightPrice); + } + case AuctionHouseSortOrder.Name: + return left.Bucket.FullName[(int)_locale].CompareTo(right.Bucket.FullName[(int)_locale]); + case AuctionHouseSortOrder.Level: + { + int leftLevel = left.Items[0].GetModifier(ItemModifier.BattlePetSpeciesId) == 0 ? left.Items[0].GetRequiredLevel() : (int)left.Items[0].GetModifier(ItemModifier.BattlePetLevel); + int rightLevel = right.Items[0].GetModifier(ItemModifier.BattlePetSpeciesId) == 0 ? right.Items[0].GetRequiredLevel() : (int)right.Items[0].GetModifier(ItemModifier.BattlePetLevel); + return leftLevel - rightLevel; + } + case AuctionHouseSortOrder.Bid: + return (int)(left.BidAmount - right.BidAmount); + case AuctionHouseSortOrder.Buyout: + return (int)(left.BuyoutOrUnitPrice - right.BuyoutOrUnitPrice); + default: + break; + } + + return 0; + } + + LocaleConstant _locale; + AuctionSortDef[] _sorts; + int _sortCount; + } + } + + + + public class AuctionsBucketData + { + public AuctionsBucketKey Key; + + // filter helpers + public byte ItemClass; + public byte ItemSubClass; + public byte InventoryType; + public AuctionHouseFilterMask QualityMask; + public uint[] QualityCounts = new uint[(int)ItemQuality.Max]; + public ulong MinPrice; // for sort + public (uint Id, uint Count)[] ItemModifiedAppearanceId = new (uint Id, uint Count)[4]; // for uncollected search + public byte RequiredLevel = 0; // for usable search + public byte MinBattlePetLevel = 0; + public byte MaxBattlePetLevel = 0; + public string[] FullName = new string[(int)LocaleConstant.Total]; + + public List Auctions = new List(); + + public void BuildBucketInfo(BucketInfo bucketInfo, Player player) + { + bucketInfo.Key = new AuctionBucketKey(Key); + bucketInfo.MinPrice = MinPrice; + bucketInfo.TotalQuantity = 0; + + foreach (AuctionPosting auction in Auctions) + { + foreach (Item item in auction.Items) + { + bucketInfo.TotalQuantity += (int)item.GetCount(); + + if (Key.BattlePetSpeciesId != 0) + { + uint breedData = item.GetModifier(ItemModifier.BattlePetBreedData); + uint breedId = breedData & 0xFFFFFF; + byte quality = (byte)((breedData >> 24) & 0xFF); + byte level = (byte)(item.GetModifier(ItemModifier.BattlePetLevel)); + + bucketInfo.MaxBattlePetQuality.Set(bucketInfo.MaxBattlePetQuality.HasValue ? Math.Max(bucketInfo.MaxBattlePetQuality.Value, quality) : quality); + bucketInfo.MaxBattlePetLevel.Set(bucketInfo.MaxBattlePetLevel.HasValue ? Math.Max(bucketInfo.MaxBattlePetLevel.Value, level) : level); + bucketInfo.BattlePetBreedID.Set((byte)breedId); + } + } + + bucketInfo.ContainsOwnerItem = bucketInfo.ContainsOwnerItem || auction.Owner == player.GetGUID(); + } + + bucketInfo.ContainsOnlyCollectedAppearances = true; + foreach (var appearance in ItemModifiedAppearanceId) + { + if (appearance.Item1 != 0) + { + bucketInfo.ItemModifiedAppearanceIDs.Add(appearance.Item1); + if (!player.GetSession().GetCollectionMgr().HasItemAppearance(appearance.Item1).PermAppearance) + bucketInfo.ContainsOnlyCollectedAppearances = false; + } + } + } + + public class Sorter : IComparer + { + public Sorter(LocaleConstant locale, AuctionSortDef[] sorts, int sortCount) + { + _locale = locale; + _sorts = sorts; + _sortCount = sortCount; + } + + public int Compare(AuctionsBucketData left, AuctionsBucketData right) + { + for (var i = 0; i < _sortCount; ++i) + { + int ordering = CompareColumns(_sorts[i].SortOrder, left, right); + if (ordering != 0) + return (ordering < 0).CompareTo(!_sorts[i].ReverseSort); + } + + return left.Key != right.Key ? 1 : 0; + } + + int CompareColumns(AuctionHouseSortOrder column, AuctionsBucketData left, AuctionsBucketData right) + { + switch (column) + { + case AuctionHouseSortOrder.Price: + case AuctionHouseSortOrder.Bid: + case AuctionHouseSortOrder.Buyout: + return (int)((long)left.MinPrice - (long)right.MinPrice); + case AuctionHouseSortOrder.Name: + return left.FullName[(int)_locale].CompareTo(right.FullName[(int)_locale]); + case AuctionHouseSortOrder.Level: + { + int leftLevel = left.MaxBattlePetLevel == 0 ? left.RequiredLevel : left.MaxBattlePetLevel; + int rightLevel = right.MaxBattlePetLevel == 0 ? right.RequiredLevel : right.MaxBattlePetLevel; + return leftLevel - rightLevel; + } + default: + break; + } + + return 0; + } + + LocaleConstant _locale; + AuctionSortDef[] _sorts; + int _sortCount; + } + } + + public class CommodityQuote + { + public ulong TotalPrice; + public uint Quantity; + public DateTime ValidTo = DateTime.MinValue; + } + + public class AuctionThrottleResult + { + public TimeSpan DelayUntilNext; + public bool Throttled; + + public AuctionThrottleResult(TimeSpan delayUntilNext, bool throttled) + { + DelayUntilNext = delayUntilNext; + Throttled = throttled; + } + } + + public class AuctionsBucketKey : IComparable + { + public uint ItemId { get; set; } + public ushort ItemLevel { get; set; } + public ushort BattlePetSpeciesId { get; set; } + public ushort SuffixItemNameDescriptionId { get; set; } + + public AuctionsBucketKey(uint itemId, ushort itemLevel, ushort battlePetSpeciesId, ushort suffixItemNameDescriptionId) + { + ItemId = itemId; + ItemLevel = itemLevel; + BattlePetSpeciesId = battlePetSpeciesId; + SuffixItemNameDescriptionId = suffixItemNameDescriptionId; + } + + public AuctionsBucketKey(AuctionBucketKey key) + { + ItemId = key.ItemID; + ItemLevel = key.ItemLevel; + BattlePetSpeciesId = (ushort)(key.BattlePetSpeciesID.HasValue ? key.BattlePetSpeciesID.Value : 0); + SuffixItemNameDescriptionId = (ushort)(key.SuffixItemNameDescriptionID.HasValue ? key.SuffixItemNameDescriptionID.Value : 0); + } + + public int CompareTo(AuctionsBucketKey other) + { + return ItemId.CompareTo(other.ItemId); + } + + public static bool operator ==(AuctionsBucketKey right, AuctionsBucketKey left) + { + return right.ItemId == left.ItemId + && right.ItemLevel == left.ItemLevel + && right.BattlePetSpeciesId == left.BattlePetSpeciesId + && right.SuffixItemNameDescriptionId == left.SuffixItemNameDescriptionId; + } + public static bool operator !=(AuctionsBucketKey right, AuctionsBucketKey left) { return !(right == left); } + + public override bool Equals(object obj) + { + return base.Equals(obj); + } + + public override int GetHashCode() + { + return ItemId.GetHashCode() ^ ItemLevel.GetHashCode() ^ BattlePetSpeciesId.GetHashCode() ^ SuffixItemNameDescriptionId.GetHashCode(); + } + + public static AuctionsBucketKey ForItem(Item item) + { + ItemTemplate itemTemplate = item.GetTemplate(); + if (itemTemplate.GetMaxStackSize() == 1) + { + return new AuctionsBucketKey(item.GetEntry(), (ushort)Item.GetItemLevel(itemTemplate, item.GetBonus(), 0, (uint)item.GetRequiredLevel(), 0, 0, 0, false, 0), + (ushort)item.GetModifier(ItemModifier.BattlePetSpeciesId), (ushort)item.GetBonus().Suffix); + } + else + return ForCommodity(item.GetEntry()); + } + + public static AuctionsBucketKey ForCommodity(uint itemId) + { + return new AuctionsBucketKey(itemId, 0, 0, 0); + } } public class AuctionSearchClassFilters { + public SubclassFilter[] Classes = new SubclassFilter[(int)ItemClass.Max]; + + public AuctionSearchClassFilters() + { + for (var i = 0; i < (int)ItemClass.Max; ++i) + Classes[i] = new SubclassFilter(); + } + + public class SubclassFilter + { + public FilterType SubclassMask; + public uint[] InvTypes = new uint[ItemConst.MaxItemSubclassTotal]; + } + public enum FilterType : uint { SkipClass = 0, SkipSubclass = 0xFFFFFFFF, SkipInvtype = 0xFFFFFFFF } + } - public Array Classes = new Array((int)ItemClass.Max); + class AuctionsResultBuilder + { + uint _offset; + IComparer _sorter; + AuctionHouseResultLimits _maxResults; + List _items = new List(); + bool _hasMoreResults; - public class SubclassFilter + public AuctionsResultBuilder(uint offset, IComparer sorter, AuctionHouseResultLimits maxResults) { - public FilterType SubclassMask = FilterType.SkipClass; - public Array InvTypes = new Array(ItemConst.MaxItemSubclassTotal); + _offset = offset; + _sorter = sorter; + _maxResults = maxResults; + _hasMoreResults = false; + } + + public void AddItem(T item) + { + var index = _items.BinarySearch(item, _sorter); + if (index < 0) + index = ~index; + _items.Insert(index, item); + if (_items.Count > (int)_maxResults + _offset) + { + _items.RemoveAt(_items.Count - 1); + _hasMoreResults = true; + } + } + + public Span GetResultRange() + { + Span h = _items.ToArray(); + return h.Slice((int)_offset); + } + + public bool HasMoreResults() + { + return _hasMoreResults; } } } diff --git a/Source/Game/Chat/Commands/ReloadCommand.cs b/Source/Game/Chat/Commands/ReloadCommand.cs index 177bc29c1..b1d4dea74 100644 --- a/Source/Game/Chat/Commands/ReloadCommand.cs +++ b/Source/Game/Chat/Commands/ReloadCommand.cs @@ -86,8 +86,7 @@ namespace Game.Chat { // Reload dynamic data tables from the database Log.outInfo(LogFilter.Server, "Re-Loading Auctions..."); - Global.AuctionMgr.LoadAuctionItems(); - Global.AuctionMgr.LoadAuctions(); + Global.AuctionHouseMgr.LoadAuctions(); handler.SendGlobalGMSysMessage("Auctions reloaded."); return true; } diff --git a/Source/Game/Entities/Player/CollectionManager.cs b/Source/Game/Entities/Player/CollectionManager.cs index 13729829e..44e4122b2 100644 --- a/Source/Game/Entities/Player/CollectionManager.cs +++ b/Source/Game/Entities/Player/CollectionManager.cs @@ -760,15 +760,15 @@ namespace Game.Entities } } - public Tuple HasItemAppearance(uint itemModifiedAppearanceId) + public (bool PermAppearance, bool TempAppearance) HasItemAppearance(uint itemModifiedAppearanceId) { if (itemModifiedAppearanceId < _appearances.Count && _appearances.Get((int)itemModifiedAppearanceId)) - return Tuple.Create(true, false); + return (true, false); if (_temporaryAppearances.ContainsKey(itemModifiedAppearanceId)) - return Tuple.Create(true, true); + return (true, true); - return Tuple.Create(false, false); + return (false, false); } public List GetItemsProvidingTemporaryAppearance(uint itemModifiedAppearanceId) @@ -776,6 +776,15 @@ namespace Game.Entities return _temporaryAppearances.LookupByKey(itemModifiedAppearanceId); } + public List GetAppearanceIds() + { + List appearances = new List(); + foreach (int id in _appearances) + appearances.Add(CliDB.ItemModifiedAppearanceStorage.LookupByKey(id).ItemAppearanceID); + + return appearances; + } + public void SetAppearanceIsFavorite(uint itemModifiedAppearanceId, bool apply) { var apperanceState = _favoriteAppearances.LookupByKey(itemModifiedAppearanceId); diff --git a/Source/Game/Entities/Player/Player.DB.cs b/Source/Game/Entities/Player/Player.DB.cs index 53c43f6f2..640d614d8 100644 --- a/Source/Game/Entities/Player/Player.DB.cs +++ b/Source/Game/Entities/Player/Player.DB.cs @@ -4013,6 +4013,10 @@ namespace Game.Entities stmt.AddValue(0, guid); trans.Append(stmt); + stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_CHARACTER_FAVORITE_AUCTIONS_BY_CHAR); + stmt.AddValue(0, guid); + trans.Append(stmt); + Corpse.DeleteFromDB(playerGuid, trans); Garrison.DeleteFromDB(guid, trans); diff --git a/Source/Game/Globals/Global.cs b/Source/Game/Globals/Global.cs index 46a2d24d9..1e7e0ffa6 100644 --- a/Source/Game/Globals/Global.cs +++ b/Source/Game/Globals/Global.cs @@ -105,7 +105,7 @@ public static class Global public static GameEventManager GameEventMgr { get { return GameEventManager.Instance; } } public static CreatureTextManager CreatureTextMgr { get { return CreatureTextManager.Instance; } } - public static AuctionManager AuctionMgr { get { return AuctionManager.Instance; } } + public static AuctionManager AuctionHouseMgr { get { return AuctionManager.Instance; } } public static SpellManager SpellMgr { get { return SpellManager.Instance; } } public static SupportManager SupportMgr { get { return SupportManager.Instance; } } diff --git a/Source/Game/Globals/ObjectManager.cs b/Source/Game/Globals/ObjectManager.cs index 2c6002a34..fff450972 100644 --- a/Source/Game/Globals/ObjectManager.cs +++ b/Source/Game/Globals/ObjectManager.cs @@ -9275,7 +9275,8 @@ namespace Game // Cleanup other tables from not existed guids ( >= hiItemGuid) DB.Characters.Execute("DELETE FROM character_inventory WHERE item >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed()); // One-time query DB.Characters.Execute("DELETE FROM mail_items WHERE item_guid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed()); // One-time query - DB.Characters.Execute("DELETE FROM auctionhouse WHERE itemguid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed()); // One-time query + DB.Characters.Execute("DELETE a, ab, ai FROM auctionhouse a LEFT JOIN auction_bidders ab ON ab.auctionId = a.id LEFT JOIN auction_items ai ON ai.auctionId = a.id WHERE ai.itemGuid >= '{0}'", + GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed()); // One-time query DB.Characters.Execute("DELETE FROM guild_bank_item WHERE item_guid >= {0}", GetGuidSequenceGenerator(HighGuid.Item).GetNextAfterMaxUsed()); // One-time query result = DB.World.Query("SELECT MAX(guid) FROM transports"); diff --git a/Source/Game/Handlers/AuctionHandler.cs b/Source/Game/Handlers/AuctionHandler.cs index 641410419..c7cd9e598 100644 --- a/Source/Game/Handlers/AuctionHandler.cs +++ b/Source/Game/Handlers/AuctionHandler.cs @@ -25,578 +25,23 @@ using Game.Network; using Game.Network.Packets; using System; using System.Collections.Generic; +using System.Linq; namespace Game { public partial class WorldSession { - [WorldPacketHandler(ClientOpcodes.AuctionHelloRequest)] - void HandleAuctionHello(AuctionHelloRequest packet) - { - Creature unit = GetPlayer().GetNPCIfCanInteractWith(packet.Guid, NPCFlags.Auctioneer, NPCFlags2.None); - if (!unit) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionHelloOpcode - {0} not found or you can't interact with him.", packet.Guid.ToString()); - return; - } - - // remove fake death - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - SendAuctionHello(packet.Guid, unit); - } - - public void SendAuctionHello(ObjectGuid guid, Creature unit) - { - if (GetPlayer().GetLevel() < WorldConfig.GetIntValue(WorldCfg.AuctionLevelReq)) - { - SendNotification(Global.ObjectMgr.GetCypherString(CypherStrings.AuctionReq), WorldConfig.GetIntValue(WorldCfg.AuctionLevelReq)); - return; - } - - AuctionHouseRecord ahEntry = Global.AuctionMgr.GetAuctionHouseEntry(unit.GetFaction()); - if (ahEntry == null) - return; - - AuctionHelloResponse packet = new AuctionHelloResponse(); - packet.Guid = guid; - packet.OpenForBusiness = false; // 3.3.3: 1 - AH enabled, 0 - AH disabled - SendPacket(packet); - } - - public void SendAuctionCommandResult(AuctionEntry auction, AuctionAction action, AuctionError errorCode, uint bidError = 0) - { - AuctionCommandResult auctionCommandResult = new AuctionCommandResult(); - auctionCommandResult.InitializeAuction(auction); - auctionCommandResult.Command = (int)action; - auctionCommandResult.ErrorCode = (int)errorCode; - SendPacket(auctionCommandResult); - } - - public void SendAuctionOutBidNotification(AuctionEntry auction, Item item) - { - AuctionOutbidNotification packet = new AuctionOutbidNotification(); - packet.BidAmount = auction.bid; - packet.MinIncrement = auction.GetAuctionOutBid(); - packet.Info.Initialize(auction, item); - SendPacket(packet); - } - - public void SendAuctionClosedNotification(AuctionEntry auction, float mailDelay, bool sold, Item item) - { - AuctionClosedNotification packet = new AuctionClosedNotification(); - packet.Info.Initialize(auction, item); - packet.ProceedsMailDelay = mailDelay; - packet.Sold = sold; - SendPacket(packet); - } - - public void SendAuctionWonNotification(AuctionEntry auction, Item item) - { - AuctionWonNotification packet = new AuctionWonNotification(); - packet.Info.Initialize(auction, item); - SendPacket(packet); - } - - public void SendAuctionOwnerBidNotification(AuctionEntry auction, Item item) - { - AuctionOwnerBidNotification packet = new AuctionOwnerBidNotification(); - packet.Info.Initialize(auction, item); - packet.Bidder = ObjectGuid.Create(HighGuid.Player, auction.bidder); - packet.MinIncrement = auction.GetAuctionOutBid(); - SendPacket(packet); - } - - [WorldPacketHandler(ClientOpcodes.AuctionSellItem)] - void HandleAuctionSellItem(AuctionSellItem packet) - { - foreach (var aitem in packet.Items) - if (aitem.Guid.IsEmpty() || aitem.UseCount == 0 || aitem.UseCount > 1000) - return; - - if (packet.MinBid == 0 || packet.RunTime == 0) - return; - - if (packet.MinBid > PlayerConst.MaxMoneyAmount || packet.BuyoutPrice > PlayerConst.MaxMoneyAmount) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionSellItem - Player {0} ({1}) attempted to sell item with higher price than max gold amount.", GetPlayer().GetName(), GetPlayer().GetGUID().ToString()); - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - - Creature creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionSellItem - {0} not found or you can't interact with him.", packet.Auctioneer.ToString()); - return; - } - - uint houseId = 0; - AuctionHouseRecord auctionHouseEntry = Global.AuctionMgr.GetAuctionHouseEntry(creature.GetFaction(), ref houseId); - if (auctionHouseEntry == null) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionSellItem - {0} has wrong faction.", packet.Auctioneer.ToString()); - return; - } - - packet.RunTime *= Time.Minute; - switch (packet.RunTime) - { - case 1 * SharedConst.MinAuctionTime: - case 2 * SharedConst.MinAuctionTime: - case 4 * SharedConst.MinAuctionTime: - break; - default: - return; - } - - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - uint finalCount = 0; - Item[] items = new Item[packet.Items.Count]; - for (var i = 0; i < packet.Items.Count; ++i) - { - items[i] = GetPlayer().GetItemByGuid(packet.Items[i].Guid); - if (!items[i]) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.ItemNotFound); - return; - } - - if (Global.AuctionMgr.GetAItem(items[i].GetGUID().GetCounter()) || !items[i].CanBeTraded() || items[i].IsNotEmptyBag() || - items[i].GetTemplate().GetFlags().HasAnyFlag(ItemFlags.Conjured) || items[i].m_itemData.Expiration != 0 || - items[i].GetCount() < packet.Items[i].UseCount) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - - finalCount += packet.Items[i].UseCount; - } - - if (packet.Items.Empty()) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - - if (finalCount == 0) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - - // check if there are 2 identical guids, in this case user is most likely cheating - for (int i = 0; i < packet.Items.Count; ++i) - { - for (int j = i + 1; j < packet.Items.Count; ++j) - { - if (packet.Items[i].Guid == packet.Items[j].Guid) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - if (items[i].GetEntry() != items[j].GetEntry()) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.ItemNotFound); - return; - } - } - } - - for (var i = 0; i < packet.Items.Count; ++i) - { - if (items[i].GetMaxStackCount() < finalCount) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - } - - Item item = items[0]; - - uint auctionTime = (uint)(packet.RunTime * WorldConfig.GetFloatValue(WorldCfg.RateAuctionTime)); - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); - - ulong deposit = Global.AuctionMgr.GetAuctionDeposit(auctionHouseEntry, packet.RunTime, item, finalCount); - if (!GetPlayer().HasEnoughMoney(deposit)) - { - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.NotEnoughMoney); - return; - } - - AuctionEntry AH = new AuctionEntry(); - SQLTransaction trans; - - if (WorldConfig.GetBoolValue(WorldCfg.AllowTwoSideInteractionAuction)) - AH.auctioneer = 23442; //@TODO - HARDCODED DB GUID, BAD BAD BAD - else - AH.auctioneer = creature.GetSpawnId(); - - // Required stack size of auction matches to current item stack size, just move item to auctionhouse - if (packet.Items.Count == 1 && item.GetCount() == packet.Items[0].UseCount) - { - if (HasPermission(RBACPermissions.LogGmTrade)) - { - Log.outCommand(GetAccountId(), "GM {0} (Account: {1}) create auction: {2} (Entry: {3} Count: {4})", - GetPlayerName(), GetAccountId(), item.GetTemplate().GetName(), item.GetEntry(), item.GetCount()); - } - - AH.Id = Global.ObjectMgr.GenerateAuctionID(); - AH.itemGUIDLow = item.GetGUID().GetCounter(); - AH.itemEntry = item.GetEntry(); - AH.itemCount = item.GetCount(); - AH.owner = GetPlayer().GetGUID().GetCounter(); - AH.startbid = (uint)packet.MinBid; - AH.bidder = 0; - AH.bid = 0; - AH.buyout = (uint)packet.BuyoutPrice; - AH.expire_time = Time.UnixTime + auctionTime; - AH.deposit = deposit; - AH.etime = packet.RunTime; - AH.auctionHouseEntry = auctionHouseEntry; - - Log.outInfo(LogFilter.Network, "CMSG_AUCTION_SELL_ITEM: {0} {1} is selling item {2} {3} to auctioneer {4} with count {5} with initial bid {6} with buyout {7} and with time {8} (in sec) in auctionhouse {9}", - GetPlayer().GetGUID().ToString(), GetPlayer().GetName(), item.GetGUID().ToString(), item.GetTemplate().GetName(), AH.auctioneer, item.GetCount(), packet.MinBid, packet.BuyoutPrice, auctionTime, AH.GetHouseId()); - Global.AuctionMgr.AddAItem(item); - auctionHouse.AddAuction(AH); - - GetPlayer().MoveItemFromInventory(item.GetBagSlot(), item.GetSlot(), true); - - trans = new SQLTransaction(); - item.DeleteFromInventoryDB(trans); - item.SaveToDB(trans); - AH.SaveToDB(trans); - GetPlayer().SaveInventoryAndGoldToDB(trans); - DB.Characters.CommitTransaction(trans); - - SendAuctionCommandResult(AH, AuctionAction.SellItem, AuctionError.Ok); - - GetPlayer().UpdateCriteria(CriteriaTypes.CreateAuction, 1); - } - else // Required stack size of auction does not match to current item stack size, clone item and set correct stack size - { - Item newItem = item.CloneItem(finalCount, GetPlayer()); - if (!newItem) - { - Log.outError(LogFilter.Network, "CMSG_AuctionAction.SellItem: Could not create clone of item {0}", item.GetEntry()); - SendAuctionCommandResult(null, AuctionAction.SellItem, AuctionError.DatabaseError); - return; - } - - if (HasPermission(RBACPermissions.LogGmTrade)) - { - Log.outCommand(GetAccountId(), "GM {0} (Account: {1}) create auction: {2} (Entry: {3} Count: {4})", - GetPlayerName(), GetAccountId(), newItem.GetTemplate().GetName(), newItem.GetEntry(), newItem.GetCount()); - } - - AH.Id = Global.ObjectMgr.GenerateAuctionID(); - AH.itemGUIDLow = newItem.GetGUID().GetCounter(); - AH.itemEntry = newItem.GetEntry(); - AH.itemCount = newItem.GetCount(); - AH.owner = GetPlayer().GetGUID().GetCounter(); - AH.startbid = (uint)packet.MinBid; - AH.bidder = 0; - AH.bid = 0; - AH.buyout = (uint)packet.BuyoutPrice; - AH.expire_time = Time.UnixTime + auctionTime; - AH.deposit = deposit; - AH.etime = packet.RunTime; - AH.auctionHouseEntry = auctionHouseEntry; - - Log.outInfo(LogFilter.Network, "CMSG_AuctionAction.SellItem: {0} {1} is selling {2} {3} to auctioneer {4} with count {5} with initial bid {6} with buyout {7} and with time {8} (in sec) in auctionhouse {9}", - GetPlayer().GetGUID().ToString(), GetPlayer().GetName(), newItem.GetGUID().ToString(), newItem.GetTemplate().GetName(), AH.auctioneer, newItem.GetCount(), packet.MinBid, packet.BuyoutPrice, auctionTime, AH.GetHouseId()); - Global.AuctionMgr.AddAItem(newItem); - auctionHouse.AddAuction(AH); - - for (var i = 0; i < packet.Items.Count; ++i) - { - Item item2 = items[i]; - - // Item stack count equals required count, ready to delete item - cloned item will be used for auction - if (item2.GetCount() == packet.Items[i].UseCount) - { - GetPlayer().MoveItemFromInventory(item2.GetBagSlot(), item2.GetSlot(), true); - - trans = new SQLTransaction(); - item2.DeleteFromInventoryDB(trans); - item2.DeleteFromDB(trans); - DB.Characters.CommitTransaction(trans); - } - else // Item stack count is bigger than required count, update item stack count and save to database - cloned item will be used for auction - { - item2.SetCount(item2.GetCount() - packet.Items[i].UseCount); - item2.SetState(ItemUpdateState.Changed, GetPlayer()); - GetPlayer().ItemRemovedQuestCheck(item2.GetEntry(), packet.Items[i].UseCount); - item2.SendUpdateToPlayer(GetPlayer()); - - trans = new SQLTransaction(); - item2.SaveToDB(trans); - DB.Characters.CommitTransaction(trans); - } - } - - trans = new SQLTransaction(); - newItem.SaveToDB(trans); - AH.SaveToDB(trans); - GetPlayer().SaveInventoryAndGoldToDB(trans); - DB.Characters.CommitTransaction(trans); - - SendAuctionCommandResult(AH, AuctionAction.SellItem, AuctionError.Ok); - - GetPlayer().UpdateCriteria(CriteriaTypes.CreateAuction, 1); - } - - GetPlayer().ModifyMoney(-(long)deposit); - } - - [WorldPacketHandler(ClientOpcodes.AuctionPlaceBid)] - void HandleAuctionPlaceBid(AuctionPlaceBid packet) - { - if (packet.AuctionID == 0 || packet.BidAmount == 0) - return; // check for cheaters - - Creature creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionPlaceBid - {0} not found or you can't interact with him.", packet.Auctioneer.ToString()); - return; - } - - // remove fake death - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); - - AuctionEntry auction = auctionHouse.GetAuction(packet.AuctionID); - Player player = GetPlayer(); - - if (auction == null || auction.owner == player.GetGUID().GetCounter()) - { - //you cannot bid your own auction: - SendAuctionCommandResult(null, AuctionAction.PlaceBid, AuctionError.BidOwn); - return; - } - - // impossible have online own another character (use this for speedup check in case online owner) - ObjectGuid ownerGuid = ObjectGuid.Create(HighGuid.Player, auction.owner); - Player auction_owner = Global.ObjAccessor.FindPlayer(ownerGuid); - if (!auction_owner && Global.CharacterCacheStorage.GetCharacterAccountIdByGuid(ownerGuid) == player.GetSession().GetAccountId()) - { - //you cannot bid your another character auction: - SendAuctionCommandResult(null, AuctionAction.PlaceBid, AuctionError.BidOwn); - return; - } - - // cheating - if (packet.BidAmount <= auction.bid || packet.BidAmount < auction.startbid) - return; - - // price too low for next bid if not buyout - if ((packet.BidAmount < auction.buyout || auction.buyout == 0) && packet.BidAmount < auction.bid + auction.GetAuctionOutBid()) - { - // client already test it but just in case ... - SendAuctionCommandResult(auction, AuctionAction.PlaceBid, AuctionError.HigherBid); - return; - } - - if (!player.HasEnoughMoney(packet.BidAmount)) - { - // client already test it but just in case ... - SendAuctionCommandResult(auction, AuctionAction.PlaceBid, AuctionError.NotEnoughMoney); - return; - } - - SQLTransaction trans = new SQLTransaction(); - if (packet.BidAmount < auction.buyout || auction.buyout == 0) - { - if (auction.bidder > 0) - { - if (auction.bidder == player.GetGUID().GetCounter()) - player.ModifyMoney(-(long)(packet.BidAmount - auction.bid)); - else - { - // mail to last bidder and return money - Global.AuctionMgr.SendAuctionOutbiddedMail(auction, packet.BidAmount, GetPlayer(), trans); - player.ModifyMoney(-(long)packet.BidAmount); - } - } - else - player.ModifyMoney(-(long)packet.BidAmount); - - auction.bidder = player.GetGUID().GetCounter(); - auction.bid = (uint)packet.BidAmount; - GetPlayer().UpdateCriteria(CriteriaTypes.HighestAuctionBid, packet.BidAmount); - - PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_AUCTION_BID); - stmt.AddValue(0, auction.bidder); - stmt.AddValue(1, auction.bid); - stmt.AddValue(2, auction.Id); - trans.Append(stmt); - - SendAuctionCommandResult(auction, AuctionAction.PlaceBid, AuctionError.Ok); - - // Not sure if we must send this now. - Player owner = Global.ObjAccessor.FindConnectedPlayer(ObjectGuid.Create(HighGuid.Player, auction.owner)); - Item item = Global.AuctionMgr.GetAItem(auction.itemGUIDLow); - if (owner && item) - owner.GetSession().SendAuctionOwnerBidNotification(auction, item); - } - else - { - //buyout: - if (player.GetGUID().GetCounter() == auction.bidder) - player.ModifyMoney(-(long)(auction.buyout - auction.bid)); - else - { - player.ModifyMoney(-(long)auction.buyout); - if (auction.bidder != 0) //buyout for bidded auction .. - Global.AuctionMgr.SendAuctionOutbiddedMail(auction, auction.buyout, GetPlayer(), trans); - } - auction.bidder = player.GetGUID().GetCounter(); - auction.bid = auction.buyout; - GetPlayer().UpdateCriteria(CriteriaTypes.HighestAuctionBid, auction.buyout); - - SendAuctionCommandResult(auction, AuctionAction.PlaceBid, AuctionError.Ok); - - //- Mails must be under transaction control too to prevent data loss - Global.AuctionMgr.SendAuctionSalePendingMail(auction, trans); - Global.AuctionMgr.SendAuctionSuccessfulMail(auction, trans); - Global.AuctionMgr.SendAuctionWonMail(auction, trans); - - auction.DeleteFromDB(trans); - - Global.AuctionMgr.RemoveAItem(auction.itemGUIDLow); - auctionHouse.RemoveAuction(auction); - } - - player.SaveInventoryAndGoldToDB(trans); - DB.Characters.CommitTransaction(trans); - } - - [WorldPacketHandler(ClientOpcodes.AuctionRemoveItem)] - void HandleAuctionRemoveItem(AuctionRemoveItem packet) - { - Creature creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionRemoveItem - {0} not found or you can't interact with him.", packet.Auctioneer.ToString()); - return; - } - - // remove fake death - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); - - AuctionEntry auction = auctionHouse.GetAuction((uint)packet.AuctionID); - Player player = GetPlayer(); - - SQLTransaction trans = new SQLTransaction(); - if (auction != null && auction.owner == player.GetGUID().GetCounter()) - { - Item pItem = Global.AuctionMgr.GetAItem(auction.itemGUIDLow); - if (pItem) - { - if (auction.bidder > 0) // If we have a bidder, we have to send him the money he paid - { - ulong auctionCut = auction.GetAuctionCut(); - if (!player.HasEnoughMoney(auctionCut)) //player doesn't have enough money, maybe message needed - return; - Global.AuctionMgr.SendAuctionCancelledToBidderMail(auction, trans); - player.ModifyMoney(-(long)auctionCut); - } - - // item will deleted or added to received mail list - new MailDraft(auction.BuildAuctionMailSubject(MailAuctionAnswers.Canceled), AuctionEntry.BuildAuctionMailBody(0, 0, auction.buyout, auction.deposit, 0)) - .AddItem(pItem) - .SendMailTo(trans, new MailReceiver(player), new MailSender(auction), MailCheckMask.Copied); - } - else - { - Log.outError(LogFilter.Network, "Auction id: {0} got non existing item (item guid : {1})!", auction.Id, auction.itemGUIDLow); - SendAuctionCommandResult(null, AuctionAction.Cancel, AuctionError.DatabaseError); - return; - } - } - else - { - SendAuctionCommandResult(null, AuctionAction.Cancel, AuctionError.DatabaseError); - //this code isn't possible ... maybe there should be assert - Log.outError(LogFilter.Network, "CHEATER: {0} tried to cancel auction (id: {1}) of another player or auction is null", player.GetGUID().ToString(), packet.AuctionID); - return; - } - - //inform player, that auction is removed - SendAuctionCommandResult(auction, AuctionAction.Cancel, AuctionError.Ok); - - // Now remove the auction - player.SaveInventoryAndGoldToDB(trans); - auction.DeleteFromDB(trans); - DB.Characters.CommitTransaction(trans); - - Global.AuctionMgr.RemoveAItem(auction.itemGUIDLow); - auctionHouse.RemoveAuction(auction); - } - - [WorldPacketHandler(ClientOpcodes.AuctionListBidderItems)] - void HandleAuctionListBidderItems(AuctionListBidderItems packet) - { - Creature creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionListBidderItems - {0} not found or you can't interact with him.", packet.Auctioneer.ToString()); - return; - } - - // remove fake death - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); - - AuctionListBidderItemsResult result = new AuctionListBidderItemsResult(); - - Player player = GetPlayer(); - auctionHouse.BuildListBidderItems(result, player); - result.DesiredDelay = 300; - SendPacket(result); - } - - [WorldPacketHandler(ClientOpcodes.AuctionListOwnerItems)] - void HandleAuctionListOwnerItems(AuctionListOwnerItems packet) - { - Creature creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) - { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionListOwnerItems - {0} not found or you can't interact with him.", packet.Auctioneer.ToString()); - return; - } - - // remove fake death - if (GetPlayer().HasUnitState(UnitState.Died)) - GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); - - AuctionListOwnerItemsResult result = new AuctionListOwnerItemsResult(); - - auctionHouse.BuildListOwnerItems(result, GetPlayer()); - result.DesiredDelay = 300; - SendPacket(result); - } - [WorldPacketHandler(ClientOpcodes.AuctionBrowseQuery)] - void HandleAuctionListItems(AuctionBrowseQuery browseQuery) + void HandleAuctionBrowseQuery(AuctionBrowseQuery browseQuery) { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + Creature creature = GetPlayer().GetNPCIfCanInteractWith(browseQuery.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); - if (!creature) + if (creature == null) { - Log.outDebug(LogFilter.Network, "WORLD: HandleAuctionListItems - {0} not found or you can't interact with him.", browseQuery.Auctioneer.ToString()); + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItems - {browseQuery.Auctioneer.ToString()} not found or you can't interact with him."); return; } @@ -604,11 +49,13 @@ namespace Game if (GetPlayer().HasUnitState(UnitState.Died)) GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - AuctionHouseObject auctionHouse = Global.AuctionMgr.GetAuctionsMap(creature.GetFaction()); + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + Log.outDebug(LogFilter.Auctionhouse, $"Auctionhouse search ({browseQuery.Auctioneer.ToString()}), searchedname: {browseQuery.Name}, levelmin: {browseQuery.MinLevel}, levelmax: {browseQuery.MaxLevel}, filters: {browseQuery.Filters}"); Optional classFilters = new Optional(); - AuctionListItemsResult result = new AuctionListItemsResult(); + AuctionListBucketsResult listBucketsResult = new AuctionListBucketsResult(); if (!browseQuery.ItemClassFilters.Empty()) { classFilters.HasValue = true; @@ -619,9 +66,12 @@ namespace Game { foreach (var subClassFilter in classFilter.SubClassFilters) { - classFilters.Value.Classes[classFilter.ItemClass].SubclassMask |= (AuctionSearchClassFilters.FilterType)(1 << subClassFilter.ItemSubclass); - if (subClassFilter.ItemSubclass < ItemConst.MaxItemSubclassTotal) - classFilters.Value.Classes[classFilter.ItemClass].InvTypes[subClassFilter.ItemSubclass] = subClassFilter.InvTypeMask; + if (classFilter.ItemClass < (int)ItemClass.Max) + { + classFilters.Value.Classes[classFilter.ItemClass].SubclassMask |= (AuctionSearchClassFilters.FilterType)(1 << subClassFilter.ItemSubclass); + if (subClassFilter.ItemSubclass < ItemConst.MaxItemSubclassTotal) + classFilters.Value.Classes[classFilter.ItemClass].InvTypes[subClassFilter.ItemSubclass] = subClassFilter.InvTypeMask; + } } } else @@ -629,40 +79,932 @@ namespace Game } } - auctionHouse.BuildListAuctionItems(result, GetPlayer(), browseQuery.Name.ToLower(), browseQuery.Offset, browseQuery.MinLevel, browseQuery.MaxLevel, browseQuery.Filters, classFilters); + auctionHouse.BuildListBuckets(listBucketsResult, _player, browseQuery.Name, browseQuery.MinLevel, browseQuery.MaxLevel, browseQuery.Filters, classFilters, + browseQuery.KnownPets, browseQuery.KnownPets.Count, (byte)browseQuery.MaxPetLevel, browseQuery.Offset, browseQuery.Sorts, browseQuery.Sorts.Count); - result.DesiredDelay = WorldConfig.GetUIntValue(WorldCfg.AuctionSearchDelay); - SendPacket(result); + listBucketsResult.BrowseMode = AuctionHouseBrowseMode.Search; + listBucketsResult.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + SendPacket(listBucketsResult); } - [WorldPacketHandler(ClientOpcodes.AuctionReplicateItems)] - void HandleReplicateItems(AuctionReplicateItems packet) + [WorldPacketHandler(ClientOpcodes.AuctionCancelCommoditiesPurchase)] + void HandleAuctionCancelCommoditiesPurchase(AuctionCancelCommoditiesPurchase cancelCommoditiesPurchase) { - /* Creature* creature = GetPlayer().GetNPCIfCanInteractWith(packet.Auctioneer, UNIT_NPC_FLAG_AUCTIONEER); - if (!creature) + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.PlaceBid); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(cancelCommoditiesPurchase.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) { - TC_LOG_DEBUG("network", "WORLD: HandleReplicateItems - {0} not found or you can't interact with him.", packet.Auctioneer.ToString().c_str()); + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItems - {cancelCommoditiesPurchase.Auctioneer.ToString()} not found or you can't interact with him."); return; } // remove fake death - if (GetPlayer().HasUnitState(UNIT_STATE_DIED)) - GetPlayer().RemoveAurasByType(SPELL_AURA_FEIGN_DEATH); + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); - AuctionHouseObject* auctionHouse = sAuctionMgr.GetAuctionsMap(creature.getFaction()); + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + auctionHouse.CancelCommodityQuote(_player.GetGUID()); + } - WorldPackets::AuctionHouse::AuctionReplicateResponse response; + [WorldPacketHandler(ClientOpcodes.AuctionConfirmCommoditiesPurchase)] + void HandleAuctionConfirmCommoditiesPurchase(AuctionConfirmCommoditiesPurchase confirmCommoditiesPurchase) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.PlaceBid); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(confirmCommoditiesPurchase.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItems - {confirmCommoditiesPurchase.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + SQLTransaction trans = new SQLTransaction(); + if (auctionHouse.BuyCommodity(trans, _player, (uint)confirmCommoditiesPurchase.ItemID, confirmCommoditiesPurchase.Quantity, throttle.DelayUntilNext)) + { + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + if (GetPlayer() && GetPlayer().GetGUID() == _player.GetGUID()) + { + if (success) + { + GetPlayer().UpdateCriteria(CriteriaTypes.WonAuctions, 1); + SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.Ok, throttle.DelayUntilNext); + } + else + SendAuctionCommandResult(0, AuctionCommand.PlaceBid, AuctionResult.CommodityPurchaseFailed, throttle.DelayUntilNext); + } + }); + } + } + + [WorldPacketHandler(ClientOpcodes.AuctionHelloRequest)] + void HandleAuctionHello(AuctionHelloRequest hello) + { + Creature unit = GetPlayer().GetNPCIfCanInteractWith(hello.Guid, NPCFlags.Auctioneer, NPCFlags2.None); + if (!unit) + { + Log.outDebug(LogFilter.Network, $"WORLD: HandleAuctionHelloOpcode - {hello.Guid.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + SendAuctionHello(hello.Guid, unit); + } + + [WorldPacketHandler(ClientOpcodes.AuctionListBidderItems)] + void HandleAuctionListBidderItems(AuctionListBidderItems listBidderItems) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(listBidderItems.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outDebug(LogFilter.Network, $"WORLD: HandleAuctionListBidderItems - {listBidderItems.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionListBidderItemsResult result = new AuctionListBidderItemsResult(); + + Player player = GetPlayer(); + auctionHouse.BuildListBidderItems(result, player, listBidderItems.Offset, listBidderItems.Sorts, listBidderItems.Sorts.Count); + result.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + SendPacket(result); + } + + [WorldPacketHandler(ClientOpcodes.AuctionListBucketsByBucketKeys)] + void HandleAuctionListBucketsByBucketKeys(AuctionListBucketsByBucketKeys listBucketsByBucketKeys) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(listBucketsByBucketKeys.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItems - {listBucketsByBucketKeys.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionListBucketsResult listBucketsResult = new AuctionListBucketsResult(); + + auctionHouse.BuildListBuckets(listBucketsResult, _player, + listBucketsByBucketKeys.BucketKeys, listBucketsByBucketKeys.BucketKeys.Count, + listBucketsByBucketKeys.Sorts, listBucketsByBucketKeys.Sorts.Count); + + listBucketsResult.BrowseMode = AuctionHouseBrowseMode.SpecificKeys; + listBucketsResult.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + SendPacket(listBucketsResult); + } + + [WorldPacketHandler(ClientOpcodes.AuctionListItemsByBucketKey)] + void HandleAuctionListItemsByBucketKey(AuctionListItemsByBucketKey listItemsByBucketKey) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(listItemsByBucketKey.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItemsByBucketKey - {listItemsByBucketKey.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionListItemsResult listItemsResult = new AuctionListItemsResult(); + listItemsResult.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + listItemsResult.BucketKey = listItemsByBucketKey.BucketKey; + ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(listItemsByBucketKey.BucketKey.ItemID); + listItemsResult.ListType = itemTemplate != null && itemTemplate.GetMaxStackSize() > 1 ? AuctionHouseListType.Commodities : AuctionHouseListType.Items; + + auctionHouse.BuildListAuctionItems(listItemsResult, _player, new AuctionsBucketKey(listItemsByBucketKey.BucketKey), listItemsByBucketKey.Offset, + listItemsByBucketKey.Sorts, listItemsByBucketKey.Sorts.Count); + + SendPacket(listItemsResult); + } + + [WorldPacketHandler(ClientOpcodes.AuctionListItemsByItemId)] + void HandleAuctionListItemsByItemID(AuctionListItemsByItemID listItemsByItemID) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(listItemsByItemID.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItemsByItemID - {listItemsByItemID.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionListItemsResult listItemsResult = new AuctionListItemsResult(); + listItemsResult.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + listItemsResult.BucketKey.ItemID = listItemsByItemID.ItemID; + ItemTemplate itemTemplate = Global.ObjectMgr.GetItemTemplate(listItemsByItemID.ItemID); + listItemsResult.ListType = itemTemplate != null && itemTemplate.GetMaxStackSize() > 1 ? AuctionHouseListType.Commodities : AuctionHouseListType.Items; + + auctionHouse.BuildListAuctionItems(listItemsResult, _player, listItemsByItemID.ItemID, listItemsByItemID.Offset, + listItemsByItemID.Sorts, listItemsByItemID.Sorts.Count); + + SendPacket(listItemsResult); + } + + [WorldPacketHandler(ClientOpcodes.AuctionListOwnerItems)] + void HandleAuctionListOwnerItems(AuctionListOwnerItems listOwnerItems) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(listOwnerItems.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outDebug(LogFilter.Network, $"WORLD: HandleAuctionListOwnerItems - {listOwnerItems.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionListOwnerItemsResult result = new AuctionListOwnerItemsResult(); + + auctionHouse.BuildListOwnerItems(result, _player, listOwnerItems.Offset, listOwnerItems.Sorts, listOwnerItems.Sorts.Count); + result.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + SendPacket(result); + } + + [WorldPacketHandler(ClientOpcodes.AuctionPlaceBid)] + void HandleAuctionPlaceBid(AuctionPlaceBid placeBid) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.PlaceBid); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(placeBid.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outDebug(LogFilter.Network, $"WORLD: HandleAuctionPlaceBid - {placeBid.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // auction house does not deal with copper + if ((placeBid.BidAmount % MoneyConstants.Silver) != 0) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.BidIncrement, throttle.DelayUntilNext); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionPosting auction = auctionHouse.GetAuction(placeBid.AuctionID); + if (auction == null || auction.IsCommodity()) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + Player player = GetPlayer(); + + // check auction owner - cannot buy own auctions + if (auction.Owner == player.GetGUID() || auction.OwnerAccount == GetAccountGUID()) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.BidOwn, throttle.DelayUntilNext); + return; + } + + bool canBid = auction.MinBid != 0; + bool canBuyout = auction.BuyoutOrUnitPrice != 0; + + // buyout attempt with wrong amount + if (!canBid && placeBid.BidAmount != auction.BuyoutOrUnitPrice) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.BidIncrement, throttle.DelayUntilNext); + return; + } + + ulong minBid = auction.BidAmount != 0 ? auction.BidAmount + auction.CalculateMinIncrement() : auction.MinBid; + if (canBid && placeBid.BidAmount < minBid) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.HigherBid, throttle.DelayUntilNext); + return; + } + + SQLTransaction trans = new SQLTransaction(); + ulong priceToPay = placeBid.BidAmount; + if (!auction.Bidder.IsEmpty()) + { + // return money to previous bidder + if (auction.Bidder != player.GetGUID()) + auctionHouse.SendAuctionOutbid(auction, player.GetGUID(), placeBid.BidAmount, trans); + else + priceToPay = placeBid.BidAmount - auction.BidAmount; + } + + // check money + if (!player.HasEnoughMoney(priceToPay)) + { + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + player.ModifyMoney(-(long)priceToPay); + auction.Bidder = player.GetGUID(); + auction.BidAmount = placeBid.BidAmount; + + if (canBuyout && placeBid.BidAmount == auction.BuyoutOrUnitPrice) + { + // buyout + auctionHouse.SendAuctionWon(auction, player, trans); + auctionHouse.SendAuctionSold(auction, null, trans); + + auctionHouse.RemoveAuction(trans, auction); + } + else + { + // place bid + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.UPD_AUCTION_BID); + stmt.AddValue(0, auction.Bidder.GetCounter()); + stmt.AddValue(1, auction.BidAmount); + stmt.AddValue(2, auction.Id); + trans.Append(stmt); + + auction.BidderHistory.Add(player.GetGUID()); + if (auction.BidderHistory.Contains(player.GetGUID())) + { + stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_AUCTION_BIDDER); + stmt.AddValue(0, auction.Id); + stmt.AddValue(1, player.GetGUID().GetCounter()); + trans.Append(stmt); + } + + // Not sure if we must send this now. + Player owner = Global.ObjAccessor.FindConnectedPlayer(auction.Owner); + if (owner != null) + owner.GetSession().SendAuctionOwnerBidNotification(auction); + } + + player.SaveInventoryAndGoldToDB(trans); + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + if (GetPlayer() && GetPlayer().GetGUID() == _player.GetGUID()) + { + if (success) + { + GetPlayer().UpdateCriteria(CriteriaTypes.HighestAuctionBid, placeBid.BidAmount); + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.Ok, throttle.DelayUntilNext); + } + else + SendAuctionCommandResult(placeBid.AuctionID, AuctionCommand.PlaceBid, AuctionResult.DatabaseError, throttle.DelayUntilNext); + } + }); + } + + [WorldPacketHandler(ClientOpcodes.AuctionRemoveItem)] + void HandleAuctionRemoveItem(AuctionRemoveItem removeItem) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.Cancel); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(removeItem.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outDebug(LogFilter.Network, $"WORLD: HandleAuctionRemoveItem - {removeItem.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionPosting auction = auctionHouse.GetAuction(removeItem.AuctionID); + Player player = GetPlayer(); + + SQLTransaction trans = new SQLTransaction(); + if (auction != null && auction.Owner == player.GetGUID()) + { + if (auction.Bidder.IsEmpty()) // If we have a bidder, we have to send him the money he paid + { + ulong cancelCost = MathFunctions.CalculatePct(auction.BidAmount, 5u); + if (!player.HasEnoughMoney(cancelCost)) //player doesn't have enough money + { + SendAuctionCommandResult(0, AuctionCommand.Cancel, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + auctionHouse.SendAuctionCancelledToBidder(auction, trans); + player.ModifyMoney(-(long)cancelCost); + } + + auctionHouse.SendAuctionRemoved(auction, player, trans); + } + else + { + SendAuctionCommandResult(0, AuctionCommand.Cancel, AuctionResult.DatabaseError, throttle.DelayUntilNext); + //this code isn't possible ... maybe there should be assert + Log.outError(LogFilter.Network, $"CHEATER: {player.GetGUID().ToString()} tried to cancel auction (id: {removeItem.AuctionID}) of another player or auction is null"); + return; + } + + // client bug - instead of removing auction in the UI, it only substracts 1 from visible count + uint auctionIdForClient = auction.IsCommodity() ? 0 : auction.Id; + + // Now remove the auction + player.SaveInventoryAndGoldToDB(trans); + auctionHouse.RemoveAuction(trans, auction); + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + if (GetPlayer() && GetPlayer().GetGUID() == _player.GetGUID()) + { + if (success) + SendAuctionCommandResult(auctionIdForClient, AuctionCommand.Cancel, AuctionResult.Ok, throttle.DelayUntilNext); //inform player, that auction is removed + else + SendAuctionCommandResult(0, AuctionCommand.Cancel, AuctionResult.DatabaseError, throttle.DelayUntilNext); + } + }); + } + + [WorldPacketHandler(ClientOpcodes.AuctionReplicateItems)] + void HandleReplicateItems(AuctionReplicateItems replicateItems) + { + Creature creature = GetPlayer().GetNPCIfCanInteractWith(replicateItems.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outError(LogFilter.Network, $"WORLD: HandleReplicateItems - {replicateItems.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); - auctionHouse.BuildReplicate(response, GetPlayer(), packet.ChangeNumberGlobal, packet.ChangeNumberCursor, packet.ChangeNumberTombstone, packet.Count); - */ - //@todo implement this properly AuctionReplicateResponse response = new AuctionReplicateResponse(); - response.ChangeNumberCursor = packet.ChangeNumberCursor; - response.ChangeNumberGlobal = packet.ChangeNumberGlobal; - response.ChangeNumberTombstone = packet.ChangeNumberTombstone; + + auctionHouse.BuildReplicate(response, GetPlayer(), replicateItems.ChangeNumberGlobal, replicateItems.ChangeNumberCursor, replicateItems.ChangeNumberTombstone, replicateItems.Count); + response.DesiredDelay = WorldConfig.GetUIntValue(WorldCfg.AuctionSearchDelay) * 5; response.Result = 0; + SendPacket(response); } + + [WorldPacketHandler(ClientOpcodes.AuctionSellCommodity)] + void HandleAuctionSellCommodity(AuctionSellCommodity sellCommodity) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.SellItem); + if (throttle.Throttled) + return; + + if (sellCommodity.UnitPrice == 0 || sellCommodity.UnitPrice > PlayerConst.MaxMoneyAmount) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionSellItem - Player {_player.GetName()} ({_player.GetGUID().ToString()}) attempted to sell item with invalid price."); + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + // auction house does not deal with copper + if ((sellCommodity.UnitPrice % MoneyConstants.Silver) != 0) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(sellCommodity.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (creature == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionListItems - {sellCommodity.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + uint houseId = 0; + AuctionHouseRecord auctionHouseEntry = Global.AuctionHouseMgr.GetAuctionHouseEntry(creature.GetFaction(), ref houseId); + if (auctionHouseEntry == null) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionSellItem - Unit ({sellCommodity.Auctioneer.ToString()}) has wrong faction."); + return; + } + + switch (sellCommodity.RunTime) + { + case 1 * SharedConst.MinAuctionTime / Time.Minute: + case 2 * SharedConst.MinAuctionTime / Time.Minute: + case 4 * SharedConst.MinAuctionTime / Time.Minute: + break; + default: + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.AuctionHouseBusy, throttle.DelayUntilNext); + return; + } + + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + // find all items for sale + ulong totalCount = 0; + Dictionary items2 = new Dictionary(); + + foreach (var itemForSale in sellCommodity.Items) + { + Item item = _player.GetItemByGuid(itemForSale.Guid); + if (!item) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + if (item.GetTemplate().GetMaxStackSize() == 1) + { + // not commodity, must use different packet + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + // verify that all items belong to the same bucket + if (!items2.Empty() && AuctionsBucketKey.ForItem(item) != AuctionsBucketKey.ForItem(items2.FirstOrDefault().Value.Item1)) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + if (Global.AuctionHouseMgr.GetAItem(item.GetGUID()) || !item.CanBeTraded() || item.IsNotEmptyBag() || + item.GetTemplate().GetFlags().HasAnyFlag(ItemFlags.Conjured) || item.m_itemData.Expiration != 0 || + item.GetCount() < itemForSale.UseCount) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + var soldItem = items2.LookupByKey(item.GetGUID()); + soldItem.Item = item; + soldItem.UseCount += itemForSale.UseCount; + items2[item.GetGUID()] = soldItem; + if (item.GetCount() < soldItem.UseCount) + { + // check that we have enough of this item to sell + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + totalCount += itemForSale.UseCount; + } + + if (totalCount == 0) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + TimeSpan auctionTime = TimeSpan.FromSeconds((long)TimeSpan.FromMinutes(sellCommodity.RunTime).TotalSeconds * WorldConfig.GetFloatValue(WorldCfg.RateAuctionTime)); + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + ulong deposit = Global.AuctionHouseMgr.GetCommodityAuctionDeposit(items2.FirstOrDefault().Value.Item.GetTemplate(), TimeSpan.FromMinutes(sellCommodity.RunTime), (uint)totalCount); + if (!_player.HasEnoughMoney(deposit)) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + uint auctionId = Global.ObjectMgr.GenerateAuctionID(); + AuctionPosting auction = new AuctionPosting(); + auction.Id = auctionId; + auction.Owner = _player.GetGUID(); + auction.OwnerAccount = GetAccountGUID(); + auction.BuyoutOrUnitPrice = sellCommodity.UnitPrice; + auction.Deposit = deposit; + auction.StartTime = GameTime.GetGameTimeSystemPoint(); + auction.EndTime = auction.StartTime + auctionTime; + + // keep track of what was cloned to undo/modify counts later + Dictionary clones = new Dictionary(); + foreach (var pair in items2) + { + Item itemForSale; + if (pair.Value.Item1.GetCount() != pair.Value.Item2) + { + itemForSale = pair.Value.Item1.CloneItem((uint)pair.Value.Item2, _player); + if (itemForSale == null) + { + Log.outError(LogFilter.Network, $"CMSG_AUCTION_SELL_COMMODITY: Could not create clone of item {pair.Value.Item1.GetEntry()}"); + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + clones.Add(pair.Value.Item1, itemForSale); + } + } + + if (!Global.AuctionHouseMgr.PendingAuctionAdd(_player, auctionHouse.GetAuctionHouseId(), auction.Id, auction.Deposit)) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + /*TC_LOG_INFO("network", "CMSG_AUCTION_SELL_COMMODITY: %s %s is selling item %s %s to auctioneer %s with count " UI64FMTD " with with unit price " UI64FMTD " and with time %u (in sec) in auctionhouse %u", + _player.GetGUID().ToString(), _player.GetName(), items2.begin().second.first.GetNameForLocaleIdx(sWorld.GetDefaultDbcLocale()), + ([&items2]() + { + std.stringstream ss; + auto itr = items2.begin(); + ss << (itr++).first.ToString(); + for (; itr != items2.end(); ++itr) + ss << ',' << itr.first.ToString(); + return ss.str(); + } ()), + creature.GetGUID().ToString(), totalCount, sellCommodity.UnitPrice, uint32(auctionTime.count()), auctionHouse.GetAuctionHouseId());*/ + + if (HasPermission(RBACPermissions.LogGmTrade)) + { + Item logItem = items2.First().Value.Item1; + Log.outCommand(GetAccountId(), $"GM {GetPlayerName()} (Account: {GetAccountId()}) create auction: {logItem.GetName(Global.WorldMgr.GetDefaultDbcLocale())} (Entry: {logItem.GetEntry()} Count: {totalCount})"); + } + + SQLTransaction trans = new SQLTransaction(); + + foreach (var pair in items2) + { + Item itemForSale = pair.Value.Item1; + var cloneItr = clones.LookupByKey(pair.Value.Item1); + if (cloneItr != null) + { + Item original = itemForSale; + original.SetCount(original.GetCount() - (uint)pair.Value.Item2); + original.SetState(ItemUpdateState.Changed, _player); + _player.ItemRemovedQuestCheck(original.GetEntry(), (uint)pair.Value.Item2); + original.SaveToDB(trans); + + itemForSale = cloneItr; + } + else + { + _player.MoveItemFromInventory(itemForSale.GetBagSlot(), itemForSale.GetSlot(), true); + itemForSale.DeleteFromInventoryDB(trans); + } + + itemForSale.SaveToDB(trans); + auction.Items.Add(itemForSale); + } + + auctionHouse.AddAuction(trans, auction); + _player.SaveInventoryAndGoldToDB(trans); + + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + if (GetPlayer() && GetPlayer().GetGUID() == _player.GetGUID()) + { + if (success) + { + GetPlayer().UpdateCriteria(CriteriaTypes.CreateAuction, 1); + SendAuctionCommandResult(auctionId, AuctionCommand.SellItem, AuctionResult.Ok, throttle.DelayUntilNext); + } + else + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + } + }); + } + + [WorldPacketHandler(ClientOpcodes.AuctionSellItem)] + void HandleAuctionSellItem(AuctionSellItem sellItem) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.SellItem); + if (throttle.Throttled) + return; + + if (sellItem.Items.Count != 1 || sellItem.Items[0].UseCount != 1) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + if (sellItem.MinBid == 0 && sellItem.BuyoutPrice == 0) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + if (sellItem.MinBid > PlayerConst.MaxMoneyAmount || sellItem.BuyoutPrice > PlayerConst.MaxMoneyAmount) + { + Log.outError(LogFilter.Network, $"WORLD: HandleAuctionSellItem - Player {_player.GetName()} ({_player.GetGUID().ToString()}) attempted to sell item with higher price than max gold amount."); + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.Inventory, throttle.DelayUntilNext, InventoryResult.TooMuchGold); + return; + } + + // auction house does not deal with copper + if ((sellItem.MinBid % MoneyConstants.Silver) != 0 || (sellItem.BuyoutPrice % MoneyConstants.Silver) != 0) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(sellItem.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outError(LogFilter.Network, "WORLD: HandleAuctionSellItem - Unit (%s) not found or you can't interact with him.", sellItem.Auctioneer.ToString()); + return; + } + + uint houseId = 0; + AuctionHouseRecord auctionHouseEntry = Global.AuctionHouseMgr.GetAuctionHouseEntry(creature.GetFaction(), ref houseId); + if (auctionHouseEntry == null) + { + Log.outError(LogFilter.Network, "WORLD: HandleAuctionSellItem - Unit (%s) has wrong faction.", sellItem.Auctioneer.ToString()); + return; + } + + switch (sellItem.RunTime) + { + case 1 * SharedConst.MinAuctionTime / Time.Minute: + case 2 * SharedConst.MinAuctionTime / Time.Minute: + case 4 * SharedConst.MinAuctionTime / Time.Minute: + break; + default: + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.AuctionHouseBusy, throttle.DelayUntilNext); + return; + } + + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + Item item = _player.GetItemByGuid(sellItem.Items[0].Guid); + if (!item) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + if (item.GetTemplate().GetMaxStackSize() > 1) + { + // commodity, must use different packet + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.ItemNotFound, throttle.DelayUntilNext); + return; + } + + if (Global.AuctionHouseMgr.GetAItem(item.GetGUID()) || !item.CanBeTraded() || item.IsNotEmptyBag() || + item.GetTemplate().GetFlags().HasAnyFlag(ItemFlags.Conjured) || item.m_itemData.Expiration != 0 || + item.GetCount() != 1) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + return; + } + + TimeSpan auctionTime = TimeSpan.FromSeconds((long)(TimeSpan.FromMinutes(sellItem.RunTime).TotalSeconds * WorldConfig.GetFloatValue(WorldCfg.RateAuctionTime))); + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + ulong deposit = Global.AuctionHouseMgr.GetItemAuctionDeposit(_player, item, TimeSpan.FromMinutes(sellItem.RunTime)); + if (!_player.HasEnoughMoney(deposit)) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + uint auctionId = Global.ObjectMgr.GenerateAuctionID(); + + AuctionPosting auction = new AuctionPosting(); + auction.Id = auctionId; + auction.Owner = _player.GetGUID(); + auction.OwnerAccount = GetAccountGUID(); + auction.MinBid = sellItem.MinBid; + auction.BuyoutOrUnitPrice = sellItem.BuyoutPrice; + auction.Deposit = deposit; + auction.BidAmount = sellItem.MinBid; + auction.StartTime = GameTime.GetGameTimeSystemPoint(); + auction.EndTime = auction.StartTime + auctionTime; + + if (HasPermission(RBACPermissions.LogGmTrade)) + Log.outCommand(GetAccountId(), $"GM {GetPlayerName()} (Account: {GetAccountId()}) create auction: {item.GetTemplate().GetName()} (Entry: {item.GetEntry()} Count: {item.GetCount()})"); + + auction.Items.Add(item); + + Log.outInfo(LogFilter.Network, $"CMSG_AuctionAction.SellItem: {_player.GetGUID().ToString()} {_player.GetName()} is selling item {item.GetGUID().ToString()} {item.GetTemplate().GetName()} " + + $"to auctioneer {creature.GetGUID().ToString()} with count {item.GetCount()} with initial bid {sellItem.MinBid} with buyout {sellItem.BuyoutPrice} and with time {auctionTime.TotalSeconds} " + + $"(in sec) in auctionhouse {auctionHouse.GetAuctionHouseId()}"); + + // Add to pending auctions, or fail with insufficient funds error + if (!Global.AuctionHouseMgr.PendingAuctionAdd(_player, auctionHouse.GetAuctionHouseId(), auctionId, auction.Deposit)) + { + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.NotEnoughMoney, throttle.DelayUntilNext); + return; + } + + _player.MoveItemFromInventory(item.GetBagSlot(), item.GetSlot(), true); + + SQLTransaction trans = new SQLTransaction(); + item.DeleteFromInventoryDB(trans); + item.SaveToDB(trans); + + auctionHouse.AddAuction(trans, auction); + _player.SaveInventoryAndGoldToDB(trans); + AddTransactionCallback(DB.Characters.AsyncCommitTransaction(trans)).AfterComplete(success => + { + if (GetPlayer() && GetPlayer().GetGUID() == _player.GetGUID()) + { + if (success) + { + GetPlayer().UpdateCriteria(CriteriaTypes.CreateAuction, 1); + SendAuctionCommandResult(auctionId, AuctionCommand.SellItem, AuctionResult.Ok, throttle.DelayUntilNext); + } + else + SendAuctionCommandResult(0, AuctionCommand.SellItem, AuctionResult.DatabaseError, throttle.DelayUntilNext); + } + }); + } + + [WorldPacketHandler(ClientOpcodes.AuctionSetFavoriteItem)] + void HandleAuctionSetFavoriteItem(AuctionSetFavoriteItem setFavoriteItem) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player); + if (throttle.Throttled) + return; + + SQLTransaction trans = new SQLTransaction(); + + PreparedStatement stmt = DB.Characters.GetPreparedStatement(CharStatements.DEL_CHARACTER_FAVORITE_AUCTION); + stmt.AddValue(0, _player.GetGUID().GetCounter()); + stmt.AddValue(1, setFavoriteItem.Item.Order); + trans.Append(stmt); + + if (!setFavoriteItem.IsNotFavorite) + { + stmt = DB.Characters.GetPreparedStatement(CharStatements.INS_CHARACTER_FAVORITE_AUCTION); + stmt.AddValue(0, _player.GetGUID().GetCounter()); + stmt.AddValue(1, setFavoriteItem.Item.Order); + stmt.AddValue(2, setFavoriteItem.Item.ItemID); + stmt.AddValue(3, setFavoriteItem.Item.ItemLevel); + stmt.AddValue(4, setFavoriteItem.Item.BattlePetSpeciesID); + stmt.AddValue(5, setFavoriteItem.Item.SuffixItemNameDescriptionID); + trans.Append(stmt); + } + + DB.Characters.CommitTransaction(trans); + } + + [WorldPacketHandler(ClientOpcodes.AuctionStartCommoditiesPurchase)] + void HandleAuctionStartCommoditiesPurchase(AuctionStartCommoditiesPurchase startCommoditiesPurchase) + { + AuctionThrottleResult throttle = Global.AuctionHouseMgr.CheckThrottle(_player, AuctionCommand.PlaceBid); + if (throttle.Throttled) + return; + + Creature creature = GetPlayer().GetNPCIfCanInteractWith(startCommoditiesPurchase.Auctioneer, NPCFlags.Auctioneer, NPCFlags2.None); + if (!creature) + { + Log.outError(LogFilter.Network, "WORLD: HandleAuctionStartCommoditiesPurchase - {startCommoditiesPurchase.Auctioneer.ToString()} not found or you can't interact with him."); + return; + } + + // remove fake death + if (GetPlayer().HasUnitState(UnitState.Died)) + GetPlayer().RemoveAurasByType(AuraType.FeignDeath); + + AuctionHouseObject auctionHouse = Global.AuctionHouseMgr.GetAuctionsMap(creature.GetFaction()); + + AuctionCommodityQuote auctionCommodityQuote = new AuctionCommodityQuote(); + + CommodityQuote quote = auctionHouse.CreateCommodityQuote(_player, (uint)startCommoditiesPurchase.ItemID, startCommoditiesPurchase.Quantity); + if (quote != null) + { + auctionCommodityQuote.TotalPrice.Set(quote.TotalPrice); + auctionCommodityQuote.Quantity.Set(quote.Quantity); + auctionCommodityQuote.QuoteDuration.Set((int)(quote.ValidTo - GameTime.GetGameTimeSteadyPoint()).TotalMilliseconds); + } + + auctionCommodityQuote.DesiredDelay = (uint)throttle.DelayUntilNext.TotalSeconds; + + SendPacket(auctionCommodityQuote); + } + + public void SendAuctionHello(ObjectGuid guid, Creature unit) + { + if (GetPlayer().GetLevel() < WorldConfig.GetIntValue(WorldCfg.AuctionLevelReq)) + { + SendNotification(Global.ObjectMgr.GetCypherString(CypherStrings.AuctionReq), WorldConfig.GetIntValue(WorldCfg.AuctionLevelReq)); + return; + } + + AuctionHouseRecord ahEntry = Global.AuctionHouseMgr.GetAuctionHouseEntry(unit.GetFaction()); + if (ahEntry == null) + return; + + AuctionHelloResponse auctionHelloResponse = new AuctionHelloResponse(); + auctionHelloResponse.Guid = guid; + auctionHelloResponse.OpenForBusiness = true; + SendPacket(auctionHelloResponse); + } + + public void SendAuctionCommandResult(uint auctionId, AuctionCommand command, AuctionResult errorCode, TimeSpan delayForNextAction, InventoryResult bagError = 0) + { + AuctionCommandResult auctionCommandResult = new AuctionCommandResult(); + auctionCommandResult.AuctionID = auctionId; + auctionCommandResult.Command = (int)command; + auctionCommandResult.ErrorCode = (int)errorCode; + auctionCommandResult.BagResult = (int)bagError; + auctionCommandResult.DesiredDelay = (uint)delayForNextAction.TotalSeconds; + SendPacket(auctionCommandResult); + } + + public void SendAuctionClosedNotification(AuctionPosting auction, float mailDelay, bool sold) + { + AuctionClosedNotification packet = new AuctionClosedNotification(); + packet.Info.Initialize(auction); + packet.ProceedsMailDelay = mailDelay; + packet.Sold = sold; + SendPacket(packet); + } + + public void SendAuctionOwnerBidNotification(AuctionPosting auction) + { + AuctionOwnerBidNotification packet = new AuctionOwnerBidNotification(); + packet.Info.Initialize(auction); + packet.Bidder = auction.Bidder; + packet.MinIncrement = auction.CalculateMinIncrement(); + SendPacket(packet); + } } } diff --git a/Source/Game/Handlers/CharacterHandler.cs b/Source/Game/Handlers/CharacterHandler.cs index 6884d1371..84f74e533 100644 --- a/Source/Game/Handlers/CharacterHandler.cs +++ b/Source/Game/Handlers/CharacterHandler.cs @@ -788,6 +788,29 @@ namespace Game // Place character in world (and load zone) before some object loading pCurrChar.LoadCorpse(holder.GetResult(PlayerLoginQueryLoad.CorpseLocation)); + stmt = DB.Characters.GetPreparedStatement(CharStatements.SEL_CHARACTER_FAVORITE_AUCTIONS); + stmt.AddValue(0, pCurrChar.GetGUID().GetCounter()); + GetQueryProcessor().AddCallback(DB.Characters.AsyncQuery(stmt)).WithCallback(favoriteAuctionResult => + { + AuctionFavoriteItems favoriteItems = new AuctionFavoriteItems(); + if (!favoriteAuctionResult.IsEmpty()) + { + do + { + AuctionFavoriteInfo item = new AuctionFavoriteInfo(); + item.Order = favoriteAuctionResult.Read(0); + item.ItemID = favoriteAuctionResult.Read(1); + item.ItemLevel = favoriteAuctionResult.Read(2); + item.BattlePetSpeciesID = favoriteAuctionResult.Read(3); + item.SuffixItemNameDescriptionID = favoriteAuctionResult.Read(4); + favoriteItems.Items.Add(item); + + } while (favoriteAuctionResult.NextRow()); + + } + SendPacket(favoriteItems); + }); + // setting Ghost+speed if dead if (pCurrChar.GetDeathState() != DeathState.Alive) { diff --git a/Source/Game/Mails/Mail.cs b/Source/Game/Mails/Mail.cs index a30c0ca7c..571222fea 100644 --- a/Source/Game/Mails/Mail.cs +++ b/Source/Game/Mails/Mail.cs @@ -90,7 +90,15 @@ namespace Game.Mails m_receiver = receiver; m_receiver_lowguid = receiver_lowguid; - // ASSERT(!receiver || receiver.GetGUID().GetCounter() == receiver_lowguid); + Cypher.Assert(!receiver || receiver.GetGUID().GetCounter() == receiver_lowguid); + } + + public MailReceiver(Player receiver, ObjectGuid receiverGuid) + { + m_receiver = receiver; + m_receiver_lowguid = receiverGuid.GetCounter(); + + Cypher.Assert(!receiver || receiver.GetGUID() == receiverGuid); } public Player GetPlayer() { return m_receiver; } @@ -140,10 +148,11 @@ namespace Game.Mails m_senderId = (uint)sender.EventId; m_stationery = MailStationery.Default; } - public MailSender(AuctionEntry sender) + + public MailSender(AuctionHouseObject sender) { m_messageType = MailMessageType.Auction; - m_senderId = sender.GetHouseId(); + m_senderId = sender.GetAuctionHouseId(); m_stationery = MailStationery.Auction; } diff --git a/Source/Game/Mails/MailDraft.cs b/Source/Game/Mails/MailDraft.cs index 46bcc5a28..f3f827cc0 100644 --- a/Source/Game/Mails/MailDraft.cs +++ b/Source/Game/Mails/MailDraft.cs @@ -137,7 +137,7 @@ namespace Game.Mails public void SendMailTo(SQLTransaction trans, MailReceiver receiver, MailSender sender, MailCheckMask checkMask = MailCheckMask.None, uint deliver_delay = 0) { Player pReceiver = receiver.GetPlayer(); // can be NULL - Player pSender = Global.ObjAccessor.FindPlayer(ObjectGuid.Create(HighGuid.Player, sender.GetSenderId())); + Player pSender = sender.GetMailMessageType() == MailMessageType.Normal ? Global.ObjAccessor.FindPlayer(ObjectGuid.Create(HighGuid.Player, sender.GetSenderId())) : null; if (pReceiver != null) PrepareItems(pReceiver, trans); // generate mail template items diff --git a/Source/Game/Network/Packets/AuctionHousePackets.cs b/Source/Game/Network/Packets/AuctionHousePackets.cs index 484e3d584..8269ea89d 100644 --- a/Source/Game/Network/Packets/AuctionHousePackets.cs +++ b/Source/Game/Network/Packets/AuctionHousePackets.cs @@ -41,7 +41,7 @@ namespace Game.Network.Packets public override void Read() { Auctioneer = _worldPacket.ReadPackedGuid(); - Offset = _worldPacket.ReadUInt8(); + Offset = _worldPacket.ReadUInt32(); MinLevel = _worldPacket.ReadUInt8(); MaxLevel = _worldPacket.ReadUInt8(); Filters = (AuctionHouseFilterMask)_worldPacket.ReadUInt32(); @@ -88,10 +88,10 @@ namespace Game.Network.Packets class AuctionConfirmCommoditiesPurchase : ClientPacket { - ObjectGuid Auctioneer; - int ItemID; - uint Quantity; - Optional TaintedBy; + public ObjectGuid Auctioneer; + public int ItemID; + public uint Quantity; + public Optional TaintedBy; public AuctionConfirmCommoditiesPurchase(WorldPacket packet) : base(packet) { } @@ -149,6 +149,34 @@ namespace Game.Network.Packets } } + class AuctionListBucketsByBucketKeys : ClientPacket + { + public ObjectGuid Auctioneer; + public Optional TaintedBy; + public Array BucketKeys = new Array(100); + public Array Sorts = new Array(2); + + public AuctionListBucketsByBucketKeys(WorldPacket packet) : base(packet) { } + + public override void Read() + { + Auctioneer = _worldPacket.ReadPackedGuid(); + TaintedBy.HasValue = _worldPacket.HasBit(); + + uint bucketKeysCount = _worldPacket.ReadBits(7); + uint sortCount = _worldPacket.ReadBits(2); + + for (var i = 0; i < sortCount; ++i) + Sorts[i] = new AuctionSortDef(_worldPacket); + + if (TaintedBy.HasValue) + TaintedBy.Value.Read(_worldPacket); + + for (var i = 0; i < bucketKeysCount; ++i) + BucketKeys[i] = new AuctionBucketKey(_worldPacket); + } + } + class AuctionListItemsByBucketKey : ClientPacket { public ObjectGuid Auctioneer; @@ -168,12 +196,10 @@ namespace Game.Network.Packets TaintedBy.HasValue = _worldPacket.HasBit(); uint sortCount = _worldPacket.ReadBits(2); - for (var i = 0; i < sortCount; ++i) Sorts[i] = new AuctionSortDef(_worldPacket); - BucketKey = new AuctionBucketKey(); - BucketKey.Read(_worldPacket); + BucketKey = new AuctionBucketKey(_worldPacket); if (TaintedBy.HasValue) TaintedBy.Value.Read(_worldPacket); @@ -183,7 +209,7 @@ namespace Game.Network.Packets class AuctionListItemsByItemID : ClientPacket { public ObjectGuid Auctioneer; - public int ItemID; + public uint ItemID; public int SuffixItemNameDescriptionID; public uint Offset; public Optional TaintedBy; @@ -194,37 +220,13 @@ namespace Game.Network.Packets public override void Read() { Auctioneer = _worldPacket.ReadPackedGuid(); - ItemID = _worldPacket.ReadInt32(); + ItemID = _worldPacket.ReadUInt32(); SuffixItemNameDescriptionID = _worldPacket.ReadInt32(); Offset = _worldPacket.ReadUInt32(); - TaintedBy.HasValue = _worldPacket.HasBit(); - - uint sortCount = _worldPacket.ReadBits(2); - for (var i = 0; i < sortCount; ++i) - Sorts[i] = new AuctionSortDef(_worldPacket); - - if (TaintedBy.HasValue) - TaintedBy.Value.Read(_worldPacket); - } - } - - class AuctionListItemsByItemKeys : ClientPacket - { - public ObjectGuid Auctioneer; - public Optional TaintedBy; - public Array BucketKeys = new Array(100); - public Array Sorts = new Array(2); - - public AuctionListItemsByItemKeys(WorldPacket packet) : base(packet) { } - - public override void Read() - { - Auctioneer = _worldPacket.ReadPackedGuid(); if (_worldPacket.HasBit()) TaintedBy.HasValue = true; - uint bucketKeyCount = _worldPacket.ReadBits(7); uint sortCount = _worldPacket.ReadBits(2); for (var i = 0; i < sortCount; ++i) @@ -232,12 +234,6 @@ namespace Game.Network.Packets if (TaintedBy.HasValue) TaintedBy.Value.Read(_worldPacket); - - for (var i = 0; i < bucketKeyCount; ++i) - { - BucketKeys[i] = new AuctionBucketKey(); - BucketKeys[i].Read(_worldPacket); - } } } @@ -290,7 +286,7 @@ namespace Game.Network.Packets class AuctionRemoveItem : ClientPacket { public ObjectGuid Auctioneer; - public int AuctionID = 0; + public uint AuctionID; public Optional TaintedBy; public AuctionRemoveItem(WorldPacket packet) : base(packet) { } @@ -298,7 +294,7 @@ namespace Game.Network.Packets public override void Read() { Auctioneer = _worldPacket.ReadPackedGuid(); - AuctionID = _worldPacket.ReadInt32(); + AuctionID = _worldPacket.ReadUInt32(); TaintedBy.HasValue = _worldPacket.HasBit(); if (TaintedBy.HasValue) @@ -364,7 +360,7 @@ namespace Game.Network.Packets public ulong MinBid; public uint RunTime; public Optional TaintedBy; - public Array Items = new Array(64); + public Array Items = new Array(1); public AuctionSellItem(WorldPacket packet) : base(packet) { } @@ -451,24 +447,6 @@ namespace Game.Network.Packets public AuctionCommandResult() : base(ServerOpcodes.AuctionCommandResult) { } - /** - * @fn void WorldPackets::AuctionHousePackets::AuctionCommandResult::InitializeAuction(AuctionEntry* auction); - * - * @brief Initialize the following fields: AuctionId, Bid, AuctionOutBid, Bidder - * - * @param auction The relevant auction object - */ - public void InitializeAuction(AuctionEntry auction) - { - if (auction != null) - { - AuctionID = auction.Id; - Money = auction.bid == auction.buyout ? 0 : auction.bid; - MinIncrement = auction.bid == auction.buyout ? 0 : auction.GetAuctionOutBid(); - Guid = ObjectGuid.Create(HighGuid.Player, auction.bidder); - } - } - public override void Write() { _worldPacket.WriteUInt32(AuctionID); @@ -482,7 +460,7 @@ namespace Game.Network.Packets } } - class AuctionCommodityPriceUpdate : ServerPacket + class AuctionCommodityQuote : ServerPacket { public Optional TotalPrice; public Optional Quantity; @@ -490,7 +468,7 @@ namespace Game.Network.Packets public int Unknown830; public uint DesiredDelay; - public AuctionCommodityPriceUpdate() : base(ServerOpcodes.AuctionCommodityPriceUpdate) { } + public AuctionCommodityQuote() : base(ServerOpcodes.AuctionCommodityQuote) { } public override void Write() { @@ -546,16 +524,16 @@ namespace Game.Network.Packets } } - class AuctionListBucketItemsResult : ServerPacket + public class AuctionListBucketsResult : ServerPacket { public List Buckets = new List(); public uint DesiredDelay; public int Unknown830_0; public int Unknown830_1; - public int BrowseMode; + public AuctionHouseBrowseMode BrowseMode; public bool HasMoreResults; - public AuctionListBucketItemsResult() : base(ServerOpcodes.AuctionListBucketItemsResult) { } + public AuctionListBucketsResult() : base(ServerOpcodes.AuctionListBucketsResult) { } public override void Write() { @@ -563,7 +541,7 @@ namespace Game.Network.Packets _worldPacket.WriteUInt32(DesiredDelay); _worldPacket.WriteInt32(Unknown830_0); _worldPacket.WriteInt32(Unknown830_1); - _worldPacket.WriteBits(BrowseMode, 1); + _worldPacket.WriteBits((int)BrowseMode, 1); _worldPacket.WriteBit(HasMoreResults); _worldPacket.FlushBits(); @@ -572,12 +550,12 @@ namespace Game.Network.Packets } } - class AuctionListFavoriteItemsResult : ServerPacket + class AuctionFavoriteItems : ServerPacket { public uint DesiredDelay; public List Items = new List(); - public AuctionListFavoriteItemsResult() : base(ServerOpcodes.AuctionListFavoriteItemsResult) { } + public AuctionFavoriteItems() : base(ServerOpcodes.AuctionFavoriteItems) { } public override void Write() { @@ -596,9 +574,9 @@ namespace Game.Network.Packets public uint Unknown830; public uint TotalCount; public uint DesiredDelay; - public uint ListType; + public AuctionHouseListType ListType; public bool HasMoreResults; - public AuctionBucketKey BucketKey; + public AuctionBucketKey BucketKey = new AuctionBucketKey(); public AuctionListItemsResult() : base(ServerOpcodes.AuctionListItemsResult) { } @@ -608,7 +586,7 @@ namespace Game.Network.Packets _worldPacket.WriteUInt32(Unknown830); _worldPacket.WriteUInt32(TotalCount); _worldPacket.WriteUInt32(DesiredDelay); - _worldPacket.WriteBits(ListType, 2); + _worldPacket.WriteBits((int)ListType, 2); _worldPacket.WriteBit(HasMoreResults); _worldPacket.FlushBits(); @@ -673,7 +651,7 @@ namespace Game.Network.Packets } } - class AuctionReplicateResponse : ServerPacket + public class AuctionReplicateResponse : ServerPacket { public uint ChangeNumberCursor; public uint ChangeNumberGlobal; @@ -718,7 +696,21 @@ namespace Game.Network.Packets public Optional BattlePetSpeciesID = new Optional(); public Optional SuffixItemNameDescriptionID = new Optional(); - public void Read(WorldPacket data) + public AuctionBucketKey() { } + + public AuctionBucketKey(AuctionsBucketKey key) + { + ItemID = key.ItemId; + ItemLevel = key.ItemLevel; + + if (key.BattlePetSpeciesId != 0) + BattlePetSpeciesID.Set(key.BattlePetSpeciesId); + + if (key.SuffixItemNameDescriptionId != 0) + SuffixItemNameDescriptionID.Set(key.SuffixItemNameDescriptionId); + } + + public AuctionBucketKey(WorldPacket data) { data.ResetBitPos(); ItemID = data.ReadBits(20); @@ -735,7 +727,7 @@ namespace Game.Network.Packets BattlePetSpeciesID.Set(data.ReadUInt16()); if (SuffixItemNameDescriptionID.HasValue) - SuffixItemNameDescriptionID = data.ReadUInt16(); + SuffixItemNameDescriptionID.Set(data.ReadUInt16()); } public void Write(WorldPacket data) @@ -786,6 +778,12 @@ namespace Game.Network.Packets public AuctionHouseSortOrder SortOrder; public bool ReverseSort; + public AuctionSortDef(AuctionHouseSortOrder sortOrder, bool reverseSort) + { + SortOrder = sortOrder; + ReverseSort = reverseSort; + } + public AuctionSortDef(WorldPacket data) { data.ResetBitPos(); @@ -812,7 +810,7 @@ namespace Game.Network.Packets public uint ItemID; public uint ItemLevel; public uint BattlePetSpeciesID; - public uint ItemSuffix; + public uint SuffixItemNameDescriptionID; public AuctionFavoriteInfo(WorldPacket data) { @@ -820,7 +818,7 @@ namespace Game.Network.Packets ItemID = data.ReadUInt32(); ItemLevel = data.ReadUInt32(); BattlePetSpeciesID = data.ReadUInt32(); - ItemSuffix = data.ReadUInt32(); + SuffixItemNameDescriptionID = data.ReadUInt32(); } public void Write(WorldPacket data) @@ -829,7 +827,7 @@ namespace Game.Network.Packets data.WriteUInt32(ItemID); data.WriteUInt32(ItemLevel); data.WriteUInt32(BattlePetSpeciesID); - data.WriteUInt32(ItemSuffix); + data.WriteUInt32(SuffixItemNameDescriptionID); } } @@ -839,11 +837,11 @@ namespace Game.Network.Packets public ulong BidAmount; public ItemInstance Item; - public void Initialize(AuctionEntry auction, Item item) + public void Initialize(AuctionPosting auction) { AuctionID = auction.Id; - Item = new ItemInstance(item); - BidAmount = auction.bid; + Item = new ItemInstance(auction.Items[0]); + BidAmount = auction.BidAmount; } public void Write(WorldPacket data) @@ -859,7 +857,7 @@ namespace Game.Network.Packets public AuctionBucketKey Key; public int TotalQuantity; public ulong MinPrice; - public List ItemModifiedAppearanceIDs = new List(); + public List ItemModifiedAppearanceIDs = new List(); public Optional MaxBattlePetQuality; public Optional MaxBattlePetLevel; public Optional BattlePetBreedID; @@ -903,7 +901,7 @@ namespace Game.Network.Packets public int Charges; public List Enchantments = new List(); public int Flags; - public int AuctionID; + public uint AuctionID; public ObjectGuid Owner; public Optional MinBid; public Optional MinIncrement; @@ -947,7 +945,7 @@ namespace Game.Network.Packets data.WriteInt32(Count); data.WriteInt32(Charges); data.WriteInt32(Flags); - data.WriteInt32(AuctionID); + data.WriteUInt32(AuctionID); data.WritePackedGuid(Owner); data.WriteInt32(DurationLeft); data.WriteUInt8(DeleteReason); @@ -993,20 +991,20 @@ namespace Game.Network.Packets struct AuctionBidderNotification { - public int AuctionID; + public uint AuctionID; public ObjectGuid Bidder; public ItemInstance Item; - public void Initialize(AuctionEntry auction, Item item) + public void Initialize(AuctionPosting auction, Item item) { - AuctionID = (int)auction.Id; + AuctionID = auction.Id; Item = new ItemInstance(item); - Bidder = ObjectGuid.Create(HighGuid.Player, auction.bidder); + Bidder = auction.Bidder; } public void Write(WorldPacket data) { - data.WriteInt32(AuctionID); + data.WriteUInt32(AuctionID); data.WritePackedGuid(Bidder); Item.Write(data); } diff --git a/Source/Game/Network/Packets/ItemPackets.cs b/Source/Game/Network/Packets/ItemPackets.cs index b221dd30c..48047a1c2 100644 --- a/Source/Game/Network/Packets/ItemPackets.cs +++ b/Source/Game/Network/Packets/ItemPackets.cs @@ -883,7 +883,7 @@ namespace Game.Network.Packets public class ItemEnchantData { - public ItemEnchantData(int id, uint expiration, int charges, byte slot) + public ItemEnchantData(uint id, uint expiration, int charges, byte slot) { ID = id; Expiration = expiration; @@ -893,13 +893,13 @@ namespace Game.Network.Packets public void Write(WorldPacket data) { - data.WriteInt32(ID); + data.WriteUInt32(ID); data.WriteUInt32(Expiration); data.WriteInt32(Charges); data.WriteUInt8(Slot); } - public int ID; + public uint ID; public uint Expiration; public int Charges; public byte Slot; diff --git a/Source/Game/Network/Packets/MailPackets.cs b/Source/Game/Network/Packets/MailPackets.cs index 018c386da..94b092b1b 100644 --- a/Source/Game/Network/Packets/MailPackets.cs +++ b/Source/Game/Network/Packets/MailPackets.cs @@ -326,7 +326,7 @@ namespace Game.Network.Packets if (item.GetEnchantmentId(slot) == 0) continue; - Enchants.Add(new ItemEnchantData((int)item.GetEnchantmentId(slot), item.GetEnchantmentDuration(slot), (int)item.GetEnchantmentCharges(slot), (byte)slot)); + Enchants.Add(new ItemEnchantData(item.GetEnchantmentId(slot), item.GetEnchantmentDuration(slot), (int)item.GetEnchantmentCharges(slot), (byte)slot)); } byte i = 0; diff --git a/Source/Game/Scripting/CoreScripts.cs b/Source/Game/Scripting/CoreScripts.cs index ed977cc66..e008b6f64 100644 --- a/Source/Game/Scripting/CoreScripts.cs +++ b/Source/Game/Scripting/CoreScripts.cs @@ -482,16 +482,16 @@ namespace Game.Scripting } // Called when an auction is added to an auction house. - public virtual void OnAuctionAdd(AuctionHouseObject ah, AuctionEntry entry) { } + public virtual void OnAuctionAdd(AuctionHouseObject ah, AuctionPosting auction) { } // Called when an auction is removed from an auction house. - public virtual void OnAuctionRemove(AuctionHouseObject ah, AuctionEntry entry) { } + public virtual void OnAuctionRemove(AuctionHouseObject ah, AuctionPosting auction) { } // Called when an auction was succesfully completed. - public virtual void OnAuctionSuccessful(AuctionHouseObject ah, AuctionEntry entry) { } + public virtual void OnAuctionSuccessful(AuctionHouseObject ah, AuctionPosting auction) { } // Called when an auction expires. - public virtual void OnAuctionExpire(AuctionHouseObject ah, AuctionEntry entry) { } + public virtual void OnAuctionExpire(AuctionHouseObject ah, AuctionPosting auction) { } } public class ConditionScript : ScriptObject diff --git a/Source/Game/Scripting/ScriptManager.cs b/Source/Game/Scripting/ScriptManager.cs index 82e0edf7b..83b88e0b9 100644 --- a/Source/Game/Scripting/ScriptManager.cs +++ b/Source/Game/Scripting/ScriptManager.cs @@ -975,29 +975,29 @@ namespace Game.Scripting } // AuctionHouseScript - public void OnAuctionAdd(AuctionHouseObject ah, AuctionEntry entry) + public void OnAuctionAdd(AuctionHouseObject ah, AuctionPosting auction) { Cypher.Assert(ah != null); - Cypher.Assert(entry != null); - ForEach(p => p.OnAuctionAdd(ah, entry)); + Cypher.Assert(auction != null); + ForEach(p => p.OnAuctionAdd(ah, auction)); } - public void OnAuctionRemove(AuctionHouseObject ah, AuctionEntry entry) + public void OnAuctionRemove(AuctionHouseObject ah, AuctionPosting auction) { Cypher.Assert(ah != null); - Cypher.Assert(entry != null); - ForEach(p => p.OnAuctionRemove(ah, entry)); + Cypher.Assert(auction != null); + ForEach(p => p.OnAuctionRemove(ah, auction)); } - public void OnAuctionSuccessful(AuctionHouseObject ah, AuctionEntry entry) + public void OnAuctionSuccessful(AuctionHouseObject ah, AuctionPosting auction) { Cypher.Assert(ah != null); - Cypher.Assert(entry != null); - ForEach(p => p.OnAuctionSuccessful(ah, entry)); + Cypher.Assert(auction != null); + ForEach(p => p.OnAuctionSuccessful(ah, auction)); } - public void OnAuctionExpire(AuctionHouseObject ah, AuctionEntry entry) + public void OnAuctionExpire(AuctionHouseObject ah, AuctionPosting auction) { Cypher.Assert(ah != null); - Cypher.Assert(entry != null); - ForEach(p => p.OnAuctionExpire(ah, entry)); + Cypher.Assert(auction != null); + ForEach(p => p.OnAuctionExpire(ah, auction)); } // ConditionScript diff --git a/Source/Game/Server/WorldManager.cs b/Source/Game/Server/WorldManager.cs index cfff3c9fd..3929c694d 100644 --- a/Source/Game/Server/WorldManager.cs +++ b/Source/Game/Server/WorldManager.cs @@ -691,12 +691,13 @@ namespace Game Log.outInfo(LogFilter.ServerLoading, "Loading Completed Achievements..."); Global.AchievementMgr.LoadCompletedAchievements(); - // Load dynamic data tables from the database - Log.outInfo(LogFilter.ServerLoading, "Loading Item Auctions..."); - Global.AuctionMgr.LoadAuctionItems(); + // Load before guilds and arena teams + Log.outInfo(LogFilter.ServerLoading, "Loading character cache store..."); + Global.CharacterCacheStorage.LoadCharacterCacheStorage(); + // Load dynamic data tables from the database Log.outInfo(LogFilter.ServerLoading, "Loading Auctions..."); - Global.AuctionMgr.LoadAuctions(); + Global.AuctionHouseMgr.LoadAuctions(); if (WorldConfig.GetBoolValue(WorldCfg.BlackmarketEnabled)) { @@ -707,10 +708,6 @@ namespace Game Global.BlackMarketMgr.LoadAuctions(); } - // Load before guilds and arena teams - Log.outInfo(LogFilter.ServerLoading, "Loading character cache store..."); - Global.CharacterCacheStorage.LoadCharacterCacheStorage(); - Log.outInfo(LogFilter.ServerLoading, "Loading Guild rewards..."); Global.GuildMgr.LoadGuildRewards(); @@ -1184,7 +1181,7 @@ namespace Game } // Handle expired auctions - Global.AuctionMgr.Update(); + Global.AuctionHouseMgr.Update(); } if (m_timers[WorldTimers.AuctionsPending].Passed()) diff --git a/sql/base/characters_database.sql b/sql/base/characters_database.sql index 5e8d47955..23240d7d9 100644 --- a/sql/base/characters_database.sql +++ b/sql/base/characters_database.sql @@ -158,6 +158,53 @@ LOCK TABLES `arena_team_member` WRITE; /*!40000 ALTER TABLE `arena_team_member` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Table structure for table `auction_bidders` +-- + +DROP TABLE IF EXISTS `auction_bidders`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auction_bidders` ( + `auctionId` int unsigned NOT NULL, + `playerGuid` bigint unsigned NOT NULL, + PRIMARY KEY (`auctionId`,`playerGuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auction_bidders` +-- + +LOCK TABLES `auction_bidders` WRITE; +/*!40000 ALTER TABLE `auction_bidders` DISABLE KEYS */; +/*!40000 ALTER TABLE `auction_bidders` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `auction_items` +-- + +DROP TABLE IF EXISTS `auction_items`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `auction_items` ( + `auctionId` int unsigned NOT NULL, + `itemGuid` bigint unsigned NOT NULL, + PRIMARY KEY (`auctionId`,`itemGuid`), + UNIQUE KEY `idx_itemGuid` (`itemGuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `auction_items` +-- + +LOCK TABLES `auction_items` WRITE; +/*!40000 ALTER TABLE `auction_items` DISABLE KEYS */; +/*!40000 ALTER TABLE `auction_items` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Table structure for table `auctionhouse` -- @@ -167,17 +214,16 @@ DROP TABLE IF EXISTS `auctionhouse`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `auctionhouse` ( `id` int(10) unsigned NOT NULL DEFAULT '0', - `auctioneerguid` bigint(20) unsigned NOT NULL DEFAULT '0', - `itemguid` bigint(20) unsigned NOT NULL DEFAULT '0', - `itemowner` bigint(20) unsigned NOT NULL DEFAULT '0', - `buyoutprice` bigint(20) unsigned NOT NULL DEFAULT '0', - `time` int(10) unsigned NOT NULL DEFAULT '0', - `buyguid` bigint(20) unsigned NOT NULL DEFAULT '0', - `lastbid` bigint(20) unsigned NOT NULL DEFAULT '0', - `startbid` bigint(20) unsigned NOT NULL DEFAULT '0', + `auctionHouseId` int(10) unsigned NOT NULL DEFAULT '0', + `owner` bigint(20) unsigned NOT NULL DEFAULT '0', + `bidder` bigint(20) unsigned NOT NULL DEFAULT '0', + `minBid` bigint(20) unsigned NOT NULL DEFAULT '0', + `buyoutOrUnitPrice` bigint(20) unsigned NOT NULL DEFAULT '0', `deposit` bigint(20) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `item_guid` (`itemguid`) + `bidAmount` bigint(20) unsigned NOT NULL DEFAULT '0', + `startTime` int(10) unsigned NOT NULL DEFAULT '0', + `endTime` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*!40101 SET character_set_client = @saved_cs_client */; @@ -733,6 +779,33 @@ LOCK TABLES `character_equipmentsets` WRITE; /*!40000 ALTER TABLE `character_equipmentsets` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Table structure for table `character_favorite_auctions` +-- + +DROP TABLE IF EXISTS `character_favorite_auctions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `character_favorite_auctions` ( + `guid` bigint(20) unsigned NOT NULL, + `order` int(10) unsigned NOT NULL DEFAULT '0', + `itemId` int(10) unsigned NOT NULL DEFAULT '0', + `itemLevel` int(10) unsigned NOT NULL DEFAULT '0', + `battlePetSpeciesId` int(10) unsigned NOT NULL DEFAULT '0', + `suffixItemNameDescriptionId` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `character_favorite_auctions` +-- + +LOCK TABLES `character_favorite_auctions` WRITE; +/*!40000 ALTER TABLE `character_favorite_auctions` DISABLE KEYS */; +/*!40000 ALTER TABLE `character_favorite_auctions` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Table structure for table `character_fishingsteps` -- @@ -3696,7 +3769,9 @@ INSERT INTO `updates` VALUES ('2019_11_22_00_characters.sql','95DFA71DBD75542C098CD86E9C0051C9690902F0','RELEASED','2019-11-20 15:10:12',0), ('2019_11_30_00_characters.sql','D0678E62B651AECA60C2DD6989BF80BD999AD12B','RELEASED','2019-11-29 22:42:01',0), ('2019_12_05_00_characters.sql','EA381C9634A5646A3168F15DF4E06A708A622762','RELEASED','2019-12-05 20:56:58',0), -('2020_02_17_00_characters.sql','E1519A81D35F19B48B3C75A83A270CB4BA0B84F2','RELEASED','2020-02-17 21:55:17',0); +('2020_02_17_00_characters.sql','E1519A81D35F19B48B3C75A83A270CB4BA0B84F2','RELEASED','2020-02-17 21:55:17',0), +('2020_04_20_00_characters.sql','977B5E0C894E0A7E80B2A9626F17CA636A69BD22','RELEASED','2020-04-20 19:08:18',0), +('2020_04_24_00_characters.sql','85E2E0395A9457A53D73A9E0A7BB39B7E4C429BF','RELEASED','2020-04-24 22:04:59',0); /*!40000 ALTER TABLE `updates` ENABLE KEYS */; UNLOCK TABLES; diff --git a/sql/updates/characters/master/2020_04_20_00_characters.sql b/sql/updates/characters/master/2020_04_20_00_characters.sql new file mode 100644 index 000000000..689f2ea4b --- /dev/null +++ b/sql/updates/characters/master/2020_04_20_00_characters.sql @@ -0,0 +1,46 @@ +ALTER TABLE `auctionhouse` ADD `auctionHouseId` int(10) unsigned NOT NULL DEFAULT '0' AFTER `id`; +ALTER TABLE `auctionhouse` DROP `auctioneerguid`; + +-- temporarily mark all auctions as coming from neutral AH (not goblin one) and expired +UPDATE `auctionhouse` SET `auctionHouseId`=1, `time`=UNIX_TIMESTAMP(); + +DROP TABLE IF EXISTS `auction_bidders`; +CREATE TABLE `auction_bidders` ( + `auctionId` int unsigned NOT NULL, + `playerGuid` bigint unsigned NOT NULL, + PRIMARY KEY (`auctionId`,`playerGuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `auction_bidders` SELECT `id`, `buyguid` FROM `auctionhouse`; + +DROP TABLE IF EXISTS `auction_items`; +CREATE TABLE `auction_items` ( + `auctionId` int unsigned NOT NULL, + `itemGuid` bigint unsigned NOT NULL, + PRIMARY KEY (`auctionId`,`itemGuid`), + UNIQUE KEY `idx_itemGuid` (`itemGuid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `character_favorite_auctions`; +CREATE TABLE `character_favorite_auctions` ( + `guid` bigint(20) unsigned NOT NULL, + `order` int(10) unsigned NOT NULL DEFAULT '0', + `itemId` int(10) unsigned NOT NULL DEFAULT '0', + `itemLevel` int(10) unsigned NOT NULL DEFAULT '0', + `battlePetSpeciesId` int(10) unsigned NOT NULL DEFAULT '0', + `suffixItemNameDescriptionId` int(10) unsigned NOT NULL DEFAULT '0', + PRIMARY KEY (`guid`,`order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `auction_items` SELECT `id`, `itemguid` FROM `auctionhouse`; + +ALTER TABLE `auctionhouse` DROP INDEX `item_guid`; +ALTER TABLE `auctionhouse` DROP `itemguid`; +ALTER TABLE `auctionhouse` CHANGE `itemowner` `owner` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `auctionHouseId`; +ALTER TABLE `auctionhouse` CHANGE `buyguid` `bidder` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `owner`; +ALTER TABLE `auctionhouse` CHANGE `startbid` `minBid` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `bidder`; +ALTER TABLE `auctionhouse` CHANGE `buyoutprice` `buyoutOrUnitPrice` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `lastbid`; +ALTER TABLE `auctionhouse` MODIFY `deposit` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `buyoutOrUnitPrice`; +ALTER TABLE `auctionhouse` CHANGE `lastbid` `bidAmount` bigint(20) unsigned NOT NULL DEFAULT '0' AFTER `deposit`; +ALTER TABLE `auctionhouse` ADD `startTime` int(10) unsigned NOT NULL DEFAULT '0' AFTER `bidAmount`; +ALTER TABLE `auctionhouse` CHANGE `time` `endTime` int(10) unsigned NOT NULL DEFAULT '0' AFTER `startTime`;